# frozen_string_literal: true

require 'spec_helper'

describe Mongo::Operation::ReadPreferenceSupported do
  let(:selector) do
    { name: 'test' }
  end

  let(:options) do
    {}
  end

  let(:cluster) do
    double('cluster').tap do |cluster|
      allow(cluster).to receive(:single?).and_return(single?)
    end
  end

  let(:operation) do
    Class.new do
      include Mongo::Operation::ReadPreferenceSupported
    end.new.tap do |op|
      allow(op).to receive(:read).and_return(read_pref)
      allow(op).to receive(:selector).and_return(selector)
      allow(op).to receive(:options).and_return(options)
    end
  end

  let(:description) do
    double('description').tap do |description|
      allow(description).to receive(:mongos?).and_return(mongos?)
      allow(description).to receive(:standalone?).and_return(standalone?)
      # TODO: consider adding tests for load-balanced topologies also
      allow(description).to receive(:load_balancer?).and_return(false)
    end
  end

  let(:server) do
    double('server').tap do |server|
      allow(server).to receive(:cluster).and_return(cluster)
      # TODO: consider adding tests for load-balanced topologies also
      allow(server).to receive(:load_balancer?).and_return(false)
    end
  end

  let(:connection) do
    double('connection').tap do |connection|
      allow(connection).to receive(:server).and_return(server)
      allow(connection).to receive(:description).and_return(description)
    end
  end

  describe '#add_secondary_ok_flag?' do
    let(:actual) do
      operation.send(:add_secondary_ok_flag?, connection)
    end

    shared_examples_for 'sets the secondary_ok flag as expected' do
      it 'sets the secondary_ok flag as expected' do
        expect(actual).to eq(expected)
      end
    end

    shared_examples_for 'never sets secondary_ok' do
      let(:expected) { false }

      context 'when no read preference is specified' do
        let(:read_pref) { Mongo::ServerSelector.get }

        it_behaves_like 'sets the secondary_ok flag as expected'
      end

      context 'when primary read preference is specified' do
        let(:read_pref) { Mongo::ServerSelector.get(mode: :primary) }

        it_behaves_like 'sets the secondary_ok flag as expected'
      end

      context 'when secondary read preference is specified' do
        let(:read_pref) { Mongo::ServerSelector.get(mode: :secondary) }

        it_behaves_like 'sets the secondary_ok flag as expected'
      end
    end

    shared_examples_for 'always sets secondary_ok' do
      let(:expected) { true }

      context 'when no read preference is specified' do
        let(:read_pref) { Mongo::ServerSelector.get }

        it_behaves_like 'sets the secondary_ok flag as expected'
      end

      context 'when primary read preference is specified' do
        let(:read_pref) { Mongo::ServerSelector.get(mode: :primary) }

        it_behaves_like 'sets the secondary_ok flag as expected'
      end

      context 'when secondary read preference is specified' do
        let(:read_pref) { Mongo::ServerSelector.get(mode: :secondary) }

        it_behaves_like 'sets the secondary_ok flag as expected'
      end
    end

    shared_examples_for 'sets secondary_ok if read preference is specified and is not primary' do
      context 'when there is no read preference set' do
        let(:read_pref) { Mongo::ServerSelector.get }

        let(:expected) { false }

        it_behaves_like 'sets the secondary_ok flag as expected'
      end

      context 'when there is a read preference' do
        context 'when the read preference requires the secondary_ok flag' do
          let(:read_pref) { Mongo::ServerSelector.get(mode: :secondary) }

          let(:expected) { true }

          it_behaves_like 'sets the secondary_ok flag as expected'
        end

        context 'when the read preference does not require the secondary_ok flag' do
          let(:read_pref) { Mongo::ServerSelector.get(mode: :primary) }

          let(:expected) { false }

          it_behaves_like 'sets the secondary_ok flag as expected'
        end
      end
    end

    context 'when the topology is Single' do
      let(:single?) { true }
      let(:mongos?) { false }

      context 'when the server is a standalone' do
        let(:standalone?) { true }

        it_behaves_like 'never sets secondary_ok'
      end

      context 'when the server is a mongos' do
        let(:standalone?) { false }
        let(:mongos?) { true }

        it_behaves_like 'always sets secondary_ok'
      end

      context 'when the server is a replica set member' do
        let(:standalone?) { false }
        let(:mongos?) { false }

        it_behaves_like 'always sets secondary_ok'
      end
    end

    context 'when the topology is not Single' do
      let(:single?) { false }
      let(:mongos?) { false }

      context 'when the server is a standalone' do
        let(:standalone?) { true }

        it_behaves_like 'never sets secondary_ok'
      end

      context 'when the server is a mongos' do
        let(:standalone?) { false }
        let(:mongos?) { true }

        it_behaves_like 'sets secondary_ok if read preference is specified and is not primary'
      end

      context 'when the server is a replica set member' do
        let(:standalone?) { false }
        let(:mongos?) { false }

        it_behaves_like 'sets secondary_ok if read preference is specified and is not primary'
      end
    end
  end

  describe '#add_read_preference_legacy' do
    let(:read_pref) do
      Mongo::ServerSelector.get(mode: mode)
    end

    # Behavior of sending $readPreference is the same regardless of topology.
    shared_examples_for '$readPreference in the command' do
      let(:actual) do
        operation.send(:add_read_preference_legacy, operation.send(:selector), connection)
      end

      let(:expected_read_preference) do
        { mode: mode.to_s.gsub(/_(.)/) { Regexp.last_match(1).upcase } }
      end

      shared_examples_for 'adds read preference moving existing contents to $query' do
        let(:expected) do
          { :$query => selector, :$readPreference => expected_read_preference }
        end

        it 'moves existing selector contents under $query and adds read preference' do
          expect(actual).to eq(expected)
        end

        context 'when the selector already has $query in it' do
          let(:selector) do
            { :$query => { name: 'test' },
              :$orderby => { name: -1 } }
          end

          let(:expected) do
            selector.merge(:$readPreference => expected_read_preference)
          end

          it 'keeps existing $query and adds read preference' do
            expect(actual).to eq(expected)
          end
        end
      end

      shared_examples_for 'does not modify selector' do
        it 'does not modify selector' do
          expect(actual).to eq(selector)
        end
      end

      shared_examples_for 'does not send read preference' do
        ([ nil ] + %i[primary primary_preferred secondary secondary_preferred nearest]).each do |_mode|
          active_mode = _mode

          context "when read preference mode is #{active_mode}" do
            let(:mode) { active_mode }

            it_behaves_like 'does not modify selector'
          end
        end
      end

      context 'when the server is a standalone' do
        let(:standalone?) { true }
        let(:mongos?) { false }

        it_behaves_like 'does not send read preference'
      end

      context 'when the server is a mongos' do
        let(:standalone?) { false }
        let(:mongos?) { true }

        context 'when the read preference mode is nil' do
          let(:mode) { nil }

          it_behaves_like 'does not modify selector'
        end

        context 'when the read preference mode is primary' do
          let(:mode) { :primary }

          it_behaves_like 'does not modify selector'
        end

        context 'when the read preference mode is primary_preferred' do
          let(:mode) { :primary_preferred }

          it_behaves_like 'adds read preference moving existing contents to $query'
        end

        context 'when the read preference mode is secondary' do
          let(:mode) { :secondary }

          it_behaves_like 'adds read preference moving existing contents to $query'
        end

        context 'when the read preference mode is secondary_preferred' do
          let(:mode) { :secondary_preferred }

          it_behaves_like 'does not modify selector'

          context 'when there are fields in the selector besides :mode' do
            let(:read_pref) do
              Mongo::ServerSelector.get(mode: mode, tag_sets: [ { 'dc' => 'nyc' } ])
            end

            let(:expected_read_preference) do
              { mode: mode.to_s.gsub(/_(.)/) { Regexp.last_match(1).upcase }, tags: [ { 'dc' => 'nyc' } ] }
            end

            it_behaves_like 'adds read preference moving existing contents to $query'
          end
        end

        context 'when the read preference mode is nearest' do
          let(:mode) { :nearest }

          it_behaves_like 'adds read preference moving existing contents to $query'
        end
      end

      context 'when the server is a replica set member' do
        let(:standalone?) { false }
        let(:mongos?) { false }

        # $readPreference is not sent to replica set nodes running legacy
        # servers - the allowance of secondary reads is handled by secondary_ok
        # flag.
        it_behaves_like 'does not send read preference'
      end
    end

    context 'in single topology' do
      let(:single?) { true }

      it_behaves_like '$readPreference in the command'
    end

    context 'not in single topology' do
      let(:single?) { false }

      it_behaves_like '$readPreference in the command'
    end
  end
end
