module Wukong
  module SpecHelpers
    class UnitTestMatcher

      attr_accessor :driver, :expected, :reason, :expected_record, :actual_record, :mismatched_index

      def matches?(driver)
        self.driver = driver
        driver.run
        if actual_size != expected_size
          self.reason = :size
          return false
        end
        return true if just_count?
        expected.each_with_index do |expectation, index|
          actual = output[index]
          if actual != expectation
            self.reason           = :element
            self.expected_record  = expectation
            self.actual_record    = actual
            self.mismatched_index = index
            return false
          end
        end
        true
      end
      
      def initialize *expected
        self.expected = expected
      end

      def failure_message
        if reason == :size
          "Expected #{expected_size} records, got #{actual_size}:\n\n#{pretty_output}"
        else
          "Expected the #{ordinalize(mismatched_index)} record to be#{parse_modifier}\n\n#{expected_record}\n\nbut got\n\n#{pretty_output}"
        end
      end

      def negative_failure_message
        if reason == :size
          "Expected to NOT get #{expected_size} records:\n\n#{output}"
        else
          "Expected the #{ordinalize(mismatched_index)} record to NOT be#{parse_modifier}\n\n#{pretty_output}"
        end
      end
      
      def records
        @just_count = true
        self                    # chaining
      end
      alias_method :record, :records

      private
      
      def just_count?
        @just_count
      end

      def actual_size
        driver.size
      end

      def expected_size
        just_count? ? expected.first.to_i : expected.size
      end

      def output
        driver
      end

      def parse_modifier
      end

      def pretty_output
        [].tap do |pretty|
          output.each_with_index do |record, index|
            s      = (record.is_a?(String) ? record : record.inspect)
            prefix = case
            when output.size > 1 && index == mismatched_index
              " => "
            when output.size > 1
              "    "
            else
              ''
            end
            pretty << [prefix,s].join('')
          end
        end.join("\n")
      end

      # http://stackoverflow.com/questions/1081926/how-do-i-format-a-date-in-ruby-to-include-rd-as-in-3rd
      def ordinalize array_index
        n = array_index + 1
        if (11..13).include?(n % 100)
          "#{n}th"
        else
          case n % 10
          when 1; "#{n}st"
          when 2; "#{n}nd"
          when 3; "#{n}rd"
          else    "#{n}th"
          end
        end
      end
    end

    class JsonMatcher < UnitTestMatcher
      def output
        driver.map do |record|
          begin
            MultiJson.load(record)
          rescue => e
            raise Error.new("Could not parse output of dataflow as JSON: \n\n#{record}")
          end
        end
      end
      def parse_modifier
        ' (after parsing as JSON)'
      end
    end

    class DelimitedMatcher < UnitTestMatcher

      attr_accessor :delimiter
      
      def initialize delimiter, *expected
        self.delimiter = delimiter
        super(*expected)
      end
      
      def output
        driver.map do |record|
          begin
            record.to_s.split(delimiter)
          rescue => e
            raise Error.new("Could not parse as #{delimited_type}': \n\n#{record}")
          end
        end
      end

      def delimited_type
        "'#{delimiter}-delimited'"
      end
      
      def parse_modifier
        " (after parsing as #{delimited_type})"
      end
    end

    class TsvMatcher < DelimitedMatcher
      def initialize *expected
        super("\t", *expected)
      end
      def delimited_type
        "TSV"
      end
    end

    class CsvMatcher < DelimitedMatcher
      def initialize *expected
        super(",", *expected)
      end
      def delimited_type
        "CSV"
      end
    end
  end
end
