#!/usr/bin/ruby

require 'optparse'
require 'json'

def do_analysis(files, options = {})
  failed_steps = Array.new
  files.each do |path|
    File.open(path, "r") do |raw_json|
      features = JSON.load(raw_json)
      features.each do |feature|
        feature['elements'].each do |scenario|
          scenario['steps'].each do |step|
            if step['result']['status'] == 'failed'
              trace = step['result']['error_message']
              if options[:match]   && not(options[:match].match(trace)) ||
                 options[:exclude] &&     options[:exclude].match(trace)
                break
              end
              failed_steps << {
                'step'     => step,
                'scenario' => scenario,
                'feature'  => feature,
              }
              # The steps following a failure will be skipped.
              break
            end
          end
        end
      end
    end
  end
  # Sort (lexically) by step, scenario, feature-file.
  sorted_failed_steps = failed_steps.sort do |a, b|
    "a['step'] a['scenario'] a['feature']" <=>
      "b['step'] b['scenario'] b['feature']"
  end
  return sorted_failed_steps
end

def print_verbose_steps(failed_steps)
  puts (failed_steps.map{ |x| x.values }.map do |step, scenario, feature|
    <<EOF
Step: #{step['name']}
  Defined at: #{step['match']['location']}
  Used in:    Scenario: #{scenario['name']}
              #{feature['uri']}:#{feature['line']}
  Error trace:
#{step['result']['error_message'].gsub(/^/, " "*4)}
EOF
  end).join("\n")
end

def print_step_failure_summary(failed_steps)
  failure_counter = Hash.new
  failed_steps.map{ |x| x.values }.each do |step, scenario, feature|
    unless failure_counter[step['name']]
      failure_counter[step['name']] = Hash.new(0)
    end
    failure_counter[step['name']][scenario['name']] += 1
  end
  # Sort by step with most failures.
  sorted_failure_counter = failure_counter.to_a.sort do |a, b|
    b[1].values.inject(:+) <=> a[1].values.inject(:+)
  end
  puts "Step failure breakdown (total: #{failed_steps.size}):"
  sorted_failure_counter.each do |step, scenario_counter|
    total = scenario_counter.values.inject(:+)
    puts "* #{total}\tStep: #{step}"
    # Sort by scenario with most failures.
    sorted_scenario_counter = scenario_counter.to_a.sort do |a, b|
      b.last <=> a.last
    end
    sorted_scenario_counter.each do |scenario, count|
      puts "  - #{count}\t  Scenario: #{scenario}"
    end
  end
end

options = {}
options[:summary] = true

opt_parser = OptionParser.new do |opts|
  opts.banner = "Usage: json-analysis [opts] LOGS..."
  opts.separator ""
  opts.separator "Options:"

  opts.on("-h", "--help", "Show this message") do
    puts opts
    exit
  end

  opts.on("-m", "--match REGEX",
          "Keep only errors with a trace matching REGEX") do |regex|
    options[:match] = Regexp.new(regex)
  end

  opts.on("-x", "--exclude REGEX",
          "Exclude errors with a trace matching REGEX") do |regex|
    options[:exclude] = Regexp.new(regex)
  end

  opts.on("-s", "--steps", "Print each step failure") do
    options[:verbose] = true
  end

  opts.on("-n", "--no-summary",
          "Do not print the summary. Implies --steps.") do
    options[:summary] = false
    options[:verbose] = true
  end
end
opt_parser.parse!(ARGV)

if ARGV.size < 1
  STDERR.puts "#{opt_parser.program_name}: " +
              "At least one .json file must be specified"
  exit 1
end

failed_steps = do_analysis(ARGV, options)
print_verbose_steps(failed_steps) if options[:verbose]
puts if options[:verbose] and options[:summary]
print_step_failure_summary(failed_steps) if options[:summary]
