Brakeman.load_brakeman_dependency 'terminal-table'

class Brakeman::Report::Table < Brakeman::Report::Base
  def initialize *args
    super
    @table = Terminal::Table
  end

  def generate_report
    summary_option = tracker.options[:summary_only]
    out = +""

    unless summary_option == :no_summary
      out << text_header <<
        "\n\n+SUMMARY+\n\n" <<
        truncate_table(generate_overview.to_s) << "\n\n" <<
        truncate_table(generate_warning_overview.to_s) << "\n"
    end

    #Return output early if only summarizing
    if summary_option == :summary_only or summary_option == true
      return out
    end

    if tracker.options[:report_routes] or tracker.options[:debug]
      out << "\n+CONTROLLERS+\n" <<
      truncate_table(generate_controllers.to_s) << "\n"
    end

    if tracker.options[:debug]
      out << "\n+TEMPLATES+\n\n" <<
      truncate_table(generate_templates.to_s) << "\n"
    end

    output_table("+Obsolete Ignore Entries+", generate_obsolete, out)
    output_table("+Errors+", generate_errors, out)
    output_table("+SECURITY WARNINGS+", generate_warnings, out)
    output_table("Controller Warnings:", generate_controller_warnings, out)
    output_table("Model Warnings:", generate_model_warnings, out)
    output_table("View Warnings:", generate_template_warnings, out)

    out << "\n"
    out
  end

  def output_table title, result, output
    return unless result

    output << "\n\n#{title}\n\n#{truncate_table(result.to_s)}"
  end

  def generate_overview
    num_warnings = all_warnings.length

    @table.new(:headings => ['Scanned/Reported', 'Total']) do |t|
      t.add_row ['Controllers', tracker.controllers.length]
      t.add_row ['Models', tracker.models.length - 1]
      t.add_row ['Templates', number_of_templates(@tracker)]
      t.add_row ['Errors', tracker.errors.length]
      t.add_row ['Security Warnings', "#{num_warnings} (#{warnings_summary[:high_confidence]})"]
      t.add_row ['Ignored Warnings', ignored_warnings.length] unless ignored_warnings.empty?
    end
  end

  #Generate table of how many warnings of each warning type were reported
  def generate_warning_overview
    types = warnings_summary.keys
    types.delete :high_confidence
    values = types.sort.collect{|warning_type| [warning_type, warnings_summary[warning_type]] }
    locals = {:types => types, :warnings_summary => warnings_summary}

    render_array('warning_overview', ['Warning Type', 'Total'], values, locals)
  end

  #Generate table of controllers and routes found for those controllers
  def generate_controllers
    controller_rows = controller_information

    cols = ['Name', 'Parent', 'Includes', 'Routes']

    locals = {:controller_rows => controller_rows}
    values = controller_rows.collect{|row| row.values_at(*cols) }
    render_array('controller_overview', cols, values, locals)
  end

  #Generate table of errors or return nil if no errors
  def generate_errors
    values = tracker.errors.collect{|error| [error[:error], error[:backtrace][0]]}
    render_array('error_overview', ['Error', 'Location'], values, {:tracker => tracker})
  end

  def generate_obsolete
    values = tracker.unused_fingerprints.collect{|fingerprint| [fingerprint] }
    render_array('obsolete_ignore_entries', ['fingerprint'], values, {:tracker => tracker})
  end

  def generate_warnings
    render_warnings generic_warnings,
                    :warning,
                    'security_warnings',
                    ["Confidence", "Class", "Method", "Warning Type", "CWE ID", "Message"],
                    'Class'
  end

  #Generate table of template warnings or return nil if no warnings
  def generate_template_warnings
    render_warnings template_warnings,
                    :template,
                    'view_warnings',
                    ['Confidence', 'Template', 'Warning Type', "CWE ID", 'Message'],
                    'Template'

  end

  #Generate table of model warnings or return nil if no warnings
  def generate_model_warnings
    render_warnings model_warnings,
                    :model,
                    'model_warnings',
                    ['Confidence', 'Model', 'Warning Type', "CWE ID", 'Message'],
                    'Model'
  end

  #Generate table of controller warnings or nil if no warnings
  def generate_controller_warnings
    render_warnings controller_warnings,
                    :controller,
                    'controller_warnings',
                    ['Confidence', 'Controller', 'Warning Type', "CWE ID", 'Message'],
                    'Controller'
  end

  def generate_ignored_warnings
    render_warnings ignored_warnings,
                    :ignored,
                    'ignored_warnings',
                    ['Confidence', 'Warning Type', "CWE ID", 'File', 'Message'],
                    'Warning Type'
  end

  def render_warnings warnings, type, template, cols, sort_col
    unless warnings.empty?
      rows = sort(convert_to_rows(warnings, type), sort_col)

      values = rows.collect { |row| row.values_at(*cols) }

      locals = { :warnings => rows }

      render_array(template, cols, values, locals)
    else
      nil
    end
  end

  #Generate listings of templates and their output
  def generate_templates
    out_processor = Brakeman::OutputProcessor.new
    template_rows = {}
    tracker.templates.each do |name, template|
      template.each_output do |out|
        out = out_processor.format out
        template_rows[name] ||= []
        template_rows[name] << out.gsub("\n", ";").gsub(/\s+/, " ")
      end
    end

    template_rows = template_rows.sort_by{|name, value| name.to_s}

    output = +''
    template_rows.each do |template|
      output << template.first.to_s << "\n\n"
      table = @table.new(:headings => ['Output']) do |t|
        # template[1] is an array of calls
        template[1].each do |v|
          t.add_row [v]
        end
      end

      output << table.to_s << "\n\n"
    end

    output
  end

  def convert_to_rows warnings, type = :warning
    warnings.map do |warning|
      w = warning.to_row type

      case type
      when :warning
        convert_warning w, warning
      when :ignored
        convert_ignored_warning w, warning
      when :template
        convert_template_warning w, warning
      else
        convert_warning w, warning
      end
    end
  end

  def convert_ignored_warning warning, original
    convert_warning warning, original
  end

  def convert_template_warning warning, original
    convert_warning warning, original
  end

  def sort rows, sort_col
    stabilizer = 0
    rows.sort_by do |row|
      stabilizer += 1

      row.values_at("Confidence", "Warning Type", sort_col) << stabilizer
    end
  end

  def render_array template, headings, value_array, locals
    return if value_array.empty?

    @table.new(:headings => headings) do |t|
      value_array.each { |value_row| t.add_row value_row }
    end
  end

  def convert_warning warning, original
    warning["Message"] = text_message original, warning["Message"]

    warning
  end

  #Escape warning message and highlight user input in text output
  def text_message warning, message
    message = message.to_s

    if warning.line
      message << " near line #{warning.line}"
    end

    if warning.code
      if @highlight_user_input and warning.user_input
        code = warning.format_with_user_input do |user_input, user_input_string|
          "+#{user_input_string}+"
        end
      else
        code = warning.format_code
      end

      message << ": #{code}"
    end

    message
  end

  #Generate header for text output
  def text_header
    <<-HEADER

+BRAKEMAN REPORT+

Application path: #{tracker.app_path}
Rails version: #{rails_version}
Brakeman version: #{Brakeman::Version}
Started at #{tracker.start_time}
Duration: #{tracker.duration} seconds
Checks run: #{checks.checks_run.sort.join(", ")}
HEADER
  end

  def truncate_table str
    @terminal_width ||= if @tracker.options[:table_width]
                          @tracker.options[:table_width]
                        elsif $stdin && $stdin.tty?
                          Brakeman.load_brakeman_dependency 'highline'
                          ::HighLine.default_instance.terminal.terminal_size[0]
                        else
                          80
                        end
    lines = str.lines

    lines.map do |line|
      if line.chomp.length > @terminal_width
        line[0..(@terminal_width - 3)] + ">>\n"
      else
        line
      end
    end.join
  end
end
