require 'ansi/code'

module Minitest
  module Reporters
    # A reporter identical to the standard Minitest reporter except with more
    # colors.
    #
    # Based upon Ryan Davis of Seattle.rb's Minitest (MIT License).
    #
    # @see https://github.com/seattlerb/minitest Minitest
    class DefaultReporter < BaseReporter
      include RelativePosition

      def initialize(options = {})
        super
        @detailed_skip = options.fetch(:detailed_skip, true)
        @slow_count = options.fetch(:slow_count, 0)
        @slow_suite_count = options.fetch(:slow_suite_count, 0)
        @fast_fail = options.fetch(:fast_fail, false)
        @options = options
      end

      def start
        super
        puts
        puts "# Running tests:"
        puts
      end

      def before_test(test)
        super
        print "\n#{test.class}##{test.name} " if options[:verbose]
      end

      def record(test)
        super

        print "#{"%.2f" % test.time} = " if options[:verbose]

        print(if test.passed?
          green('.')
        elsif test.skipped?
          yellow('S')
        elsif test.failure
          red('F')
        end)

        if @fast_fail && (test.skipped? || test.failure)
          puts
          print_failure(test)
        end
      end

      def report
        super
        status_line = "Finished tests in %.6fs, %.4f tests/s, %.4f assertions/s." %
          [total_time, count / total_time, assertions / total_time]

        puts
        puts
        puts colored_for(suite_result, status_line)

        unless @fast_fail
          tests.reject(&:passed?).each do |test|
            puts
            print_failure(test)
          end
        end

        if @slow_count > 0
          slow_tests = tests.sort_by(&:time).reverse.take(@slow_count)

          puts
          puts "Slowest tests:"
          puts

          slow_tests.each do |test|
            puts "%.6fs %s" % [test.time, "#{test.name}##{test.class}"]
          end
        end

        if @slow_suite_count > 0
          slow_suites = @suite_times.sort_by { |x| x[1] }.reverse.take(@slow_suite_count)

          puts
          puts "Slowest test classes:"
          puts

          slow_suites.each do |slow_suite|
            puts "%.6fs %s" % [slow_suite[1], slow_suite[0]]
          end
        end

        puts
        print colored_for(suite_result, result_line)
        puts
      end

      def print_failure(test)
        puts colored_for(result(test), message_for(test))
      end

      private

      def color?
        return @color if defined?(@color)
        @color = @options.fetch(:color) do
          io.tty? && (
            ENV["TERM"] =~ /^screen|color/ ||
            ENV["EMACS"] == "t"
          )
        end
      end

      def green(string)
        color? ? ANSI::Code.green(string) : string
      end

      def yellow(string)
        color? ? ANSI::Code.yellow(string) : string
      end

      def red(string)
        color? ? ANSI::Code.red(string) : string
      end

      def colored_for(result, string)
        case result
        when :fail, :error; red(string)
        when :skip; yellow(string)
        else green(string)
        end
      end

      def suite_result
        case
        when failures > 0; :fail
        when errors > 0; :error
        when skips > 0; :skip
        else :pass
        end
      end

      def location(exception)
        last_before_assertion = ''

        exception.backtrace.reverse_each do |s|
          break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
          last_before_assertion = s
        end

        last_before_assertion.sub(/:in .*$/, '')
      end

      def message_for(test)
        e = test.failure

        if test.skipped?
          if @detailed_skip
            "Skipped:\n#{test.class}##{test.name} [#{location(e)}]:\n#{e.message}"
          end
        elsif test.error?
          "Error:\n#{test.class}##{test.name}:\n#{e.message}"
        else
          "Failure:\n#{test.class}##{test.name} [#{test.failure.location}]\n#{e.class}: #{e.message}"
        end
      end

      def result_line
        '%d tests, %d assertions, %d failures, %d errors, %d skips' %
          [count, assertions, failures, errors, skips]
      end
    end
  end
end
