# frozen_string_literal: true

require 'spec_helper'

describe Mongo::QueryCache do
  around do |spec|
    Mongo::QueryCache.clear
    Mongo::QueryCache.cache { spec.run }
  end

  before do
    authorized_collection.delete_many
  end

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

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

  let(:authorized_collection) { client['collection_spec'] }

  let(:events) do
    subscriber.command_started_events('find')
  end

  describe '#enabled' do
    context 'when query cache is disabled' do
      before do
        Mongo::QueryCache.enabled = false
      end

      it 'disables the query cache' do
        expect(Mongo::QueryCache.enabled?).to be(false)
      end
    end

    context 'when query cache is enabled' do
      before do
        Mongo::QueryCache.enabled = true
      end

      it 'enables the query cache' do
        expect(Mongo::QueryCache.enabled?).to be(true)
      end
    end
  end

  describe '#cache' do
    before do
      Mongo::QueryCache.enabled = false
    end

    it 'enables the query cache inside the block' do
      Mongo::QueryCache.cache do
        expect(Mongo::QueryCache.enabled?).to be(true)
      end
      expect(Mongo::QueryCache.enabled?).to be(false)
    end
  end

  describe '#uncached' do
    it 'disables the query cache inside the block' do
      Mongo::QueryCache.uncached do
        expect(Mongo::QueryCache.enabled?).to be(false)
      end
      expect(Mongo::QueryCache.enabled?).to be(true)
    end
  end

  describe '#cache_table' do
    before do
      authorized_collection.insert_one({ name: 'testing' })
      authorized_collection.find(name: 'testing').to_a
    end

    it 'gets the cached query' do
      expect(Mongo::QueryCache.send(:cache_table).length).to eq(1)
      authorized_collection.find(name: 'testing').to_a
      expect(events.length).to eq(1)
    end
  end

  describe '#clear' do
    before do
      authorized_collection.insert_one({ name: 'testing' })
      authorized_collection.find(name: 'testing').to_a
    end

    it 'clears the cache' do
      expect(Mongo::QueryCache.send(:cache_table).length).to eq(1)
      Mongo::QueryCache.clear
      expect(Mongo::QueryCache.send(:cache_table).length).to eq(0)
    end
  end

  describe '#set' do
    let(:caching_cursor) { double('Mongo::CachingCursor') }
    let(:namespace) { 'db.coll' }
    let(:selector) { { field: 'value' } }
    let(:skip) { 5 }
    let(:sort) { { field: 'asc' } }
    let(:limit) { 5 }
    let(:projection) { { field: 1 } }
    let(:collation) { { locale: 'fr_CA' } }
    let(:read_concern) { { level: :majority } }
    let(:read_preference) { { mode: :secondary } }

    let(:options) do
      {
        namespace: namespace,
        selector: selector,
        skip: skip,
        sort: sort,
        limit: limit,
        projection: projection,
        collation: collation,
        read_concern: read_concern,
        read_preference: read_preference,
      }
    end

    it 'stores the cursor at the correct key' do
      Mongo::QueryCache.set(caching_cursor, **options)
      expect(Mongo::QueryCache.send(:cache_table)[namespace][[ namespace, selector, skip, sort, projection, collation,
                                                               read_concern, read_preference ]]).to eq(caching_cursor)
    end
  end

  describe '#get' do
    let(:view) do
      double('Mongo::Collection::View').tap do |view|
        allow(view).to receive(:client).and_return(client)
        allow(view).to receive(:operation_timeouts).and_return({})
      end
    end

    let(:result) do
      double('Mongo::Operation::Result').tap do |result|
        allow(result).to receive(:is_a?).with(Mongo::Operation::Result).and_return(true)
      end
    end

    let(:server) { double('Mongo::Server') }
    let(:caching_cursor) { Mongo::CachingCursor.new(view, result, server) }

    let(:options) do
      {
        namespace: 'db.coll',
        selector: { field: 'value' },
      }
    end

    before do
      allow(result).to receive(:cursor_id).and_return(0)
      allow(result).to receive(:namespace).and_return('db.coll')
      allow(result).to receive(:connection_global_id).and_return(1)
      allow(view).to receive(:limit).and_return(nil)
    end

    [ true, false ].each do |load_balancer|
      context "when load_balancer is #{load_balancer}" do
        before do
          allow(server).to receive(:load_balancer?) { load_balancer }
          allow(result).to receive(:connection).and_return(nil) if load_balancer
        end

        context 'when there is no entry in the cache' do
          it 'returns nil' do
            expect(Mongo::QueryCache.get(**options)).to be_nil
          end
        end

        context 'when there is an entry in the cache' do
          before do
            Mongo::QueryCache.set(caching_cursor, **caching_cursor_options)
          end

          context 'when that entry has no limit' do
            let(:caching_cursor_options) do
              {
                namespace: 'db.coll',
                selector: { field: 'value' },
              }
            end

            let(:query_options) do
              caching_cursor_options.merge(limit: limit)
            end

            context 'when the query has a limit' do
              let(:limit) { 5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'when the query has a limit but negative' do
              let(:limit) { -5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'when the query has no limit' do
              let(:limit) { nil }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'when the query has a 0 limit' do
              let(:limit) { 0 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end
          end

          context 'when that entry has a 0 limit' do
            let(:caching_cursor_options) do
              {
                namespace: 'db.coll',
                selector: { field: 'value' },
                limit: 0,
              }
            end

            let(:query_options) do
              caching_cursor_options.merge(limit: limit)
            end

            before do
              allow(view).to receive(:limit).and_return(0)
            end

            context 'when the query has a limit' do
              let(:limit) { 5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'when the query has a limit but negative' do
              let(:limit) { -5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'when the query has no limit' do
              let(:limit) { nil }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'when the query has a 0 limit' do
              let(:limit) { 0 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end
          end

          context 'when that entry has a limit' do
            let(:caching_cursor_options) do
              {
                namespace: 'db.coll',
                selector: { field: 'value' },
                limit: 5,
              }
            end

            let(:query_options) do
              caching_cursor_options.merge(limit: limit)
            end

            before do
              allow(view).to receive(:limit).and_return(5)
            end

            context 'and the new query has a smaller limit' do
              let(:limit) { 4 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'and the new query has a smaller limit but negative' do
              let(:limit) { -4 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'and the new query has a larger limit' do
              let(:limit) { 6 }

              it 'returns nil' do
                expect(Mongo::QueryCache.get(**query_options)).to be_nil
              end
            end

            context 'and the new query has a larger limit but negative' do
              let(:limit) { -6 }

              it 'returns nil' do
                expect(Mongo::QueryCache.get(**query_options)).to be_nil
              end
            end

            context 'and the new query has the same limit' do
              let(:limit) { 5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'and the new query has the same limit but negative' do
              let(:limit) { -5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'and the new query has no limit' do
              let(:limit) { nil }

              it 'returns nil' do
                expect(Mongo::QueryCache.get(**query_options)).to be_nil
              end
            end

            context 'and the new query has a 0 limit' do
              let(:limit) { 0 }

              it 'returns nil' do
                expect(Mongo::QueryCache.get(**query_options)).to be_nil
              end
            end
          end

          context 'when that entry has a negative limit' do
            let(:caching_cursor_options) do
              {
                namespace: 'db.coll',
                selector: { field: 'value' },
                limit: -5,
              }
            end

            let(:query_options) do
              caching_cursor_options.merge(limit: limit)
            end

            before do
              allow(view).to receive(:limit).and_return(-5)
            end

            context 'and the new query has a smaller limit' do
              let(:limit) { 4 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'and the new query has a larger limit' do
              let(:limit) { 6 }

              it 'returns nil' do
                expect(Mongo::QueryCache.get(**query_options)).to be_nil
              end
            end

            context 'and the new query has the same negative limit' do
              let(:limit) { -5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'and the new query has the same positive limit' do
              let(:limit) { 5 }

              it 'returns the caching cursor' do
                expect(Mongo::QueryCache.get(**query_options)).to eq(caching_cursor)
              end
            end

            context 'and the new query has no limit' do
              let(:limit) { nil }

              it 'returns nil' do
                expect(Mongo::QueryCache.get(**query_options)).to be_nil
              end
            end

            context 'and the new query has a 0 limit' do
              let(:limit) { 0 }

              it 'returns nil' do
                expect(Mongo::QueryCache.get(**query_options)).to be_nil
              end
            end
          end
        end
      end
    end
  end

  describe '#clear_namespace' do
    let(:caching_cursor) { double('Mongo::CachingCursor') }
    let(:namespace1) { 'db.coll' }
    let(:namespace2) { 'db.coll2' }
    let(:namespace3) { 'db.coll3' }
    let(:selector) { { field: 'value' } }

    before do
      Mongo::QueryCache.set(caching_cursor, namespace: namespace1, selector: selector)
      Mongo::QueryCache.set(caching_cursor, namespace: namespace2, selector: selector)
      Mongo::QueryCache.set(caching_cursor, namespace: namespace3, selector: selector, multi_collection: true)
    end

    it 'returns nil' do
      expect(Mongo::QueryCache.clear_namespace(namespace1)).to be_nil
    end

    it 'clears the specified namespace in the query cache' do
      Mongo::QueryCache.clear_namespace(namespace1)
      expect(Mongo::QueryCache.send(:cache_table)[namespace1]).to be_nil
    end

    it 'does not clear other namespaces in the query cache' do
      Mongo::QueryCache.clear_namespace(namespace1)
      expect(Mongo::QueryCache.send(:cache_table)[namespace2]).not_to be_nil
    end

    it 'clears the nil namespace' do
      Mongo::QueryCache.clear_namespace(namespace1)
      expect(Mongo::QueryCache.send(:cache_table)[nil]).to be_nil
    end
  end
end
