# frozen_string_literal: true
# rubocop:todo all

shared_examples 'an operation using a session' do

  describe 'operation execution' do
    min_server_fcv '3.6'
    require_topology :replica_set, :sharded

    context 'when the session is created from the same client used for the operation' do

      let(:session) do
        client.start_session
      end

      let(:server_session) do
        session.instance_variable_get(:@server_session)
      end

      let!(:before_last_use) do
        server_session.last_use
      end

      let!(:before_operation_time) do
        (session.operation_time || 0)
      end

      let!(:operation_result) do
        operation
      end

      after do
        session.end_session
      end

      it 'updates the last use value' do
        expect(server_session.last_use).not_to eq(before_last_use)
      end

      it 'updates the operation time value' do
        expect(session.operation_time).not_to eq(before_operation_time)
      end

      it 'does not close the session when the operation completes' do
        expect(session.ended?).to be(false)
      end
    end

    context 'when a session from another client is provided' do

      let(:session) do
        another_authorized_client.start_session
      end

      let(:operation_result) do
        operation
      end

      it 'raises an exception' do
        expect do
          operation_result
        end.to raise_exception(Mongo::Error::InvalidSession)
      end
    end

    context 'when the session is ended before it is used' do

      let(:session) do
        client.start_session
      end

      before do
        session.end_session
      end

      let(:operation_result) do
        operation
      end

      it 'raises an exception' do
        expect {
          operation_result
        }.to raise_exception(Mongo::Error::InvalidSession)
      end
    end
  end
end

shared_examples 'a failed operation using a session' do

  context 'when the operation fails' do
    min_server_fcv '3.6'
    require_topology :replica_set, :sharded

    let!(:before_last_use) do
      session.instance_variable_get(:@server_session).last_use
    end

    let!(:before_operation_time) do
      (session.operation_time || 0)
    end

    let!(:operation_result) do
      sleep 0.2
      begin; failed_operation; rescue => e; e; end
    end

    let(:session) do
      client.start_session
    end

    it 'raises an error' do
      expect([Mongo::Error::OperationFailure::Family,
              Mongo::Error::BulkWriteError].any? { |e| e === operation_result }).to be true
    end

    it 'updates the last use value' do
      expect(session.instance_variable_get(:@server_session).last_use).not_to eq(before_last_use)
    end

    it 'updates the operation time value' do
      expect(session.operation_time).not_to eq(before_operation_time)
    end
  end
end

shared_examples 'an explicit session with an unacknowledged write' do

  context 'when sessions are supported' do
    min_server_fcv '3.6'

    let(:session) do
      client.start_session
    end

    it 'does not add a session id to the operation' do
      subscriber.clear_events!
      operation
      subscriber.non_auth_command_started_events.length.should == 1
      expect(subscriber.non_auth_command_started_events.collect(&:command).collect { |cmd| cmd['lsid'] }.compact).to be_empty
    end
  end

  context 'when sessions are not supported' do
    max_server_version '3.4'

    let(:session) do
      nil
    end

    it 'does not add a session id to the operation' do
      expect(Mongo::Session).not_to receive(:new)
      subscriber.clear_events!
      operation
      subscriber.non_auth_command_started_events.length.should == 1
      expect(subscriber.non_auth_command_started_events.collect(&:command).collect { |cmd| cmd['lsid'] }.compact).to be_empty
    end
  end
end

shared_examples 'an implicit session with an unacknowledged write' do

  context 'when sessions are supported' do
    min_server_fcv '3.6'

    it 'does not add a session id to the operation' do
      subscriber.clear_events!
      operation
      subscriber.non_auth_command_started_events.length.should == 1
      expect(subscriber.non_auth_command_started_events.collect(&:command).collect { |cmd| cmd['lsid'] }.compact).to be_empty
    end
  end

  context 'when sessions are not supported' do
    max_server_version '3.4'

    it 'does not add a session id to the operation' do
      subscriber.clear_events!
      operation
      subscriber.non_auth_command_started_events.length.should == 1
      expect(subscriber.non_auth_command_started_events.collect(&:command).collect { |cmd| cmd['lsid'] }.compact).to be_empty
    end
  end
end

