require 'spec_helper'

describe Mongo::Session::SessionPool, if: test_sessions? do

  let(:cluster) do
    authorized_client.cluster
  end

  describe '.create' do

    let!(:pool) do
      described_class.create(cluster)
    end

    it 'creates a session pool' do
      expect(pool).to be_a(Mongo::Session::SessionPool)
    end

    it 'adds the pool as an instance variable on the cluster' do
      expect(cluster.session_pool).to eq(pool)
    end
  end

  describe '#initialize' do

    let(:pool) do
      described_class.new(cluster)
    end

    it 'sets the cluster' do
      expect(pool.instance_variable_get(:@cluster)).to be(authorized_client.cluster)
    end
  end

  describe '#inspect' do

    let(:pool) do
      described_class.new(cluster)
    end

    before do
      s = pool.checkout
      pool.checkin(s)
    end

    it 'includes the Ruby object_id in the formatted string' do
      expect(pool.inspect).to include(pool.object_id.to_s)
    end

    it 'includes the pool size in the formatted string' do
      expect(pool.inspect).to include('current_size=1')
    end
  end

  describe 'checkout' do

    let(:pool) do
      described_class.new(cluster)
    end

    context 'when a session is checked out' do

      let!(:session_a) do
        pool.checkout
      end

      let!(:session_b) do
        pool.checkout
      end

      before do
        pool.checkin(session_a)
        pool.checkin(session_b)
      end

      it 'is returned to the front of the queue' do
        expect(pool.checkout).to be(session_b)
        expect(pool.checkout).to be(session_a)
      end
    end

    context 'when there are sessions about to expire in the queue' do

      let(:old_session_a) do
        pool.checkout
      end

      let(:old_session_b) do
        pool.checkout
      end

      before do
        pool.checkin(old_session_a)
        pool.checkin(old_session_b)
        allow(old_session_a).to receive(:last_use).and_return(Time.now - 1800)
        allow(old_session_b).to receive(:last_use).and_return(Time.now - 1800)
      end

      context 'when a session is checked out' do

        let(:checked_out_session) do
          pool.checkout
        end

        it 'disposes of the old session and returns a new one' do
          expect(checked_out_session).not_to be(old_session_a)
          expect(checked_out_session).not_to be(old_session_b)
          expect(pool.instance_variable_get(:@queue)).to be_empty
        end
      end
    end

    context 'when a sessions that is about to expire is checked in' do

      let(:old_session_a) do
        pool.checkout
      end

      let(:old_session_b) do
        pool.checkout
      end

      before do
        allow(old_session_a).to receive(:last_use).and_return(Time.now - 1800)
        allow(old_session_b).to receive(:last_use).and_return(Time.now - 1800)
        pool.checkin(old_session_a)
        pool.checkin(old_session_b)
      end

      it 'disposes of the old sessions instead of adding them to the pool' do
        expect(pool.checkout).not_to be(old_session_a)
        expect(pool.checkout).not_to be(old_session_b)
        expect(pool.instance_variable_get(:@queue)).to be_empty
      end
    end
  end

  describe '#end_sessions' do

    let(:pool) do
      described_class.create(client.cluster)
    end

    let!(:session_a) do
      pool.checkout
    end

    let!(:session_b) do
      pool.checkout
    end

    let(:client) do
      subscribed_client
    end

    after do
      client.close
    end

    context 'when the number of ids is not larger than 10,000' do

      before do
        client.database.command(ping: 1)
        pool.checkin(session_a)
        pool.checkin(session_b)
      end

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

      let(:end_sessions_command) do
        pool.end_sessions
        EventSubscriber.started_events.find { |c| c.command_name == :endSessions}
      end

      it 'sends the endSessions command with all the session ids' do
        end_sessions_command
        expect(end_sessions_command.command[:endSessions]).to include(BSON::Document.new(session_a.session_id))
        expect(end_sessions_command.command[:endSessions]).to include(BSON::Document.new(session_b.session_id))
      end

      context 'when talking to a replica set or mongos' do

        it 'sends the endSessions command with all the session ids and cluster time' do
          end_sessions_command
          expect(end_sessions_command.command[:endSessions]).to include(BSON::Document.new(session_a.session_id))
          expect(end_sessions_command.command[:endSessions]).to include(BSON::Document.new(session_b.session_id))
          expect(end_sessions_command.command[:$clusterTime]).to eq(client.cluster.cluster_time)
        end
      end
    end

    context 'when the number of ids is larger than 10_000' do

      let(:ids) do
        10_001.times.map do |i|
          bytes = [SecureRandom.uuid.gsub(/\-/, '')].pack('H*')
          BSON::Document.new(id: BSON::Binary.new(bytes, :uuid))
        end
      end

      before do
        queue = []
        ids.each do |id|
          queue << double('session', session_id: id)
        end
        pool.instance_variable_set(:@queue, queue)
        expect(Mongo::Operation::Commands::Command).to receive(:new).at_least(:twice).and_call_original
      end

      let(:end_sessions_commands) do
        EventSubscriber.started_events.select { |c| c.command_name == :endSessions}
      end

      it 'sends the command more than once' do
        pool.end_sessions
        expect(end_sessions_commands.size).to eq(2)
        expect(end_sessions_commands[0].command[:endSessions]).to eq(ids[0...10_000])
        expect(end_sessions_commands[1].command[:endSessions]).to eq([ids[10_000]])
      end
    end
  end
end
