require 'spec_helper'

describe Mongo::Cursor do
  let(:authorized_collection) do
    authorized_client['cursor_spec_collection']
  end

  before do
    authorized_collection.drop
  end

  describe '#initialize' do
    let(:server) do
      view.send(:server_selector).select_server(authorized_client.cluster)
    end

    let(:reply) do
      view.send(:send_initial_query, server)
    end

    let(:cursor) do
      described_class.new(view, reply, server)
    end

    before do
      documents = [{test: 1}] * 10
      authorized_collection.insert_many(documents)
    end

    shared_context 'with initialized pool' do
      before do
        ClientRegistry.instance.close_all_clients

        # These tests really like creating pools (and thus scheduling
        # the pools' finalizers) when querying collections.
        # Deal with this by pre-creating pools for all known servers.
        cluster = authorized_collection.client.cluster
        cluster.next_primary
        cluster.servers_list.each do |server|
          server.pool
        end
      end
    end

    context 'cursor exhausted by initial result' do
      include_context 'with initialized pool'

      let(:view) do
        Mongo::Collection::View.new(authorized_collection)
      end

      it 'does not schedule the finalizer' do
        # Due to https://jira.mongodb.org/browse/RUBY-1772, restrict
        # the scope of the assertion
        RSpec::Mocks.with_temporary_scope do
          expect(ObjectSpace).not_to receive(:define_finalizer)
          cursor
        end
      end
    end

    context 'cursor not exhausted by initial result' do
      include_context 'with initialized pool'

      let(:view) do
        Mongo::Collection::View.new(authorized_collection, {}, batch_size: 2)
      end

      it 'schedules the finalizer' do
        # Due to https://jira.mongodb.org/browse/RUBY-1772, restrict
        # the scope of the assertion
        RSpec::Mocks.with_temporary_scope do
          expect(ObjectSpace).to receive(:define_finalizer)
          cursor
        end
      end
    end

    context 'server is unknown' do
      let(:server) do
        view.send(:server_selector).select_server(authorized_client.cluster).tap do |server|
          authorized_client.cluster.disconnect!
          server.unknown!
        end
      end

      let(:view) do
        Mongo::Collection::View.new(authorized_collection)
      end

      it 'works' do
        cursor
      end
    end
  end

  describe '#each' do

    let(:server) do
      view.send(:server_selector).select_server(authorized_client.cluster)
    end

    let(:reply) do
      view.send(:send_initial_query, server)
    end

    let(:cursor) do
      described_class.new(view, reply, server)
    end

    context 'when no options are provided to the view' do

      let(:view) do
        Mongo::Collection::View.new(authorized_collection)
      end

      context 'when the initial query retrieves all documents' do

        let(:documents) do
          (1..10).map{ |i| { field: "test#{i}" }}
        end

        before do
          authorized_collection.insert_many(documents)
        end

        it 'returns the correct amount' do
          expect(cursor.to_a.count).to eq(10)
        end

        it 'iterates the documents' do
          cursor.each do |doc|
            expect(doc).to have_key('field')
          end
        end
      end

      context 'when the initial query does not retrieve all documents' do

        let(:documents) do
          (1..102).map{ |i| { field: "test#{i}" }}
        end

        before do
          authorized_collection.insert_many(documents)
        end

        context 'when a getMore gets a socket error' do

          let(:op) do
            double('operation')
          end

          before do
            expect(cursor).to receive(:get_more_operation).and_return(op).ordered
            expect(op).to receive(:execute).and_raise(Mongo::Error::SocketError).ordered
          end

          it 'raises the error' do
            expect do
              cursor.each do |doc|
              end
            end.to raise_error(Mongo::Error::SocketError)
          end
        end

        context 'when no errors occur' do

          it 'returns the correct amount' do
            expect(cursor.to_a.count).to eq(102)
          end

          it 'iterates the documents' do
            cursor.each do |doc|
              expect(doc).to have_key('field')
            end
          end
        end
      end
    end

    context 'when options are provided to the view' do

      let(:documents) do
        (1..10).map{ |i| { field: "test#{i}" }}
      end

      before do
        authorized_collection.drop
        authorized_collection.insert_many(documents)
      end

      context 'when a limit is provided' do

        context 'when no batch size is provided' do

          context 'when the limit is positive' do

            let(:view) do
              Mongo::Collection::View.new(authorized_collection, {}, :limit => 2)
            end

            it 'returns the correct amount' do
              expect(cursor.to_a.count).to eq(2)
            end

            it 'iterates the documents' do
              cursor.each do |doc|
                expect(doc).to have_key('field')
              end
            end
          end

          context 'when the limit is negative' do

            let(:view) do
              Mongo::Collection::View.new(authorized_collection, {}, :limit => -2)
            end

            it 'returns the positive number of documents' do
              expect(cursor.to_a.count).to eq(2)
            end

            it 'iterates the documents' do
              cursor.each do |doc|
                expect(doc).to have_key('field')
              end
            end
          end

          context 'when the limit is zero' do

            let(:view) do
              Mongo::Collection::View.new(authorized_collection, {}, :limit => 0)
            end

            it 'returns all documents' do
              expect(cursor.to_a.count).to eq(10)
            end

            it 'iterates the documents' do
              cursor.each do |doc|
                expect(doc).to have_key('field')
              end
            end
          end
        end

        context 'when a batch size is provided' do

          context 'when the batch size is less than the limit' do

            let(:view) do
              Mongo::Collection::View.new(
                authorized_collection,
                {},
                :limit => 5, :batch_size => 3
              )
            end

            it 'returns the limited number of documents' do
              expect(cursor.to_a.count).to eq(5)
            end

            it 'iterates the documents' do
              cursor.each do |doc|
                expect(doc).to have_key('field')
              end
            end
          end

          context 'when the batch size is more than the limit' do

            let(:view) do
              Mongo::Collection::View.new(
                authorized_collection,
                {},
                :limit => 5, :batch_size => 7
              )
            end

            it 'returns the limited number of documents' do
              expect(cursor.to_a.count).to eq(5)
            end

            it 'iterates the documents' do
              cursor.each do |doc|
                expect(doc).to have_key('field')
              end
            end
          end

          context 'when the batch size is the same as the limit' do

            let(:view) do
              Mongo::Collection::View.new(
                authorized_collection,
                {},
                :limit => 5, :batch_size => 5
              )
            end

            it 'returns the limited number of documents' do
              expect(cursor.to_a.count).to eq(5)
            end

            it 'iterates the documents' do
              cursor.each do |doc|
                expect(doc).to have_key('field')
              end
            end
          end
        end
      end
    end

    context 'when the cursor is not fully iterated and is garbage collected' do

      let(:documents) do
        (1..6).map{ |i| { field: "test#{i}" }}
      end

      let(:cluster) do
        authorized_client.cluster
      end

      before do
        authorized_collection.insert_many(documents)
        cluster.schedule_kill_cursor(cursor.id,
                                     cursor.send(:kill_cursors_op_spec),
                                     cursor.instance_variable_get(:@server))
      end

      let(:view) do
        Mongo::Collection::View.new(
            authorized_collection,
            {},
            :batch_size => 2,
        )
      end

      let!(:cursor) do
        view.to_enum.next
        view.instance_variable_get(:@cursor)
      end

      it 'schedules a kill cursors op' do
        cluster.instance_variable_get(:@periodic_executor).flush
        expect do
          cursor.to_a
        end.to raise_exception(Mongo::Error::OperationFailure, /[cC]ursor.*not found/)
      end

      context 'when the cursor is unregistered before the kill cursors operations are executed' do
        # Sometimes JRuby yields 4 documents even though we are allowing
        # repeated cursor iteration below
        fails_on_jruby

        it 'does not send a kill cursors operation for the unregistered cursor' do
          # We need to verify that the cursor was able to retrieve more documents
          # from the server so that more than one batch is successfully received

          cluster.unregister_cursor(cursor.id)

          # The initial read is done on an enum obtained from the cursor.
          # The read below is done directly on the cursor. These are two
          # different objects. In MRI, iterating like this yields all of the
          # documents, hence we retrieved one document in the setup and
          # we expect to retrieve the remaining 5 here. In JRuby it appears that
          # the enum may buffers the first batch, such that the second document
          # sometimes is lost to the iteration and we retrieve 4 documents below.
          # But sometimes we get all 5 documents. In either case, all of the
          # documents are retrieved via two batches thus fulfilling the
          # requirement of the test to continue iterating the cursor.

