require "minitest/autorun"
require "minitest/metametameta"
require "forwardable"

class Runnable
  def woot
    assert true
  end
end

class TestMinitestReporter < MetaMetaMetaTestCase

  attr_accessor :r, :io

  def new_composite_reporter
    # Ruby bug in older versions of 2.2 & 2.3 on all platforms
    # Latest Windows builds were 2.2.6 and 2.3.3.  Latest Ruby releases were
    # 2.2.10 and 2.3.8.
    skip if windows? && RUBY_VERSION < '2.4'
    reporter = Minitest::CompositeReporter.new
    reporter << Minitest::SummaryReporter.new(self.io)
    reporter << Minitest::ProgressReporter.new(self.io)

    # eg reporter.results -> reporters.first.results
    reporter.extend Forwardable
    reporter.delegate :first => :reporters
    reporter.delegate %i[results count assertions options to_s] => :first

    reporter
  end

  def setup
    super
    self.io = StringIO.new("")
    self.r  = new_composite_reporter
  end

  def error_test
    unless defined? @et then
      @et = Minitest::Test.new(:woot)
      @et.failures << Minitest::UnexpectedError.new(begin
                                                      raise "no"
                                                    rescue => e
                                                      e
                                                    end)
      @et = Minitest::Result.from @et
    end
    @et
  end

  def system_stack_error_test
    unless defined? @sse then

      ex = SystemStackError.new

      pre  = ("a".."c").to_a
      mid  = ("aa".."ad").to_a * 67
      post = ("d".."f").to_a
      ary  = pre + mid + post

      ex.set_backtrace ary

      @sse = Minitest::Test.new(:woot)
      @sse.failures << Minitest::UnexpectedError.new(ex)
      @sse = Minitest::Result.from @sse
    end
    @sse
  end

  def fail_test
    unless defined? @ft then
      @ft = Minitest::Test.new(:woot)
      @ft.failures <<   begin
                          raise Minitest::Assertion, "boo"
                        rescue Minitest::Assertion => e
                          e
                        end
      @ft = Minitest::Result.from @ft
    end
    @ft
  end

  def passing_test
    @pt ||= Minitest::Result.from Minitest::Test.new(:woot)
  end

  def passing_test_with_metadata
    test = Minitest::Test.new(:woot)
    test.metadata[:meta] = :data
    @pt ||= Minitest::Result.from test
  end

  def skip_test
    unless defined? @st then
      @st = Minitest::Test.new(:woot)
      @st.failures << begin
                        raise Minitest::Skip
                      rescue Minitest::Assertion => e
                        e
                      end
      @st = Minitest::Result.from @st
    end
    @st
  end

  def test_to_s
    r.record passing_test
    r.record fail_test
    assert_match "woot", r.to_s
  end

  def test_options_skip_F
    r.options[:skip] = "F"

    r.record passing_test
    r.record fail_test

    refute_match "woot", r.to_s
  end

  def test_options_skip_E
    r.options[:skip] = "E"

    r.record passing_test
    r.record error_test

    refute_match "RuntimeError: no", r.to_s
  end

  def test_passed_eh_empty
    assert_predicate r, :passed?
  end

  def test_passed_eh_failure
    r.results << fail_test

    refute_predicate r, :passed?
  end

  SKIP_MSG = "\n\nYou have skipped tests. Run with --verbose for details."

  def test_passed_eh_error
    r.start

    r.results << error_test

    refute_predicate r, :passed?

    r.report

    refute_match SKIP_MSG, io.string
  end

  def test_passed_eh_skipped
    r.start
    r.results << skip_test
    assert r.passed?

    restore_env do
      r.report
    end

    assert_match SKIP_MSG, io.string
  end

  def test_passed_eh_skipped_verbose
    r.options[:verbose] = true

    r.start
    r.results << skip_test
    assert r.passed?
    r.report

    refute_match SKIP_MSG, io.string
  end

  def test_start
    r.start

    exp = "Run options: \n\n# Running:\n\n"

    assert_equal exp, io.string
  end

  def test_record_pass
    r.record passing_test

    assert_equal ".", io.string
    assert_empty r.results
    assert_equal 1, r.count
    assert_equal 0, r.assertions
  end

  def test_record_pass_with_metadata
    reporter = self.r

    def reporter.metadata
      @metadata
    end

    def reporter.record result
      super
      @metadata = result.metadata if result.metadata?
    end

    r.record passing_test_with_metadata

    exp = { :meta => :data }
    assert_equal exp, reporter.metadata

    assert_equal ".", io.string
    assert_empty r.results
    assert_equal 1, r.count
    assert_equal 0, r.assertions
  end

  def test_record_fail
    fail_test = self.fail_test
    r.record fail_test

    assert_equal "F", io.string
    assert_equal [fail_test], r.results
    assert_equal 1, r.count
    assert_equal 0, r.assertions
  end

  def test_record_error
    error_test = self.error_test
    r.record error_test

    assert_equal "E", io.string
    assert_equal [error_test], r.results
    assert_equal 1, r.count
    assert_equal 0, r.assertions
  end

  def test_record_skip
    skip_test = self.skip_test
    r.record skip_test

    assert_equal "S", io.string
    assert_equal [skip_test], r.results
    assert_equal 1, r.count
    assert_equal 0, r.assertions
  end

  def test_report_empty
    r.start
    r.report

    exp = clean <<-EOM
      Run options:

      # Running:



      Finished in 0.00

      0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
    EOM

    assert_equal exp, normalize_output(io.string)
  end

  def test_report_passing
    r.start
    r.record passing_test
    r.report

    exp = clean <<-EOM
      Run options:

      # Running:

      .

      Finished in 0.00

      1 runs, 0 assertions, 0 failures, 0 errors, 0 skips
    EOM

    assert_equal exp, normalize_output(io.string)
  end

  def test_report_failure
    r.start
    r.record fail_test
    r.report

    exp = clean <<-EOM
      Run options:

      # Running:

      F

      Finished in 0.00

        1) Failure:
      Minitest::Test#woot [FILE:LINE]:
      boo

      1 runs, 0 assertions, 1 failures, 0 errors, 0 skips
    EOM

    assert_equal exp, normalize_output(io.string)
  end

  def test_report_error
    r.start
    r.record error_test
    r.report

    exp = clean <<-EOM
      Run options:

      # Running:

      E

      Finished in 0.00

        1) Error:
      Minitest::Test#woot:
      RuntimeError: no
          FILE:LINE:in `error_test'
          FILE:LINE:in `test_report_error'

      1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
    EOM

    assert_equal exp, normalize_output(io.string)
  end

  def test_report_error__sse
    r.start
    r.record system_stack_error_test
    r.report

    exp = clean <<-EOM
      Run options:

      # Running:

      E

      Finished in 0.00

        1) Error:
      Minitest::Test#woot:
      SystemStackError: 274 -> 12
          a
          b
          c
           +->> 67 cycles of 4 lines:
           | aa
           | ab
           | ac
           | ad
           +-<<
          d
          e
          f

      1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
    EOM

    assert_equal exp, normalize_output(io.string)
  end

  def test_report_skipped
    r.start
    r.record skip_test

    restore_env do
      r.report
    end

    exp = clean <<-EOM
      Run options:

      # Running:

      S

      Finished in 0.00

      1 runs, 0 assertions, 0 failures, 0 errors, 1 skips

      You have skipped tests. Run with --verbose for details.
    EOM

    assert_equal exp, normalize_output(io.string)
  end

  def test_report_failure_uses_backtrace_filter
    filter = Minitest::BacktraceFilter.new
    def filter.filter _bt
      ["foo.rb:123:in `foo'"]
    end

    with_backtrace_filter filter do
      r.start
      r.record fail_test
      r.report
    end

    exp = "Minitest::Test#woot [foo.rb:123]"

    assert_includes io.string, exp
  end

  def test_report_failure_uses_backtrace_filter_complex_sorbet
    backtrace = <<~EOBT
      /Users/user/.gem/ruby/3.2.2/gems/minitest-5.20.0/lib/minitest/assertions.rb:183:in `assert'
      example_test.rb:9:in `assert_false'
      /Users/user/.gem/ruby/3.2.2/gems/sorbet-runtime-0.5.11068/lib/types/private/methods/call_validation.rb:256:in `bind_call'
      /Users/user/.gem/ruby/3.2.2/gems/sorbet-runtime-0.5.11068/lib/types/private/methods/call_validation.rb:256:in `validate_call'
      /Users/user/.gem/ruby/3.2.2/gems/sorbet-runtime-0.5.11068/lib/types/private/methods/_methods.rb:275:in `block in _on_method_added'
      example_test.rb:25:in `test_something'
      /Users/user/.gem/ruby/3.2.2/gems/minitest-5.20.0/lib/minitest/test.rb:94:in `block (3 levels) in run'
      /Users/user/.gem/ruby/3.2.2/gems/minitest-5.20.0/lib/minitest/test.rb:191:in `capture_exceptions'
      /Users/user/.gem/ruby/3.2.2/gems/minitest-5.20.0/lib/minitest/test.rb:89:in `block (2 levels) in run'
      ... so many lines ...
    EOBT

    filter = Minitest::BacktraceFilter.new %r%lib/minitest|gems/sorbet%

    with_backtrace_filter filter do
      begin
        assert_equal 1, 2
      rescue Minitest::Assertion => e
        e.set_backtrace backtrace.lines.map(&:chomp)

        assert_match "example_test.rb:25", e.location
      end
    end
  end
end