class OAIResponseChecker

  # If any of our unpublished notes turn up in our output, something's gone bad.
  #
  # Disabling this for now because some MARC/EAD record exports are currently
  # including unpublished content, and it's not clear whether that's deliberate
  # or not.  More information in ANW-77.
  FORBIDDEN_TEXT = nil

  # These elements will change in normal operation, and that's OK.
  SKIPPED_ELEMENT_NAMES = ['responseDate', 'datestamp', 'resumptionToken', 'date', 'identifier', 'id', 'url']

  # Compare two XML-string OAI responses to see if they differ in any meaningful way.
  #
  # Raises MismatchError if they do.
  def self.compare(correct_response, test_result_response)
    if FORBIDDEN_TEXT && test_result_response =~ FORBIDDEN_TEXT
      raise MismatchError.new("Response should never contain text '#{FORBIDDEN_TEXT}'", "", "", [])
    end

    correct = Nokogiri::XML(correct_response) do |config|
      config.options = Nokogiri::XML::ParseOptions::NOBLANKS
    end

    test_result = Nokogiri::XML(test_result_response) do |config|
      config.options = Nokogiri::XML::ParseOptions::NOBLANKS
    end

    correct.remove_namespaces!
    test_result.remove_namespaces!

    new.compare_or_fail(correct.root, test_result.root)
  end

  # Recursive comparison helper for comparing Nokogiri elements.
  def compare_or_fail(ours, theirs, path = [])
    if ours.class != theirs.class
      raise MismatchError.new("Element type mismatch", ours.class, theirs.class, path)
    end

    if ours.is_a?(Nokogiri::XML::Element)
      unless ours.name == theirs.name
        raise MismatchError.new("Tag name mismatch", ours.name, theirs.name, path)
      end

      new_path = path + [ours.name + '[' + ours.attributes.map(&:to_s).join(" ") + ']']

      unless attributes_equal?(ours.attributes, theirs.attributes)
        raise MismatchError.new("Attributes mismatch", ours.attributes, theirs.attributes, new_path)
      end

      unless ours.children.length == theirs.children.length
        our_xml = ours.to_xml
        their_xml = theirs.to_xml

        raise MismatchError.new("Child count mismatch", ours.children.length, theirs.children.length, new_path,
                               "Ours: #{our_xml}\n\nTheirs: #{their_xml}")
      end

      # Compare all children
      unless skip_element?(ours)
        ours.children.zip(theirs.children).each do |our_child, their_child|
          compare_or_fail(our_child, their_child, new_path)
        end
      end

    elsif ours.is_a?(Nokogiri::XML::Text)

      # don't worry about database ids when comparing uris with fixture
      if [ours, theirs].all? { |node| node.text.strip.match(/^\/repositories\/\d+/) }
        unless ours.text.strip.gsub(/\/\d+/, "/0") == theirs.text.strip.gsub(/\/\d+/, "/0")
          raise MismatchError.new("String mismatch", ours, theirs, path)
        end
      else
        unless ours.text.strip == theirs.text.strip
          raise MismatchError.new("String mismatch", ours, theirs, path)
        end
      end

    else
      raise "Unexpected type: #{ours.class}"
    end
  end

  private

  def attributes_equal?(our_attributes, their_attributes)
    our_attributes.zip(their_attributes).each do |(our_name, our_value), (their_name, their_value)|
      return false if our_name != their_name

      if SKIPPED_ELEMENT_NAMES.include?(our_name)
        # Don't compare things that are likely to have been randomly generated
        next
      end

      if our_value.text != their_value.text
        return false
      end
    end

    true
  end

  # Some elements will give us false positive.  Skip them!
  def skip_element?(element)
    return true if SKIPPED_ELEMENT_NAMES.include?(element.name)

    if element.name == 'datafield' && ['040', '852'].include?(element.attribute('tag').text)
      # MARC fields 040 & 852 include the repository name, which is randomly
      # generated by the test suite.
      return true
    end

    if element.name == 'publicationstmt'
      # EAD export picks up some repository bits which are also randomly
      # generated.
      return true
    end
  end

  class MismatchError < StandardError
    def initialize(description, expected, got, path, additional_information = nil)
      msg = "#{description}:\nexpected '#{expected}'\ngot: '#{got}'\nat: #{path.join('/')}"

      if additional_information
        msg += "\n"
        msg += additional_information
      end

      $stderr.puts(msg)
      super(msg)
    end
  end
end