=begin When repeated iteration of cursors is prohibited, these are the expectations
          if BSON::Environment.jruby?
            expected_counts = [4, 5]
          else
            expected_counts = [5]
          end
=end

          # Since currently repeated iteration of cursors is allowed, calling
          # to_a on the cursor would perform such an iteration and return
          # all documents of the initial read.
          expected_counts = [6]

          expect(expected_counts).to include(cursor.to_a.size)
        end
      end
    end

    context 'when the cursor is fully iterated' do

      let(:documents) do
        (1..3).map{ |i| { field: "test#{i}" }}
      end

      before do
        authorized_collection.delete_many
        authorized_collection.insert_many(documents)
      end

      let(:view) do
        authorized_collection.find({}, batch_size: 2)
      end

      let(:cursor) do
        view.instance_variable_get(:@cursor)
      end

      let!(:cursor_id) do
        enum.next
        enum.next
        cursor.id
      end

      let(:enum) do
        view.to_enum
      end

      let(:cursor_reaper) do
        authorized_collection.client.cluster.instance_variable_get(:@cursor_reaper)
      end

      it 'removes the cursor id from the active cursors tracked by the cluster cursor manager' do
        enum.next
        expect(cursor_reaper.instance_variable_get(:@active_cursors)).not_to include(cursor_id)
      end
    end
  end

  context 'when an implicit session is used' do
    min_server_fcv '3.6'

    let(:subscriber) { EventSubscriber.new }

    let(:subscribed_client) do
      authorized_client.tap do |client|
        client.subscribe(Mongo::Monitoring::COMMAND, subscriber)
      end
    end

    let(:collection) do
      subscribed_client[TEST_COLL]
    end

    before do
      collection.insert_many(documents)
    end

    let(:cursor) do
      view.instance_variable_get(:@cursor)
    end

    let(:enum) do
      view.to_enum
    end

    let(:session_pool_ids) do
      queue = view.client.cluster.session_pool.instance_variable_get(:@queue)
      queue.collect { |s| s.session_id }
    end

    let(:find_events) do
      subscriber.started_events.select { |e| e.command_name == "find" }
    end

    context 'when all results are retrieved in the first response' do

      let(:documents) do
        (1..2).map{ |i| { field: "test#{i}" }}
      end

      let(:view) do
        collection.find
      end

      it 'returns the session to the cluster session pool' do
        1.times { enum.next }
        expect(find_events.collect { |event| event.command['lsid'] }.uniq.size).to eq(1)
        expect(session_pool_ids).to include(find_events.collect { |event| event.command['lsid'] }.uniq.first)
      end
    end

    context 'when a getMore is needed to retrieve all results' do
      min_server_fcv '3.6'
      require_topology :single, :replica_set

      let(:documents) do
        (1..4).map{ |i| { field: "test#{i}" }}
      end

      let(:view) do
        collection.find({}, batch_size: 2, limit: 4)
      end


      context 'when not all documents are iterated' do

        it 'returns the session to the cluster session pool' do
          3.times { enum.next }
          expect(find_events.collect { |event| event.command['lsid'] }.uniq.size).to eq(1)
          expect(session_pool_ids).to include(find_events.collect { |event| event.command['lsid'] }.uniq.first)
        end
      end

      context 'when all documents are iterated' do

        it 'returns the session to the cluster session pool' do
          4.times { enum.next }
          expect(find_events.collect { |event| event.command['lsid'] }.uniq.size).to eq(1)
          expect(session_pool_ids).to include(find_events.collect { |event| event.command['lsid'] }.uniq.first)
        end
      end
    end
  end

  describe '#inspect' do

    let(:view) do
      Mongo::Collection::View.new(authorized_collection)
    end

    let(:query_spec) do
      { selector: {}, options: {},
        db_name: SpecConfig.instance.test_db, coll_name: TEST_COLL }
    end

    let(:reply) do
      Mongo::Operation::Find.new(query_spec).tap do |reply|
        allow(reply).to receive(:cursor_id).and_return(42)
      end
    end

    let(:cursor) do
      described_class.new(view, reply, authorized_primary)
    end

    it 'returns a string' do
      expect(cursor.inspect).to be_a(String)
    end

    it 'returns a string containing the collection view inspect string' do
      expect(cursor.inspect).to match(/.*#{view.inspect}.*/)
    end
  end

  describe '#to_a' do

    let(:view) do
      Mongo::Collection::View.new(authorized_collection, {}, batch_size: 10)
    end

    let(:query_spec) do
      { :selector => {}, :options => {}, :db_name => SpecConfig.instance.test_db,
        :coll_name => authorized_collection.name }
    end

    let(:reply) do
      view.send(:send_initial_query, authorized_primary)
    end

    let(:cursor) do
      described_class.new(view, reply, authorized_primary)
    end

    context 'after partially iterating the cursor' do
      before do
        authorized_collection.drop
        docs = []
        100.times do |i|
          docs << {a: i}
        end
        authorized_collection.insert_many(docs)
      end

      context 'after #each was called once' do
        before do
          cursor.each do |doc|
            break
          end
        end

        it 'iterates from the beginning of the view' do
          expect(cursor.to_a.map { |doc| doc['a'] }).to eq((0..99).to_a)
        end
      end

      context 'after exactly one batch was iterated' do
        before do
          cursor.each_with_index do |doc, i|
            break if i == 9
          end
        end

        it 'iterates from the beginning of the view' do
          expect(cursor.to_a.map { |doc| doc['a'] }).to eq((0..99).to_a)
        end
      end

      context 'after two batches were iterated' do
        before do
          cursor.each_with_index do |doc, i|
            break if i == 19
          end
        end

=begin Behavior of pre-2.10 driver:
        it 'skips the second batch' do
          expect(cursor.to_a.map { |doc| doc['a'] }).to eq((0..9).to_a + (20..99).to_a)
        end
=end
        it 'raises InvalidCursorOperation' do
          expect do
            cursor.to_a
          end.to raise_error(Mongo::Error::InvalidCursorOperation, 'Cannot restart iteration of a cursor which issued a getMore')
        end
      end
    end
  end
end
