require 'spec_helper'
require 'stringio'
require 'oai_helper'

require_relative 'export_spec_helper'
require_relative 'oai_response_checker'

describe 'OAI handler' do
  FIXTURES_DIR = OAIHelper::FIXTURES_DIR

  before(:all) do
    @oai_repo_id, @test_record_count, @test_resource_record, @test_archival_object_record = OAIHelper.load_oai_data
    @test_resource_record_id = JSONModel(:resource).id_for(@test_resource_record)
    @test_archival_object_record_id = JSONModel(:archival_object).id_for(@test_archival_object_record)

    # Ensure an ARK is created for our test resource and archival object
    AppConfig[:arks_enabled] = true
    RequestContext.open(:repo_id => @oai_repo_id) do
      ArkName.ensure_ark_for_record(Resource[@test_resource_record_id], nil)
      ArkName.ensure_ark_for_record(ArchivalObject[@test_archival_object_record_id], nil)
    end
    AppConfig[:arks_enabled] = false
  end

  around(:each) do |example|
    JSONModel.with_repository(@oai_repo_id) do
      RequestContext.open(:repo_id => @oai_repo_id) do
        example.run
      end
    end
  end

  before(:each) do
    # EAD export normally tries the search index first, but for the tests we'll
    # skip that since Solr isn't running.
    allow(Search).to receive(:records_for_uris) do |*|
      {'results' => []}
    end
  end

  def format_xml(s)
    Nokogiri::XML(s).to_xml(:indent => 2, :save_with => Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
  end

  XML_HEADER = "<!-- THIS FILE IS AUTOMATICALLY GENERATED - DO NOT EDIT.  To update it, just delete it and it will be regenerated from your test data. -->"

  def check_oai_request_against_fixture(fixture_name, params)
    fixture_file = File.join(FIXTURES_DIR, 'responses', fixture_name) + ".xml"
    result = ArchivesSpaceOaiProvider.new.process_request(params)

    # Setting the OAI_SPEC_RECORD will override our stored fixtures with
    # whatever we get back from the OAI.  Be sure to manually check the changes
    # to ensure that responses are what you were expecting.
    if !File.exist?(fixture_file)
      $stderr.puts("NOTE: Updating fixture #{fixture_file}")
      File.write(fixture_file, XML_HEADER + "\n" + format_xml(result))
    else
      OAIResponseChecker.compare(File.read(fixture_file), result)
    end
  end


  ###
  ### Tests start here
  ###

  describe "OAI protocol and mapping support" do

    RESOURCE_BASED_FORMATS = ['oai_ead']
    RESOURCE_AND_COMPONENT_BASED_FORMATS = ['oai_dc', 'oai_dcterms', 'oai_mods', 'oai_marc']

    it "responds to an OAI Identify request" do
      expect {
        check_oai_request_against_fixture('identify', :verb => 'Identify')
      }.not_to raise_error
    end

    it "responds to an OAI ListMetadataFormats request" do
      expect {
        check_oai_request_against_fixture('list_metadata_formats', :verb => 'ListMetadataFormats')
      }.not_to raise_error
    end

    it "responds to an OAI ListSets request" do
      expect {
        check_oai_request_against_fixture('list_sets', :verb => 'ListSets')
      }.not_to raise_error
    end

    it "includes sets defined in OAIConfig table in ListSets request" do
      oc = OAIConfig.first
      oc.update(:repo_set_codes       => ['foo', 'bar'].to_json,
                :repo_set_description => "foobar",
                :repo_set_name        => "repo_set_name",
                :sponsor_set_names    => ['bim', 'baz'].to_json,
                :sponsor_set_description => "bimbaz",
                :sponsor_set_name        => "sponsor_set_name",
                )

      result = ArchivesSpaceOaiProvider.new.process_request(:verb => 'ListSets')
      expect(result).to match(/<setSpec>sponsor_set_name<\/setSpec><setName>sponsor_set_name<\/setName>/)
      expect(result).to match(/<setSpec>repo_set_name<\/setSpec><setName>repo_set_name<\/setName>/)
      expect(result).to match(/<oai_dc:description.*>bimbaz<\/oai_dc:description>/)
      expect(result).to match(/<oai_dc:description.*>foobar<\/oai_dc:description>/)
    end

    RESOURCE_BASED_FORMATS.each do |prefix|
      it "responds to a GetRecord request for type #{prefix}, mapping appropriately" do
        expect {
          check_oai_request_against_fixture("getrecord_#{prefix}",
                                            :verb => 'GetRecord',
                                            :identifier => 'oai:archivesspace:' + @test_resource_record,
                                            :metadataPrefix => prefix)
        }.not_to raise_error
      end
    end

    context 'when include_uris is provided from AppConfig[:oai_ead_options] and is set to true' do
      before do
        AppConfig[:oai_ead_options] = { include_uris: true }
      end

      RESOURCE_BASED_FORMATS.each do |prefix|
        it "responds to a GetRecord request for type #{prefix}, mapping appropriately" do
          expect {
            check_oai_request_against_fixture("getrecord_#{prefix}",
                                              :verb => 'GetRecord',
                                              :identifier => 'oai:archivesspace:' + @test_resource_record,
                                              :metadataPrefix => prefix)
          }.not_to raise_error
        end
      end
    end

    context 'when include_uris is provided from AppConfig[:oai_ead_options] and is set to false' do
      before do
        AppConfig[:oai_ead_options] = { include_uris: false }
      end

      RESOURCE_BASED_FORMATS.each do |prefix|
        it "responds to a GetRecord request for type #{prefix}, mapping appropriately" do
          expect {
            check_oai_request_against_fixture("getrecord_#{prefix}_without_uris",
                                              :verb => 'GetRecord',
                                              :identifier => 'oai:archivesspace:' + @test_resource_record,
                                              :metadataPrefix => prefix)
          }.not_to raise_error
        end
      end
    end

    # TODO: Fix this test and fixtures
    # RESOURCE_AND_COMPONENT_BASED_FORMATS.each do |prefix|
    #  it "responds to a GetRecord request for type #{prefix}, mapping appropriately" do
    #    expect {
    #      check_oai_request_against_fixture("getrecord_#{prefix}",
    #                                        :verb => 'GetRecord',
    #                                        :identifier => 'oai:archivesspace:' + @test_archival_object_record,
    #                                        :metadataPrefix => prefix)

    #     check_oai_request_against_fixture("getrecord_resource_#{prefix}",
    #                                        :verb => 'GetRecord',
    #                                        :identifier => 'oai:archivesspace:' + @test_resource_record,
    #                                        :metadataPrefix => prefix)
    #    }.not_to raise_error
    #  end
    #end
  end

  describe "ListIdentifiers" do

    def list_identifiers(prefix)
      params = {
        :verb => 'ListIdentifiers',
        :metadataPrefix => prefix,
      }

      result = ArchivesSpaceOaiProvider.new.process_request(params)

      doc = Nokogiri::XML(result)
      doc.remove_namespaces!

      doc.xpath("//identifier").map {|elt| elt.text}
    end

    RESOURCE_BASED_FORMATS.each do |prefix|
      it "responds to a ListIdentifiers request for type #{prefix}" do
        list_identifiers(prefix).all? {|identifier| identifier =~ %r{/resources/}}
      end
    end

    RESOURCE_AND_COMPONENT_BASED_FORMATS.each do |prefix|
      it "responds to a ListIdentifiers request for type #{prefix}" do
        list_identifiers(prefix).all? {|identifier| identifier =~ %r{/archival_objects|resources/}}
      end
    end

  end

  describe "ListRecords" do

    let (:page_size) { 2 }

    let (:oai_repo) {
      oai_repo = ArchivesSpaceOAIRepository.new

      # Drop our page size to ensure we get a resumption token
      smaller_pages = ArchivesSpaceOAIRepository::FormatOptions.new([ArchivalObject], page_size)

      allow(oai_repo).to receive(:options_for_type)
                           .with('oai_dc')
                           .and_return(smaller_pages)

      oai_repo
    }

    it "supports an unqualified ListRecords request" do
      response = oai_repo.find(:all, {:metadata_prefix => "oai_dc"})
      expect(response.records.length).to eq(page_size)
    end

    it "supports resumption tokens" do
      page1_response = oai_repo.find(:all, {:metadata_prefix => "oai_dc"})
      page1_uris = page1_response.records.map(&:jsonmodel_record).map(&:uri)

      expect(page1_response.token).not_to be_nil

      page2_response = oai_repo.find(:all, {:resumption_token => page1_response.token.serialize})
      page2_uris = page2_response.records.map(&:jsonmodel_record).map(&:uri)

      # We got some different URIs on the next page
      expect((page2_uris + page1_uris).length).to eq(page1_uris.length + page2_uris.length)
    end

    it "supports date ranges when listing records" do
      start_time = Time.parse('1975-01-01 00:00:00 UTC')
      end_time = Time.parse('1976-01-01 00:00:00 UTC')

      margin = 86400
      record_count = 2

      # Backdate some of our AOs to fall within our timeframe of interest.
      #
      # Note that we do a simple UPDATE here to avoid having our Sequel model
      # save hook from updating the system_mtime to Time.now.
      #
      ao_ids = ArchivalObject.filter(:repo_id => @oai_repo_id).order(:id).all.take(record_count).map(&:id)
      ArchivalObject.filter(:id => ao_ids).update(:system_mtime => (start_time + margin))

      response = oai_repo.find(:all, {:metadata_prefix => "oai_dc",
                                      :from => start_time,
                                      :until => end_time})

      expect(response.records.length).to eq(record_count)
    end

    it "lists deletes" do
      token = nil
      loop do
        opts = {:metadata_prefix => "oai_dc"}

        if token
          opts[:resumption_token] = token
        end

        response = oai_repo.find(:all, opts)

        if response.is_a?(Array)
          # Our final page of results--which should be entirely deletes
          expect(response.all?(&:deleted?)).to be_truthy

          break
        elsif response.token
          # Next page!
          token = response.token.serialize
        else
          # Shouldn't have happened...
          fail "no deletes found"
          break
        end
      end
    end

    it "supports OAI sets based on levels" do
      response = oai_repo.find(:all, {:metadata_prefix => "oai_dc", :set => 'fonds'})
      expect(response.records.length).to be > 0

      expect(response.records.map(&:jsonmodel_record).map(&:level).uniq).to eq(['fonds'])
    end


    it "supports OAI sets based on sponsors" do
      oc = OAIConfig.first
      oc.update(:sponsor_set_names       => ['sponsor_0'].to_json,
                :sponsor_set_description => "sponsor_set_description")

      response = oai_repo.find(:all, {:metadata_prefix => "oai_dc", :set => 'sponsor_set'})

      expect(response.records.all? {|record| record.jsonmodel_record.resource['ref'] == @test_resource_record})
        .to be_truthy
    end

    it "supports OAI sets based on repositories" do
      oc = OAIConfig.first
      oc.update(:repo_set_codes       => ['oai_test'].to_json,
                :repo_set_name        => "repo_set_name",
                :repo_set_description => "repo_set_description")

      response = oai_repo.find(:all, {:metadata_prefix => "oai_dc", :set => 'repo_set_name'})
      response.records.all? {|record| record.sequel_record.repo_id == @oai_repo_id}
        .should be(true)
    end

    describe "OAI sets for resource records" do

      let (:oai_repo) {
        oai_repo = ArchivesSpaceOAIRepository.new

        allow(oai_repo).to receive(:options_for_type)
                             .with('oai_dc')
                             .and_return(ArchivesSpaceOAIRepository::FormatOptions.new([Resource], 5))

        oai_repo
      }

      it "supports OAI sets based on sponsors for resource records too" do
        oc = OAIConfig.first
        oc.update(:sponsor_set_names       => ['sponsor_0'].to_json,
                  :sponsor_set_name        => "sponsor_set_name",
                  :sponsor_set_description => "sponsor_set_description")

        response = oai_repo.find(:all, {:metadata_prefix => "oai_dc", :set => 'sponsor_set_name'})

        # Just matched the single collection
        expect(response.records.length).to eq(1)

        resource = response.records.first
        expect(resource.jsonmodel_record['finding_aid_sponsor']).to eq('sponsor_0')
      end

      it "supports OAI sets based on repositories for resource records too" do
        oc = OAIConfig.first
        oc.update(:repo_set_codes          => ['oai_test'].to_json,
                  :repo_set_name        => "repo_set_name",
                  :repo_set_description    => "repo_set_description")

        response = oai_repo.find(:all, {:metadata_prefix => "oai_dc", :set => 'repo_set_name'})
        response.records.count.should eq(5)
      end
    end

    it "doesn't reveal published or suppressed records" do
      unpublished = create(:json_archival_object, :publish => false, :resource => {:ref => @test_resource_record})
      suppressed = create(:json_archival_object, :publish => true, :resource => {:ref => @test_resource_record})
      ArchivalObject[suppressed.id].set_suppressed(true)

      token = nil
      loop do
        opts = {:metadata_prefix => "oai_dc"}

        if token
          opts[:resumption_token] = token
        end

        response = oai_repo.find(:all, opts)

        records = []

        if response.respond_to?(:token)
          # A partial response
          token = response.token.serialize
          records = response.records
        elsif response.is_a?(Array)
          records = response
          token = nil
        else
          # Shouldn't have happened...
          fail "unexpected result"
          break
        end

        prohibited_uris = [unpublished.uri, suppressed.uri]

        records.each do |record|
          if record.is_a?(ArchivesSpaceOAIRecord) && prohibited_uris.include?(record.jsonmodel_record.uri)
            fail "URI #{record.jsonmodel_record.uri} is unpublished/suppressed and should not be shown in OAI results"
          end
        end

        break if token.nil?
      end
    end
  end

  describe 'OAI mappers output' do
    describe 'DC output' do
      it "should map Conditions Governing Access and Conditions Governing Use to <dc:rights>" do

        uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_dc"

        response = get uri
        expect(response.body).to match(/<dc:rights.*>conditions governing access note<\/dc:rights>/)
        expect(response.body).to match(/<dc:rights.*>conditions governing use note<\/dc:rights>/)

        expect(response.body).not_to match(/<dc:relation>conditions governing access note<\/dc:relation>/)
        expect(response.body).not_to match(/<dc:relation>conditions governing use note<\/dc:relation>/)
      end

      it "should map Extents to dc:format, not dc:extent" do
        uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_dc"

        response = get uri

        expect(response.body).to match(/<dc:format.*>10 Volumes; Container summary<\/dc:format>/)
        expect(response.body).to match(/<dc:format.*>physical description note<\/dc:format>/)
        expect(response.body).to match(/<dc:format.*>dimensions note<\/dc:format>/)

        expect(response.body).not_to match(/<dc:extent>10 Volumes; Container summary<\/dc:extent>/)
        expect(response.body).not_to match(/<dc:extent>physical description note<\/dc:extent>/)
        expect(response.body).not_to match(/<dc:extent>dimensions note<\/dc:extent>/)
      end
      describe 'ARKs enabled' do
        before(:all) do
          AppConfig[:arks_enabled] = true
        end
        it "should output ARK name in identifier tag" do
          uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_dc"
          response = get uri
          resource_id = JSONModel(:resource).id_for(@test_resource_record)
          ark_url = ArkName.first(:resource_id => resource_id.to_i).value
          expect(response.body).to match(/<dc:identifier.*>#{ark_url}<\/dc:identifier>/)
        end
      end
      describe 'ARKs disabled' do
        before(:all) do
          AppConfig[:arks_enabled] = false
        end
        it "should not output ARK name in identifier tag if ARK output is disabled" do
          uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_dcterms"
          response = get uri
          resource_id = @test_resource_record.split("/")[4]
          ark_url = ArkName.first(:resource_id => resource_id.to_i).value
          expect(response.body).to_not match(/<dc:identifier.*>#{ark_url}<\/dc:identifier>/)
        end
      end
    end
    describe 'DCTerms output' do
      describe 'ARKs enabled' do
        before(:all) do
          AppConfig[:arks_enabled] = true
        end
        it "should output ARK name in identifier tag" do
          uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_dcterms"
          response = get uri
          resource_id = @test_resource_record.split("/")[4]
          ark_url = ArkName.first(:resource_id => resource_id.to_i).value
          expect(response.body).to match(/<dcterms:identifier.*>#{ark_url}<\/dcterms:identifier>/)
        end
      end
      describe 'ARKs disabled' do
        before(:all) do
          AppConfig[:arks_enabled] = false
        end
        it "should not output ARK name in identifier tag if ARK output is disabled" do
          uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_dcterms"
          response = get uri
          resource_id = @test_resource_record.split("/")[4]
          ark_url = ArkName.first(:resource_id => resource_id.to_i).value
          expect(response.body).to_not match(/<dcterms:identifier.*>#{ark_url}<\/dcterms:identifier>/)
        end
      end
    end
    describe 'MODS output' do
      describe 'ARKs enabled' do
        before(:all) do
          AppConfig[:arks_enabled] = true
        end
        it "should output ARK name in identifier tag" do
          uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_mods"
          response = get uri
          resource_id = @test_resource_record.split("/")[4]
          ark_url = ArkName.first(:resource_id => resource_id.to_i).value
          expect(response.body).to match(/<identifier.*>#{ark_url}<\/identifier>/)
        end
      end
    end
  end

  describe 'publish flags' do
    it "should respect publish flags for dc exports" do
      uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_dc"
      response = get uri
      expect(response.body).not_to match(/note with unpublished parent node/)
    end

    it "should respect publish flags for ead exports" do
      uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_ead"
      response = get uri
      expect(response.body).not_to match(/note with unpublished parent node/)
    end

    it "should respect publish flags on creator agents for ead exports" do
      uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_ead"
      response = get uri
      expect(response.body).not_to match(/UNPUBLISHED Creator Agent/)
    end

    it "should respect publish flags on source agents for ead exports" do
      uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_ead"
      response = get uri
      expect(response.body).not_to match(/UNPUBLISHED Corporate Agent/)
    end

    it "should respect publish flags for marc exports" do
      uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_marc"
      response = get uri
      expect(response.body).not_to match(/note with unpublished parent node/)
    end

    it "should respect publish flags for mods exports" do
      uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@test_resource_record}&metadataPrefix=oai_mods"
      response = get uri
      expect(response.body).not_to match(/note with unpublished parent node/)
    end

  end

  describe "respository with OAI harvesting disabled" do
    before(:all) do
      as_test_user('admin') do
        @repo_disabled = create(:json_repository, :oai_is_disabled => true)

        $another_repo_id = $repo_id
        $repo_id = @repo_disabled.id

        JSONModel.set_repository($repo_id)

        @resource = create(:json_resource,
                            :level => 'collection')
      end
    end

    after(:all) do
      as_test_user('admin') do
        @resource.delete
        $repo_id = $another_repo_id

        JSONModel.set_repository($repo_id)
      end
    end

    it "does not publish resources in a repository with OAI disabled" do
      uri = "/oai?verb=GetRecord&identifier=oai:archivesspace:#{@resource['uri']}&metadataPrefix=oai_marc"

      response = get uri
      expect(response.body).to match(/<error code="idDoesNotExist">/)
    end
  end

  describe "repository with sets disabled" do
    before(:all) do
      # 891 is the enum_value_id for 'fonds'
      # add a set restriction for only 'fonds' objects
      Repository.all.each do |r|
        r.update(:oai_sets_available => (["891"]).to_json)
      end
    end

    after(:all) do
      # change things back: remove all set restrictions
      Repository.all.each do |r|
        r.update(:oai_sets_available => "[]")
      end
    end

    it "does not return an object if set excluded from OAI in repo" do
      uri = "/oai?verb=ListRecords&set=collection&metadataPrefix=oai_dc"
      response = get uri
      doc = Nokogiri::XML(response.body)

      # should not have any non-tombstone results in xml
      expect(doc.xpath("//xmlns:header[not(@status='deleted')]").length).to eq(0)
    end

    it "returns an object if set included in OAI in repo" do
        # query explicitly for only fonds objects
      uri = "/oai?verb=ListRecords&set=fonds&metadataPrefix=oai_dc"
      response = get uri
      doc = Nokogiri::XML(response.body)

        # should have at least 1 result in XML
      expect(doc.xpath("//xmlns:header[not(@status='deleted')]").length > 0).to be true
    end

    it "does not list a set in ListSets if that set is not enabled for at least one repository" do
      uri = "/oai?verb=ListSets"
      response = get uri
      doc = Nokogiri::XML(response.body)

      expect(doc.to_s).to match(/<set><setSpec>fonds<\/setSpec>/)

      expect(doc.to_s).to_not match(/<set><setSpec>class<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>collection<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>file<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>item<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>otherlevel<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>recordgrp<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>series<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>subfonds<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>subgrp<\/setSpec>/)
      expect(doc.to_s).to_not match(/<set><setSpec>subseries<\/setSpec>/)
    end
  end

  describe "repository without sets disabled" do
    it "does list a set in ListSets if that set is enabled for at least one repository" do
      Repository.all.each do |r|
        r.update(:oai_sets_available => "[]")
      end

      uri = "/oai?verb=ListSets"
      response = get uri
      doc = Nokogiri::XML(response.body)

      expect(doc.to_s).to match(/<set><setSpec>fonds<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>class<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>collection<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>file<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>item<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>otherlevel<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>recordgrp<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>series<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>subfonds<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>subgrp<\/setSpec>/)
      expect(doc.to_s).to match(/<set><setSpec>subseries<\/setSpec>/)
    end
  end
end
