module QUnited
  module Driver

    # Collects test results from lines of JavaScript interpreter output.
    #
    # QUnited test running drivers may run a JavaScript interpreter in a separate process and
    # observe the output (say, on stdout) for text containing test results. These results may
    # be delimited by tokens that allow ResultsCollector to recognize the beginning and end of
    # JSON test results. The test running driver must be properly configured to emit the correct
    # tokens, matching TEST_RESULT_START_TOKEN and TEST_RESULT_END_TOKEN before and after strings
    # of test results serialized as valid JSON.
    #
    # If everything is set up correctly the recognized results are parsed and QUnitTestResult
    # objects are produced for each.
    #
    #
    # To use, initialize with the IO object that provides the output from the test running
    # process. Then call on_test_result with a block to be called when a test is collected.
    # A QUnited::QUnitTestResult object will be passed to the block for each test.
    #
    #   rc = ResultsCollector.new(stdout_from_test_runner)
    #   rc.on_test_result {|test_result| puts "I've got a result: #{test_result.inspect}" }
    #
    # If you need to capture output that is not part of any test result, you can call
    # on_non_test_result_line with another block to do this. Each line of output that is not part
    # of test result JSON is passed to the block.
    #
    #   rc.on_non_test_result_line {|line| puts "This line is not part of a test result: #{line}"}
    #
    class ResultsCollector
      TEST_RESULT_START_TOKEN    = 'QUNITED_TEST_RESULT_START_TOKEN'
      TEST_RESULT_END_TOKEN      = 'QUNITED_TEST_RESULT_END_TOKEN'
      ONE_LINE_TEST_RESULT_REGEX = /#{TEST_RESULT_START_TOKEN}(.*?)#{TEST_RESULT_END_TOKEN}/

      def initialize(io)
        @io = io
        @results = []
        @on_test_result_block = nil
        @on_non_test_result_line_block = nil
        @partial_test_result = ''
      end

      # Set a block to be called when a test result has been parsed. The block is passed a
      # QUnitTestResult object.
      def on_test_result(&block)
        raise ArgumentError.new('must provide a block') unless block_given?
        @on_test_result_block = block
      end

      # Set a block to be called when a line of output is read from the IO object that is not
      # part of a test result. The block is passed the line of output.
      def on_non_test_result_line(&block)
        raise ArgumentError.new('must provide a block') unless block_given?
        @on_non_test_result_line_block = block
      end

      # Read all available lines from the IO and parse results from it. If blocks have been set
      # with on_test_result and/or on_non_test_result_line they will be called when appropriate.
      def collect_results
        while collect_next_line; end
      end

      # Read the next line from the IO and parse results from it, if applicable. If blocks have
      # been set with on_test_result and/or on_non_test_result_line they will be called when
      # appropriate.
      #
      # Usually collect_results should be used unless lines need to be read one at a time for
      # some reason.
      def collect_next_line
        line = @io.gets
        return nil unless line

        if line =~ ONE_LINE_TEST_RESULT_REGEX
          process_test_result $1

        elsif line.include? TEST_RESULT_START_TOKEN
          @partial_test_result << line.sub(TEST_RESULT_START_TOKEN, '')

        elsif line.include? TEST_RESULT_END_TOKEN
          @partial_test_result << line.sub(TEST_RESULT_END_TOKEN, '')
          process_test_result @partial_test_result
          @partial_test_result = ''

        elsif !@partial_test_result.empty?
          # Middle of a test result
          @partial_test_result << line

        else
          @on_non_test_result_line_block.call(line) if @on_non_test_result_line_block

        end

        line
      end

      private

      def process_test_result(test_result_json)
        result = ::QUnited::QUnitTestResult.from_json(test_result_json)
        @results << result
        @on_test_result_block.call(result) if @on_test_result_block
      end
    end
  end
end