shared_examples 'an operation supporting causally consistent reads' do

  let(:subscriber) { Mrss::EventSubscriber.new }

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

  context 'when connected to a standalone' do
    min_server_fcv '3.6'
    require_topology :single

    context 'when the collection specifies a read concern' do

      let(:collection) do
        client[TEST_COLL, read_concern: { level: 'majority' }]
      end

      context 'when the session has causal_consistency set to true' do

        let(:session) do
          client.start_session(causal_consistency: true)
        end

        it 'does not add the afterClusterTime to the read concern in the command' do
          expect(command['readConcern']['afterClusterTime']).to be_nil
        end
      end

      context 'when the session has causal_consistency set to false' do

        let(:session) do
          client.start_session(causal_consistency: false)
        end

        it 'does not add the afterClusterTime to the read concern in the command' do
          expect(command['readConcern']['afterClusterTime']).to be_nil
        end
      end

      context 'when the session has causal_consistency not set' do

        let(:session) do
          client.start_session
        end

        it 'does not add the afterClusterTime to the read concern in the command' do
          expect(command['readConcern']['afterClusterTime']).to be_nil
        end
      end
    end

    context 'when the collection does not specify a read concern' do

      let(:collection) do
        client[TEST_COLL]
      end

      context 'when the session has causal_consistency set to true' do

        let(:session) do
          client.start_session(causal_consistency: true)
        end

        it 'does not include the read concern in the command' do
          expect(command['readConcern']).to be_nil
        end
      end

      context 'when the session has causal_consistency set to false' do

        let(:session) do
          client.start_session(causal_consistency: false)
        end

        it 'does not include the read concern in the command' do
          expect(command['readConcern']).to be_nil
        end
      end

      context 'when the session has causal_consistency not set' do

        let(:session) do
          client.start_session
        end

        it 'does not include the read concern in the command' do
          expect(command['readConcern']).to be_nil
        end
      end
    end
  end

  context 'when connected to replica set or sharded cluster' do
    min_server_fcv '3.6'
    require_topology :replica_set, :sharded

    context 'when the collection specifies a read concern' do

      let(:collection) do
        client[TEST_COLL, read_concern: { level: 'majority' }]
      end

      context 'when the session has causal_consistency set to true' do

        let(:session) do
          client.start_session(causal_consistency: true)
        end

        context 'when the session has an operation time' do

          before do
            client.database.command({ ping: 1 }, session: session)
          end

          let!(:operation_time) do
            session.operation_time
          end

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority', afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the read concern in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the session does not have an operation time' do

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority')
          end

          it 'leaves the read concern document unchanged' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the operation time is advanced' do

          before do
            session.advance_operation_time(operation_time)
          end

          let(:operation_time) do
            BSON::Timestamp.new(0, 1)
          end

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority', afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the new operation time and read concern in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end
      end

      context 'when the session has causal_consistency set to false' do

        let(:session) do
          client.start_session(causal_consistency: false)
        end

        context 'when the session does not have an operation time' do

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority')
          end

          it 'leaves the read concern document unchanged' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the session has an operation time' do

          before do
            client.database.command({ ping: 1 }, session: session)
          end

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority')
          end

          it 'leaves the read concern document unchanged' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the operation time is advanced' do

          before do
            session.advance_operation_time(operation_time)
          end

          let(:operation_time) do
            BSON::Timestamp.new(0, 1)
          end

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority')
          end

          it 'leaves the read concern document unchanged' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end
      end

      context 'when the session has causal_consistency not set' do

        let(:session) do
          client.start_session
        end

        context 'when the session does not have an operation time' do

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority')
          end

          it 'leaves the read concern document unchanged' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the session has an operation time' do

          before do
            client.database.command({ ping: 1 }, session: session)
          end

          let!(:operation_time) do
            session.operation_time
          end

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority', afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the new operation time and read concern in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the operation time is advanced' do

          before do
            session.advance_operation_time(operation_time)
          end

          let(:operation_time) do
            BSON::Timestamp.new(0, 1)
          end

          let(:expected_read_concern) do
            BSON::Document.new(level: 'majority', afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the new operation time and read concern in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end
      end
    end

    context 'when the collection does not specify a read concern' do

      let(:collection) do
        client[TEST_COLL]
      end

      context 'when the session has causal_consistency set to true' do

        let(:session) do
          client.start_session(causal_consistency: true)
        end

        context 'when the session does not have an operation time' do

          it 'does not include the read concern in the command' do
            expect(command['readConcern']).to be_nil
          end
        end

        context 'when the session has an operation time' do

          before do
            client.database.command({ ping: 1 }, session: session)
          end

          let!(:operation_time) do
            session.operation_time
          end

          let(:expected_read_concern) do
            BSON::Document.new(afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the read concern in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the operation time is advanced' do

          before do
            session.advance_operation_time(operation_time)
          end

          let(:operation_time) do
            BSON::Timestamp.new(0, 1)
          end

          let(:expected_read_concern) do
            BSON::Document.new(afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the new operation time in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end
      end

      context 'when the session has causal_consistency set to false' do

        let(:session) do
          client.start_session(causal_consistency: false)
        end

        context 'when the session does not have an operation time' do

          it 'does not include the read concern in the command' do
            expect(command['readConcern']).to be_nil
          end
        end

        context 'when the session has an operation time' do

          before do
            client.database.command({ ping: 1 }, session: session)
          end

          it 'does not include the read concern in the command' do
            expect(command['readConcern']).to be_nil
          end
        end

        context 'when the operation time is advanced' do

          before do
            session.advance_operation_time(operation_time)
          end

          let(:operation_time) do
            BSON::Timestamp.new(0, 1)
          end

          let(:expected_read_concern) do
            BSON::Document.new(afterClusterTime: operation_time)
          end

          it 'does not include the read concern in the command' do
            expect(command['readConcern']).to be_nil
          end
        end
      end

      context 'when the session has causal_consistency not set' do

        let(:session) do
          client.start_session
        end

        context 'when the session does not have an operation time' do

          it 'does not include the read concern in the command' do
            expect(command['readConcern']).to be_nil
          end
        end

        context 'when the session has an operation time' do

          before do
            client.database.command({ ping: 1 }, session: session)
          end

          let!(:operation_time) do
            session.operation_time
          end

          let(:expected_read_concern) do
            BSON::Document.new(afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the read concern in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end

        context 'when the operation time is advanced' do

          before do
            session.advance_operation_time(operation_time)
          end

          let(:operation_time) do
            BSON::Timestamp.new(0, 1)
          end

          let(:expected_read_concern) do
            BSON::Document.new(afterClusterTime: operation_time)
          end

          it 'merges the afterClusterTime with the new operation time in the command' do
            expect(command['readConcern']).to eq(expected_read_concern)
          end
        end
      end
    end
  end
end

# Since background operatons can advance cluster time, exact cluster time
# comparisons sometimes fail. Work around this by retrying the tests.
shared_examples 'an operation updating cluster time' do

  let(:cluster) do
    client.cluster
  end

  let(:session) do
    client.start_session
  end

  let(:subscriber) { Mrss::EventSubscriber.new }

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

  shared_examples_for 'does not update the cluster time of the cluster' do
    retry_test
    it 'does not update the cluster time of the cluster' do
      bct = before_cluster_time
      reply_cluster_time
      expect(client.cluster.cluster_time).to eq(before_cluster_time)
    end
  end

  context 'when the command is run once' do

    context 'when the server is version 3.6' do
      min_server_fcv '3.6'

      context 'when the cluster is sharded or a replica set' do
        retry_test
        require_topology :replica_set, :sharded

        let(:reply_cluster_time) do
          operation_with_session
          subscriber.succeeded_events[-1].reply['$clusterTime']
        end

        it 'updates the cluster time of the cluster' do
          rct = reply_cluster_time
          expect(cluster.cluster_time).to eq(rct)
        end

        it 'updates the cluster time of the session' do
          rct = reply_cluster_time
          expect(session.cluster_time).to eq(rct)
        end
      end

      context 'when the server is a standalone' do
        require_topology :single

        let(:before_cluster_time) do
          client.cluster.cluster_time
        end

        let!(:reply_cluster_time) do
          operation_with_session
          subscriber.succeeded_events[-1].reply['$clusterTime']
        end

        it_behaves_like 'does not update the cluster time of the cluster'

        retry_test
        it 'does not update the cluster time of the session' do
          reply_cluster_time
          expect(session.cluster_time).to be_nil
        end
      end
    end

    context 'when the server is less than version 3.6' do
      max_server_version '3.4'

      let(:before_cluster_time) do
        client.cluster.cluster_time
      end

      let(:reply_cluster_time) do
        operation
        subscriber.succeeded_events[-1].reply['$clusterTime']
      end

      it_behaves_like 'does not update the cluster time of the cluster'
    end
  end

  context 'when the command is run twice' do

    let(:reply_cluster_time) do
      operation_with_session
      subscriber.succeeded_events[-1].reply['$clusterTime']
    end

    context 'when the cluster is sharded or a replica set' do
      min_server_fcv '3.6'
      require_topology :replica_set, :sharded

      context 'when the session cluster time is advanced' do

        before do
          session.advance_cluster_time(advanced_cluster_time)
        end

        let(:second_command_cluster_time) do
          second_operation
          subscriber.non_auth_command_started_events[-1].command['$clusterTime']
        end

        context 'when the advanced cluster time is greater than the existing cluster time' do

          let(:advanced_cluster_time) do
            new_timestamp = BSON::Timestamp.new(reply_cluster_time[Mongo::Cluster::CLUSTER_TIME].seconds,
                                                reply_cluster_time[Mongo::Cluster::CLUSTER_TIME].increment + 1)
            new_cluster_time = reply_cluster_time.dup
            new_cluster_time.merge(Mongo::Cluster::CLUSTER_TIME => new_timestamp)
          end

          retry_test
          it 'includes the advanced cluster time in the second command' do
            expect(second_command_cluster_time).to eq(advanced_cluster_time)
          end
        end

        context 'when the advanced cluster time is not greater than the existing cluster time' do

          let(:advanced_cluster_time) do
            expect(reply_cluster_time[Mongo::Cluster::CLUSTER_TIME].increment > 0).to be true

            new_timestamp = BSON::Timestamp.new(reply_cluster_time[Mongo::Cluster::CLUSTER_TIME].seconds,
                                                reply_cluster_time[Mongo::Cluster::CLUSTER_TIME].increment - 1)
            new_cluster_time = reply_cluster_time.dup
            new_cluster_time.merge(Mongo::Cluster::CLUSTER_TIME => new_timestamp)
          end

          retry_test
          it 'does not advance the cluster time' do
            expect(second_command_cluster_time).to eq(reply_cluster_time)
          end
        end
      end

      context 'when the session cluster time is not advanced' do

        let(:second_command_cluster_time) do
          second_operation
          subscriber.non_auth_command_started_events[-1].command['$clusterTime']
        end

        retry_test
        it 'includes the received cluster time in the second command' do
          reply_cluster_time
          expect(second_command_cluster_time).to eq(reply_cluster_time)
        end
      end
    end

    context 'when the server is a standalone' do
      min_server_fcv '3.6'
      require_topology :single

      let(:before_cluster_time) do
        client.cluster.cluster_time
      end

      let(:second_command_cluster_time) do
        second_operation
        subscriber.non_auth_command_started_events[-1].command['$clusterTime']
      end

      it 'does not update the cluster time of the cluster' do
        bct = before_cluster_time
        second_command_cluster_time
        expect(client.cluster.cluster_time).to eq(bct)
      end
    end
  end

  context 'when the server is less than version 3.6' do
    max_server_version '3.4'

    let(:before_cluster_time) do
      client.cluster.cluster_time
    end

    it 'does not update the cluster time of the cluster' do
      bct = before_cluster_time
      operation
      expect(client.cluster.cluster_time).to eq(bct)
    end
  end
end

shared_examples 'an operation not using a session' do
  min_server_fcv '3.6'

  describe 'operation execution' do

    context 'when the client has a session' do

      let(:session) do
        client.start_session
      end

      let(:server_session) do
        session.instance_variable_get(:@server_session)
      end

      let!(:before_last_use) do
        server_session.last_use
      end

      let!(:before_operation_time) do
        session.operation_time
      end

      let!(:operation_result) do
        operation
      end

      after do
        session.end_session
      end

      it 'does not send session id in command' do
        expect(command).not_to have_key('lsid')
      end

      it 'does not update the last use value' do
        expect(server_session.last_use).to eq(before_last_use)
      end

      it 'does not update the operation time value' do
        expect(session.operation_time).to eq(before_operation_time)
      end

      it 'does not close the session when the operation completes' do
        expect(session.ended?).to be(false)
      end
    end

    context 'when the session is ended before it is used' do
      let(:session) do
        client.start_session
      end

      before do
        session.end_session
      end

      let(:operation_result) do
        operation
      end

      it 'does not raise an exception' do
        expect {
          operation_result
        }.not_to raise_exception
      end
    end
  end
end

shared_examples 'a failed operation not using a session' do
  min_server_fcv '3.6'

  context 'when the operation fails' do

    let!(:before_last_use) do
      session.instance_variable_get(:@server_session).last_use
    end

    let!(:before_operation_time) do
      session.operation_time
    end

    let!(:operation_result) do
      sleep 0.2
      begin; failed_operation; rescue => e; e; end
    end

    let(:session) do
      client.start_session
    end

    it 'raises an error' do
      expect([Mongo::Error::OperationFailure,
              Mongo::Error::BulkWriteError]).to include(operation_result.class)
    end

    it 'does not update the last use value' do
      expect(session.instance_variable_get(:@server_session).last_use).to eq(before_last_use)
    end

    it 'does not update the operation time value' do
      expect(session.operation_time).to eq(before_operation_time)
    end
  end
end
