# frozen_string_literal: true

require 'spec_helper'

describe 'CRUD operations' do
  let(:client) { authorized_client }
  let(:collection) { client['crud_integration'] }

  before do
    collection.delete_many
  end

  describe 'find' do
    context 'when using the legacy $query syntax' do
      before do
        collection.insert_one(_id: 1, test: 1)
        collection.insert_one(_id: 2, test: 2)
        collection.insert_one(_id: 3, test: 3)
      end

      context 'filter only' do
        it 'passes the filter' do
          expect(collection.find('$query': { test: 1 }).first).to eq({ '_id' => 1, 'test' => 1 })
        end
      end

      context 'empty filter with order' do
        it 'passes the filter' do
          expect(collection.find('$query': {}, '$orderby': { test: 1 }).first).to eq({ '_id' => 1, 'test' => 1 })
          expect(collection.find('$query': {}, '$orderby': { test: -1 }).first).to eq({ '_id' => 3, 'test' => 3 })
        end
      end

      context 'filter with order' do
        it 'passes both filter and order' do
          expect(collection.find(:'$query' => { test: { '$gt' => 1 } },
                                 '$orderby' => { test: 1 }).first).to eq({ '_id' => 2,
                                                                           'test' => 2 })
          expect(collection.find(:'$query' => { test: { '$gt' => 1 } },
                                 '$orderby' => { test: -1 }).first).to eq({
                                                                            '_id' => 3, 'test' => 3
                                                                          })
        end
      end
    end

    context 'with read concern' do
      context 'with read concern specified on operation level' do
        it 'passes the read concern' do
          event = Utils.get_command_event(client, 'find') do |client|
            client['foo'].find({}, read_concern: { level: :local }).to_a
          end
          expect(event.command.fetch('readConcern')).to eq({ 'level' => 'local' })
        end
      end

      context 'with read concern specified on collection level' do
        it 'passes the read concern' do
          event = Utils.get_command_event(client, 'find') do |client|
            client['foo', read_concern: { level: :local }].find.to_a
          end
          expect(event.command.fetch('readConcern')).to eq({ 'level' => 'local' })
        end
      end

      context 'with read concern specified on client level' do
        let(:client) { authorized_client.with(read_concern: { level: :local }) }

        it 'passes the read concern' do
          event = Utils.get_command_event(client, 'find') do |client|
            client['foo'].find.to_a
          end
          expect(event.command.fetch('readConcern')).to eq({ 'level' => 'local' })
        end
      end
    end

    context 'with oplog_replay option' do
      let(:collection_name) { 'crud_integration_oplog_replay' }

      let(:oplog_query) do
        { ts: { '$gt' => 1 } }
      end

      context 'passed to operation' do
        it 'passes the option' do
          event = Utils.get_command_event(client, 'find') do |client|
            client[collection_name].find(oplog_query, oplog_replay: true).to_a
          end
          event.command.fetch('oplogReplay').should be true
        end

        it 'warns' do
          client.should receive(:log_warn).with('The :oplog_replay option is deprecated and ignored by MongoDB 4.4 and later')
          client[collection_name].find(oplog_query, oplog_replay: true).to_a
        end
      end

      context 'set on collection' do
        it 'passes the option' do
          event = Utils.get_command_event(client, 'find') do |client|
            client[collection_name, oplog_replay: true].find(oplog_query).to_a
          end
          event.command.fetch('oplogReplay').should be true
        end

        it 'warns' do
          client.should receive(:log_warn).with('The :oplog_replay option is deprecated and ignored by MongoDB 4.4 and later')
          client[collection_name, oplog_replay: true].find(oplog_query).to_a
        end
      end
    end
  end

  describe 'explain' do
    context 'with explicit session' do
      it 'passes the session' do
        client.start_session do |session|
          event = Utils.get_command_event(client, 'explain') do |client|
            client['foo'].find({}, session: session).explain.should be_explain_output
          end
          expect(event.command.fetch('lsid')).to eq(session.session_id)
        end
      end
    end

    context 'with read preference specified on operation level' do
      require_topology :sharded

      it 'passes the read preference' do
        event = Utils.get_command_event(client, 'explain') do |client|
          client['foo'].find({}, read: { mode: :secondary_preferred }).explain.should be_explain_output
        end
        expect(event.command.fetch('$readPreference')).to eq({ 'mode' => 'secondaryPreferred' })
      end
    end

    context 'with read preference specified on collection level' do
      require_topology :sharded

      it 'passes the read preference' do
        event = Utils.get_command_event(client, 'explain') do |client|
          client['foo', read: { mode: :secondary_preferred }].find.explain.should be_explain_output
        end
        expect(event.command.fetch('$readPreference')).to eq({ 'mode' => 'secondaryPreferred' })
      end
    end

    context 'with read preference specified on client level' do
      require_topology :sharded

      let(:client) { authorized_client.with(read: { mode: :secondary_preferred }) }

      it 'passes the read preference' do
        event = Utils.get_command_event(client, 'explain') do |client|
          client['foo'].find.explain.should be_explain_output
        end
        expect(event.command.fetch('$readPreference')).to eq({ 'mode' => 'secondaryPreferred' })
      end
    end

    context 'with read concern' do
      context 'with read concern specified on operation level' do
        # Read concern is not allowed in explain command, driver drops it.
        it 'drops the read concern' do
          event = Utils.get_command_event(client, 'explain') do |client|
            client['foo'].find({}, read_concern: { level: :local }).explain.should have_key('queryPlanner')
          end
          event.command.should_not have_key('readConcern')
        end
      end

      context 'with read concern specified on collection level' do
        # Read concern is not allowed in explain command, driver drops it.
        it 'drops the read concern' do
          event = Utils.get_command_event(client, 'explain') do |client|
            client['foo', read_concern: { level: :local }].find.explain.should have_key('queryPlanner')
          end
          event.command.should_not have_key('readConcern')
        end
      end

      context 'with read concern specified on client level' do
        let(:client) { authorized_client.with(read_concern: { level: :local }) }

        # Read concern is not allowed in explain command, driver drops it.
        it 'drops the read concern' do
          event = Utils.get_command_event(client, 'explain') do |client|
            client['foo'].find.explain.should have_key('queryPlanner')
          end
          event.command.should_not have_key('readConcern')
        end
      end
    end
  end

  describe 'insert' do
    context 'user documents' do
      let(:doc) do
        IceNine.deep_freeze(test: 42)
      end

      it 'does not mutate user documents' do
        lambda do
          collection.insert_one(doc)
        end.should_not raise_error
      end
    end

    context 'inserting a BSON::Int64' do
      before do
        collection.insert_one(int64: BSON::Int64.new(42))
      end

      it 'is stored as the correct type' do
        result = collection.find(int64: { '$type' => 'long' }).first
        expect(result).not_to be_nil
        expect(result['int64']).to eq(42)
      end
    end

    context 'inserting a BSON::Int32' do
      before do
        collection.insert_one(int32: BSON::Int32.new(42))
      end

      it 'is stored as the correct type' do
        result = collection.find(int32: { '$type' => 'int' }).first
        expect(result).not_to be_nil
        expect(result['int32']).to eq(42)
      end
    end

    context 'with automatic encryption' do
      require_libmongocrypt
      require_enterprise

      include_context 'define shared FLE helpers'
      include_context 'with local kms_providers'

      let(:encrypted_collection) do
        new_local_client(
          SpecConfig.instance.addresses,
          SpecConfig.instance.test_options.merge(
            auto_encryption_options: {
              kms_providers: kms_providers,
              key_vault_namespace: key_vault_namespace,
              schema_map: { 'auto_encryption.users' => schema_map },
              # Spawn mongocryptd on non-default port for sharded cluster tests
              extra_options: extra_options,
            },
            database: 'auto_encryption'
          )
        )['users']
      end

      let(:collection) { authorized_client.use('auto_encryption')['users'] }

      context 'inserting a BSON::Int64' do
        before do
          encrypted_collection.insert_one(ssn: '123-456-7890', int64: BSON::Int64.new(42))
        end

        it 'is stored as the correct type' do
          result = collection.find(int64: { '$type' => 'long' }).first
          expect(result).not_to be_nil
          expect(result['int64']).to eq(42)
        end
      end

      context 'inserting a BSON::Int32' do
        before do
          encrypted_collection.insert_one(ssn: '123-456-7890', int32: BSON::Int32.new(42))
        end

        it 'is stored as the correct type' do
          result = collection.find(int32: { '$type' => 'int' }).first
          expect(result).not_to be_nil
          expect(result['int32']).to eq(42)
        end
      end
    end
  end

  describe 'upsert' do
    context 'with default write concern' do
      it 'upserts' do
        expect(collection.count_documents).to eq(0)

        res = collection.find(_id: 'foo').update_one({ '$set' => { foo: 'bar' } }, upsert: true)

        expect(res.documents.first['upserted'].length).to eq(1)

        expect(collection.count_documents).to eq(1)
      end
    end

    context 'unacknowledged write' do
      let(:unack_collection) do
        collection.with(write_concern: { w: 0 })
      end

      before do
        unack_collection.write_concern.acknowledged?.should be false
      end

      it 'upserts' do
        expect(unack_collection.count_documents).to eq(0)

        unack_collection.find(_id: 'foo').update_one({ '$set' => { foo: 'bar' } }, upsert: true)

        # since write concern is unacknowledged, wait for the data to be
        # persisted (hopefully)
        sleep 0.25

        expect(unack_collection.count_documents).to eq(1)
      end
    end
  end
end
