# frozen_string_literal: true

require 'stringio'

TRYOUTS_LIB_HOME = __dir__ unless defined?(TRYOUTS_LIB_HOME)

require_relative 'tryouts/console'
require_relative 'tryouts/section'
require_relative 'tryouts/testbatch'
require_relative 'tryouts/testcase'
require_relative 'tryouts/version'

class Tryouts
  @debug = false
  @quiet = false
  @noisy = false
  @fails = false
  @container = Class.new
  @cases = []
  @sysinfo = nil
  @testcase_io = StringIO.new

  module ClassMethods
    attr_accessor :container, :quiet, :noisy, :fails
    attr_writer :debug
    attr_reader :cases, :testcase_io

    def sysinfo
      require 'sysinfo'
      @sysinfo ||= SysInfo.new
      @sysinfo
    end

    def debug?
      @debug == true
    end

    def update_load_path(lib_glob)
      Dir.glob(lib_glob).each { |dir| $LOAD_PATH.unshift(dir) }
    end

    def run_all *paths
      batches = paths.collect do |path|
        parse path
      end

      all = 0
      skipped_tests = 0
      failed_tests = 0
      skipped_batches = 0
      failed_batches = 0

      msg format('Ruby %s @ %-60s', RUBY_VERSION, Time.now), $/

      if Tryouts.debug?
        Tryouts.debug "Found #{paths.size} files:"
        paths.each { |path| Tryouts.debug "  #{path}" }
        Tryouts.debug
      end

      batches.each do |batch|
        path = batch.path.gsub(%r{#{Dir.pwd}/?}, '')
        divider = '-' * 70
        path_pretty = format('>>>>>  %-20s  %s', path, '').ljust(70, '<')

        msg $/
        vmsg Console.reverse(divider)
        vmsg Console.reverse(path_pretty)
        vmsg Console.reverse(divider)
        vmsg $/

        before_handler = proc do |tc|
          if Tryouts.noisy
            tc_title = tc.desc.to_s
            vmsg Console.underline(format('%-58s ', tc_title))
            vmsg tc.test.inspect, tc.exps.inspect
          end
        end

        batch.run(before_handler) do |tc|
          all += 1
          failed_tests += 1 if tc.failed?
          skipped_tests += 1 if tc.skipped?
          codelines = tc.outlines.join($/)
          first_exp_line = tc.exps.first
          result_adjective = tc.failed? ? 'FAILED' : 'PASSED'

          first_exp_line = tc.exps.first
          location = format('%s:%d', tc.exps.path, first_exp_line)

          expectation = Console.color(tc.color, codelines)
          summary = Console.color(tc.color, "%s @ %s" % [tc.adjective, location])
          vmsg '         %s' % expectation
          if tc.failed?
            msg Console.reverse(summary)
          else
            msg summary
          end
          vmsg

          # Output buffered testcase_io to stdout
          # and reset it for the next test case.
          unless Tryouts.fails && !tc.failed?
            $stdout.puts testcase_io.string unless Tryouts.quiet
          end

          # Reset the testcase IO buffer
          testcase_io.truncate(0)
        end
      end

      # Create a line of separation before the result summary
      msg $INPUT_RECORD_SEPARATOR  # newline

      if all
        suffix = "tests passed (#{skipped_tests} skipped)" if skipped_tests > 0
        actual_test_size = all - skipped_tests
        if actual_test_size > 0
          msg cformat(all - failed_tests - skipped_tests, all - skipped_tests, suffix)
        end
      end

      actual_batch_size = (batches.size - skipped_batches)
      if batches.size > 1 && actual_batch_size > 0
        suffix = 'batches passed'
        suffix << " (#{skipped_batches} skipped)" if skipped_batches > 0
        msg cformat(batches.size - skipped_batches - failed_batches, batches.size - skipped_batches, suffix)
      end

      # Print out the buffered result summary
      $stdout.puts testcase_io.string

      failed_tests  # returns the number of failed tests (0 if all passed)
    end

    def cformat(*args)
      Console.bright '%d of %d %s' % args
    end

    def run(path)
      batch = parse path
      batch.run
      batch
    end

    def parse(path)
      # debug "Loading #{path}"
      lines = File.readlines path
      skip_ahead = 0
      batch = TestBatch.new path, lines
      lines.size.times do |idx|
        skip_ahead -= 1 and next if skip_ahead > 0

        line = lines[idx].chomp
        # debug('%-4d %s' % [idx, line])
        next unless expectation? line

        offset = 0
        exps = Section.new(path, idx + 1)
        exps << line.chomp
        while idx + offset < lines.size
          offset += 1
          this_line = lines[idx + offset]
          break if ignore?(this_line)

          if expectation?(this_line)
            exps << this_line.chomp
            skip_ahead += 1
          end
          exps.last += 1
        end

        offset = 0
        buffer = Section.new(path)
        desc = Section.new(path)
        test = Section.new(path, idx) # test start the line before the exp.
        while idx - offset >= 0
          offset += 1
          this_line = lines[idx - offset].chomp
          buffer.unshift this_line if ignore?(this_line)
          buffer.unshift this_line if comment?(this_line)
          if test?(this_line)
            test.unshift(*buffer) && buffer.clear
            test.unshift this_line
          end
          if test_begin?(this_line)
            while test_begin?(lines[idx - (offset + 1)].chomp)
              offset += 1
              buffer.unshift lines[idx - offset].chomp
            end
          end
          next unless test_begin?(this_line) || idx - offset == 0 || expectation?(this_line)

          adjust = expectation?(this_line) ? 2 : 1
          test.first = idx - offset + buffer.size + adjust
          desc.unshift(*buffer)
          desc.last = test.first - 1
          desc.first = desc.last - desc.size + 1
          # remove empty lines between the description
          # and the previous expectation
          while !desc.empty? && desc[0].empty?
            desc.shift
            desc.first += 1
          end
          break
        end

        batch << TestCase.new(desc, test, exps)
      end

      batch
    end
    def print(str)
      return if Tryouts.quiet

      $stdout.print str
      $stdout.flush
    end

    def vmsg *msgs
      msg(*msgs) if Tryouts.noisy
    end

    def msg *msgs
      testcase_io.puts(*msgs) unless Tryouts.quiet
    end

    def err *msgs
      msgs.each do |line|
        $stderr.puts Console.color :red, line
      end
    end

    def debug *msgs
      $stderr.puts(*msgs) if debug?
    end

    def eval(str, path, line)
      Kernel.eval str, @container.send(:binding), path, line
    rescue SyntaxError, LoadError => e
      Tryouts.err Console.color(:red, e.message),
                  Console.color(:red, e.backtrace.first)
      nil
    end

    private

    def expectation?(str)
      !ignore?(str) && str.strip.match(/\A\#+\s*=>/)
    end

    def comment?(str)
      !str.strip.match(/^\#+/).nil? && !expectation?(str)
    end

    def test?(str)
      !ignore?(str) && !expectation?(str) && !comment?(str)
    end

    def ignore?(str)
      str.to_s.strip.chomp.empty?
    end

    def test_begin?(str)
      !str.strip.match(/\#+\s*TEST/i).nil? ||
        !str.strip.match(/\A\#\#+[\s\w]+/i).nil?
    end
  end

  extend ClassMethods
end