# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"

describe Mongoid::Criteria::Queryable::Selectable do

  let(:query) do
    Mongoid::Query.new("id" => "_id")
  end

  shared_examples_for 'returns a cloned query' do

    it "returns a cloned query" do
      expect(selection).to_not equal(query)
    end
  end

  # Hoisting means the operator can be elided, for example
  # Foo.and(a: 1) produces simply {'a' => 1}.
  shared_examples_for 'a hoisting logical operation' do

    let(:query) do
      Mongoid::Query.new
    end

    context "when provided a single criterion" do

      shared_examples_for 'adds the conditions to top level' do

        it "adds the conditions to top level" do
          expect(selection.selector).to eq(
            "field" => [ 1, 2 ]
          )
        end

        it_behaves_like 'returns a cloned query'
      end

      let(:selection) do
        query.send(tested_method, field: [ 1, 2 ])
      end

      it_behaves_like 'adds the conditions to top level'

      context 'when the criterion is wrapped in an array' do
        let(:selection) do
          query.send(tested_method, [{field: [ 1, 2 ] }])
        end

        it_behaves_like 'adds the conditions to top level'
      end

      context 'when the criterion is wrapped in a deep array with nil elements' do
        let(:selection) do
          query.send(tested_method, [[[{field: [ 1, 2 ] }]], [nil]])
        end

        it_behaves_like 'adds the conditions to top level'
      end
    end

    context 'when argument is a Criteria' do
      let(:base) do
        query.where(hello: 'world')
      end

      let(:other) do
        query.where(foo: 'bar')
      end

      let(:result) { base.send(tested_method, other) }

      it 'combines' do
        expect(result.selector).to eq(
          'hello' => 'world',
          'foo' => 'bar',
        )
      end
    end

    context "when provided a single criterion that is handled via Key" do

      shared_examples_for 'adds the conditions to top level' do

        it "adds the conditions to top level" do
          expect(selection.selector).to eq({
            "field" => {'$gt' => 3},
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      let(:selection) do
        query.send(tested_method, :field.gt => 3)
      end

      it_behaves_like 'adds the conditions to top level'

      context 'when the criterion is wrapped in an array' do
        let(:selection) do
          query.send(tested_method, [{ :field.gt => 3 }])
        end

        it_behaves_like 'adds the conditions to top level'
      end

      context 'when the criterion is wrapped in a deep array with nil elements' do
        let(:selection) do
          query.send(tested_method, [[[{ :field.gt => 3 }]], [nil]])
        end

        it_behaves_like 'adds the conditions to top level'
      end

      context 'when the criterion is a time' do
        let(:selection) do
          query.send(tested_method, :field.gte => Time.new(2020, 1, 1))
        end

        it 'adds the conditions' do
          expect(selection.selector).to eq({
            "field" => {'$gte' => Time.new(2020, 1, 1)},
          })
        end

        it 'keeps argument type' do
          selection.selector['field']['$gte'].should be_a(Time)
        end
      end

      context 'when the criterion is a datetime' do
        let(:selection) do
          query.send(tested_method, :field.gte => DateTime.new(2020, 1, 1))
        end

        it 'adds the conditions' do
          expect(selection.selector).to eq({
            "field" => {'$gte' => Time.utc(2020, 1, 1)},
          })
        end

        it 'converts argument to a time' do
          selection.selector['field']['$gte'].should be_a(Time)
        end
      end

      context 'when the criterion is a date' do
        let(:selection) do
          query.send(tested_method, :field.gte => Date.new(2020, 1, 1))
        end

        it 'adds the conditions' do
          expect(selection.selector).to eq({
            "field" => {'$gte' => Time.utc(2020, 1, 1)},
          })
        end

        it 'converts argument to a time' do
          selection.selector['field']['$gte'].should be_a(Time)
        end
      end
    end

    context "when provided a nested criterion" do

      let(:selection) do
        query.send(tested_method, :test.elem_match => { :field.in => [ 1, 2 ] })
      end

      it "builds the correct selector" do
        expect(selection.selector).to eq({
          "test" => { "$elemMatch" => { "field" => { "$in" => [ 1, 2 ] }}}
        })
      end

      it_behaves_like 'returns a cloned query'
    end

    context "when chaining the criteria" do

      context "when the criteria are for different fields" do

        let(:selection) do
          query.and(first: [ 1, 2 ]).send(tested_method, second: [ 3, 4 ])
        end

        it "adds the conditions to top level" do
          expect(selection.selector).to eq({
            "first" => [ 1, 2 ],
            "second" => [ 3, 4 ],
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      context "when the criteria are on the same field" do

        let(:selection) do
          query.and(first: [ 1, 2 ]).send(tested_method, first: [ 3, 4 ])
        end

        it "combines via $and operator" do
          expect(selection.selector).to eq({
            "first" => [ 1, 2 ],
            "$and" => [
              { "first" => [ 3, 4 ] }
            ]
          })
        end

        it_behaves_like 'returns a cloned query'
      end
    end
  end

  # Non-hoisting means the operator is always present, for example
  # Foo.or(a: 1) produces {'$or' => [{'a' => 1}]}.
  shared_examples_for 'a non-hoisting logical operation' do

    context 'when there is a single predicate' do
      let(:query) do
        Mongoid::Query.new.send(tested_method, hello: 'world')
      end

      it 'adds the predicate' do
        expect(query.selector).to eq(expected_operator => [{'hello' => 'world'}])
      end
    end

    context 'when the single predicate is wrapped in an array' do
      let(:query) do
        Mongoid::Query.new.send(tested_method, [{hello: 'world'}])
      end

      it 'adds the predicate' do
        expect(query.selector).to eq(expected_operator => [{'hello' => 'world'}])
      end
    end

    context 'when argument is a Criteria' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other) do
        Mongoid::Query.new.where(foo: 'bar')
      end

      let(:result) { query.send(tested_method, other) }

      it 'combines' do
        # This is used for $or / $nor, the two conditions should remain
        # as separate hashes
        expect(result.selector).to eq(expected_operator => [{'hello' => 'world'}, {'foo' => 'bar'}])
      end
    end

    context 'when argument is a mix of Criteria and hashes' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other1) do
        Mongoid::Query.new.where(foo: 'bar')
      end

      let(:other2) do
        {bar: 42}
      end

      let(:other3) do
        Mongoid::Query.new.where(a: 2)
      end

      let(:result) { query.send(tested_method, other1, other2, other3) }

      it 'combines' do
        expect(result.selector).to eq(expected_operator => [
          {'hello' => 'world'},
          {'foo' => 'bar'},
          {'bar' => 42},
          {'a' => 2},
        ])
      end
    end
  end

  describe "#and" do

    let(:tested_method) { :and }
    let(:expected_operator) { '$and' }

    it_behaves_like 'a hoisting logical operation'

    context "when provided no criterion" do

      let(:selection) do
        query.and
      end

      it "does not add any criterion" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end

      it_behaves_like 'returns a cloned query'
    end

    context "when provided nil" do

      let(:selection) do
        query.and(nil)
      end

      it "does not add any criterion" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end

      it_behaves_like 'returns a cloned query'
    end

    context "when provided multiple criteria" do

      context "when the criterion is already included" do

        context 'simple criterion' do
          let(:selection) do
            query.and({ first: [ 1, 2 ] }).and({ first: [ 1, 2 ] })
          end

          it "adds all conditions" do
            expect(selection.selector).to eq({
              'first' => [1, 2],
              "$and" => [
                { "first" => [ 1, 2 ] }
              ]
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        context 'Key criterion' do
          let(:selection) do
            query.and({ first: [ 1, 2 ] }).and(:first.gt => 3)
          end

          it "adds all conditions" do
            expect(selection.selector).to eq({
              'first' => [1, 2],
              "$and" => [
                { "first" => {'$gt' => 3} }
              ]
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        context 'Key criterion when existing criterion is an operator' do
          let(:selection) do
            query.and(:first.lt => 5).and(:first.gt => 3)
          end

          it "adds all conditions" do
            expect(selection.selector).to eq({
              'first' => {'$lt' => 5, '$gt' => 3},
            })
          end

          it_behaves_like 'returns a cloned query'
        end
      end

      context "when the new criteria are for different fields" do

        let(:selection) do
          query.and({ first: [ 1, 2 ] }, { second: [ 3, 4 ] })
        end

        it "adds all conditions to top level" do
          expect(selection.selector).to eq({
            "first" => [ 1, 2 ],
            "second" => [ 3, 4 ],
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      context "when the new criteria are for the same field" do

        context 'when criteria are simple' do
          let(:selection) do
            query.and({ first: [ 1, 2 ] }, { first: [ 3, 4 ] })
          end

          it "combines via $and operator" do
            expect(selection.selector).to eq({
              "first" => [ 1, 2 ],
              "$and" => [
                { "first" => [ 3, 4 ] }
              ]
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        context 'when criteria use operators' do
          shared_examples 'behave correctly' do
            let(:selection) do
              query.and(
                { field: {first_operator => [ 1, 2 ] }},
                { field: {second_operator => [ 3, 4 ] }},
              )
            end

            it "combines via $and operator and stringifies all keys" do
              expect(selection.selector).to eq({
                "field" => {'$in' => [ 1, 2 ]},
                "$and" => [
                  { "field" => {'$in' => [ 3, 4 ] }}
                ]
              })
            end
          end

          [
            ['$in', '$in'],
            [:$in, '$in'],
            ['$in', :$in],
            [:$in, :$in],
          ].each do |first_operator, second_operator|
            context "when first operator is #{first_operator.inspect} and second operator is #{second_operator.inspect}" do
              let(:first_operator) { first_operator }
              let(:second_operator) { second_operator }

              include_examples 'behave correctly'
            end
          end
        end

        context 'when criteria are handled via Key' do
          shared_examples_for 'adds the conditions to top level' do

            it "adds the conditions to top level" do
              expect(selection.selector).to eq({
                "field" => {'$gt' => 3, '$lt' => 5},
              })
            end

            it_behaves_like 'returns a cloned query'
          end

          context 'criteria are provided in the same hash' do
            let(:selection) do
              query.send(tested_method, :field.gt => 3, :field.lt => 5)
            end

            it_behaves_like 'adds the conditions to top level'
          end

          context 'criteria are provided in separate hashes' do
            let(:selection) do
              query.send(tested_method, {:field.gt => 3}, {:field.lt => 5})
            end

            it_behaves_like 'adds the conditions to top level'
          end

          context 'when the criterion is wrapped in an array' do
            let(:selection) do
              query.send(tested_method, [:field.gt => 3], [:field.lt => 5])
            end

            it_behaves_like 'adds the conditions to top level'
          end
        end

        context 'when criteria are simple and handled via Key' do
          shared_examples_for 'combines conditions with $and' do

            it "combines conditions with $and" do
              expect(selection.selector).to eq({
                "field" => 3,
                '$and' => ['field' => {'$lt' => 5}],
              })
            end

            it_behaves_like 'returns a cloned query'
          end

          shared_examples_for 'combines conditions with $eq' do

            it "combines conditions with $eq" do
              expect(selection.selector).to eq({
                "field" => {'$eq' => 3, '$lt' => 5},
              })
            end

            it_behaves_like 'returns a cloned query'
          end

          shared_examples_for 'combines conditions with $regex' do

            it "combines conditions with $regex" do
              expect(selection.selector).to eq({
                "field" => {'$regex' => /t/, '$lt' => 5},
              })
            end

            it_behaves_like 'returns a cloned query'
          end

          context 'criteria are provided in the same hash' do
            context 'non-regexp argument' do
              let(:selection) do
                query.send(tested_method, :field => 3, :field.lt => 5)
              end

              it_behaves_like 'combines conditions with $eq'
            end

            context 'regexp argument' do
              let(:selection) do
                query.send(tested_method, :field => /t/, :field.lt => 5)
              end

              it_behaves_like 'combines conditions with $regex'
            end
          end

          context 'criteria are provided in separate hashes' do
            let(:selection) do
              query.send(tested_method, {:field => 3}, {:field.lt => 5})
            end

            it_behaves_like 'combines conditions with $and'
          end

          context 'when the criterion is wrapped in an array' do
            let(:selection) do
              query.send(tested_method, [:field => 3], [:field.lt => 5])
            end

            it_behaves_like 'combines conditions with $and'
          end
        end

        context 'when criteria are handled via Key and simple' do
          shared_examples_for 'combines conditions with $and' do

            it "combines conditions with $and" do
              expect(selection.selector).to eq({
                "field" => {'$gt' => 3},
                '$and' => ['field' => 5],
              })
            end

            it_behaves_like 'returns a cloned query'
          end

          shared_examples_for 'combines conditions with $eq' do

            it "combines conditions with $eq" do
              expect(selection.selector).to eq({
                "field" => {'$gt' => 3, '$eq' => 5},
              })
            end

            it_behaves_like 'returns a cloned query'
          end

          shared_examples_for 'combines conditions with $regex' do

            it "combines conditions with $regex" do
              expect(selection.selector).to eq({
                "field" => {'$gt' => 3, '$regex' => /t/},
              })
            end

            it_behaves_like 'returns a cloned query'
          end

          context 'criteria are provided in the same hash' do
            context 'non-regexp argument' do
              let(:selection) do
                query.send(tested_method, :field.gt => 3, :field => 5)
              end

              it_behaves_like 'combines conditions with $eq'
            end

            context 'regexp argument' do
              let(:selection) do
                query.send(tested_method, :field.gt => 3, :field => /t/)
              end

              it_behaves_like 'combines conditions with $regex'
            end
          end

          context 'criteria are provided in separate hashes' do
            let(:selection) do
              query.send(tested_method, {:field.gt => 3}, {:field => 5})
            end

            it_behaves_like 'combines conditions with $and'
          end

          context 'when the criterion is wrapped in an array' do
            let(:selection) do
              query.send(tested_method, [:field.gt => 3], [:field => 5])
            end

            it_behaves_like 'combines conditions with $and'
          end
        end
      end
    end

    context 'when argument is a Criteria' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:result) { query.and(other) }

      context 'different fields' do

        let(:other) do
          Mongoid::Query.new.where(foo: 'bar')
        end

        it 'combines both fields at top level' do
          expect(result.selector).to eq('hello' => 'world', 'foo' => 'bar')
        end
      end

      context 'same field' do

        let(:other) do
          Mongoid::Query.new.where(hello: /bar/)
        end

        it 'combines fields with $and' do
          expect(result.selector).to eq('hello' => 'world', '$and' => [{'hello' => /bar/}])
        end
      end
    end

    context 'when argument is a mix of Criteria and hashes' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other1) do
        Mongoid::Query.new.where(foo: 'bar')
      end

      let(:other2) do
        {bar: 42}
      end

      let(:other3) do
        Mongoid::Query.new.where(a: 2)
      end

      let(:result) { query.and(other1, other2, other3) }

      it 'combines' do
        expect(result.selector).to eq('hello' => 'world',
          'foo' => 'bar',
          'bar' => 42,
          'a' => 2,
        )
      end
    end

    context 'when Key instances are used and types involved have serializers' do
      let(:time) { Time.now }

      let(:query) do
        Band.all.and(:created_at.gt => time)
      end

      let(:expected) do
        {'created_at' => {'$gt' => time.utc}}
      end

      it 'combines and evolves' do
        expect(query.selector).to eq(expected)
      end
    end

    describe 'query shape' do
      shared_examples_for 'adds most recent criterion as $and' do
        let(:selector) { scope.selector }

        it 'adds most recent criterion as $and' do
          expect(selector).to eq('foo' => 1, '$and' => [{'foo' => 2}])
        end
      end

      context 'and/and' do
        let(:scope) do
          Band.and(foo: 1).and(foo: 2)
        end

        it_behaves_like 'adds most recent criterion as $and'
      end

      context 'and/and' do
        let(:scope) do
          Band.and(foo: 1).and(foo: 2)
        end

        it_behaves_like 'adds most recent criterion as $and'
      end

      context 'and/where' do
        let(:scope) do
          Band.and(foo: 1).where(foo: 2)
        end

        it_behaves_like 'adds most recent criterion as $and'
      end

      context 'where/and' do
        let(:scope) do
          Band.where(foo: 1).and(foo: 2)
        end

        it_behaves_like 'adds most recent criterion as $and'
      end

      context 'where/where' do
        let(:scope) do
          Band.where(foo: 1).where(foo: 2)
        end

        it_behaves_like 'adds most recent criterion as $and'
      end
    end

    context 'when conditions already exist in criteria' do
      let(:base_selection) do
        query.where(foo: 'bar')
      end

      context 'when hash conditions are given' do
        let(:selection) do
          base_selection.and(hello: 'world')
        end

        it 'adds new conditions to top level' do
          selection.selector.should == {
            'foo' => 'bar',
            'hello' => 'world',
          }
        end
      end

      context 'when criteria conditions are given' do
        let(:selection) do
          base_selection.and(query.where(hello: 'world'))
        end

        it 'adds new conditions to top level' do
          selection.selector.should == {
            'foo' => 'bar',
            'hello' => 'world',
          }
        end
      end

      context 'when complex criteria conditions are given' do
        let(:selection) do
          base_selection.and(query.or([one: 'one'], [two: 'two']))
        end

        it 'adds new conditions to top level' do
          selection.selector.should == {
            'foo' => 'bar',
            '$or' => [
              {'one' => 'one'},
              {'two' => 'two'},
            ],
          }
        end
      end
    end
  end

  shared_examples '$or/$nor' do

    it_behaves_like 'a non-hoisting logical operation'

    context "when provided no arguments" do

      let(:selection) do
        query.send(tested_method)
      end

      it_behaves_like 'returns a cloned query'

      it "does not add any criteria" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end
    end

    context "when provided nil" do

      let(:selection) do
        query.send(tested_method, nil)
      end

      it_behaves_like 'returns a cloned query'

      it "does not add any criteria" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end
    end

    context "when provided a single criterion" do

      let(:selection) do
        query.send(tested_method, field: [ 1, 2 ])
      end

      it_behaves_like 'returns a cloned query'

      it "adds the $or/$nor selector" do
        expect(selection.selector).to eq({
          expected_operator => [{ "field" => [ 1, 2 ] }]
        })
      end

      context 'when the criterion is wrapped in array' do

        let(:selection) do
          query.send(tested_method, [{ field: [ 1, 2 ] }])
        end

        it_behaves_like 'returns a cloned query'

        it "adds the $or/$nor selector" do
          expect(selection.selector).to eq({
            expected_operator => [{ "field" => [ 1, 2 ] }]
          })
        end

        context 'when the array has nil as one of the elements' do

          let(:selection) do
            query.send(tested_method, [{ field: [ 1, 2 ] }, nil])
          end

          it_behaves_like 'returns a cloned query'

          it "adds the $or/$nor selector ignoring the nil element" do
            expect(selection.selector).to eq({
              expected_operator => [{ "field" => [ 1, 2 ] }]
            })
          end
        end
      end

      context 'when query already has a condition on another field' do

        let(:selection) do
          query.where(foo: 'bar').send(tested_method, field: [ 1, 2 ])
        end

        it 'moves original conditions under $or/$nor' do
          expect(selection.selector).to eq({
            expected_operator => [{'foo' => 'bar'}, { "field" => [ 1, 2 ] }]
          })
        end
      end

      context 'when query already has an $or/$nor condition and another condition' do

        let(:selection) do
          query.send(tested_method, field: [ 1, 2 ]).where(foo: 'bar').send(tested_method, test: 1)
        end

        it 'unions existing conditions' do
          expect(selection.selector).to eq(
            expected_operator => [
              {
                expected_operator => [{ "field" => [ 1, 2 ] }],
                'foo' => 'bar',
              },
              {'test' => 1},
            ]
          )
        end
      end
    end

    context "when provided multiple criteria" do

      context "when the criteria are for different fields" do

        let(:selection) do
          query.send(tested_method, { first: [ 1, 2 ] }, { second: [ 3, 4 ] })
        end

        it_behaves_like 'returns a cloned query'

        it "adds the $or/$nor selector" do
          expect(selection.selector).to eq({
            expected_operator => [
              { "first" => [ 1, 2 ] },
              { "second" => [ 3, 4 ] }
            ]
          })
        end
      end

      context "when the criteria uses a Key instance" do

        let(:selection) do
          query.send(tested_method, { first: [ 1, 2 ] }, { :second.gt => 3 })
        end

        it "adds the $or/$nor selector" do
          expect(selection.selector).to eq({
            expected_operator => [
              { "first" => [ 1, 2 ] },
              { "second" => { "$gt" => 3 }}
            ]
          })
        end

        it_behaves_like 'returns a cloned query'

        context 'when the criterion is a time' do
          let(:selection) do
            query.send(tested_method, :field.gte => Time.new(2020, 1, 1))
          end

          it 'adds the conditions' do
            expect(selection.selector).to eq(expected_operator => [
              "field" => {'$gte' => Time.new(2020, 1, 1)},
            ])
          end

          it 'keeps the type' do
            selection.selector[expected_operator].first['field']['$gte'].should be_a(Time)
          end
        end

        context 'when the criterion is a datetime' do
          let(:selection) do
            query.send(tested_method, :field.gte => DateTime.new(2020, 1, 1))
          end

          it 'adds the conditions' do
            expect(selection.selector).to eq(expected_operator => [
              "field" => {'$gte' => Time.utc(2020, 1, 1)},
            ])
          end

          it 'converts argument to a time' do
            selection.selector[expected_operator].first['field']['$gte'].should be_a(Time)
          end
        end

        context 'when the criterion is a date' do
          let(:selection) do
            query.send(tested_method, :field.gte => Date.new(2020, 1, 1))
          end

          it 'adds the conditions' do
            expect(selection.selector).to eq(expected_operator => [
              "field" => {'$gte' => Time.utc(2020, 1, 1)},
            ])
          end

          it 'converts argument to a time' do
            selection.selector[expected_operator].first['field']['$gte'].should be_a(Time)
          end
        end
      end

      context "when a criterion has an aliased field" do

        let(:selection) do
          query.send(tested_method, { id: 1 })
        end

        it "adds the $or/$nor selector and aliases the field" do
          expect(selection.selector).to eq({
            expected_operator => [ { "_id" => 1 } ]
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      context "when a criterion is wrapped in an array" do

        let(:selection) do
          query.send(tested_method, [{ first: [ 1, 2 ] }, { :second.gt => 3 }])
        end

        it_behaves_like 'returns a cloned query'

        it "adds the $or/$nor selector" do
          expect(selection.selector).to eq({
            expected_operator => [
              { "first" => [ 1, 2 ] },
              { "second" => { "$gt" => 3 }}
            ]
          })
        end
      end

      context "when the criteria are on the same field" do

        context 'simple criteria' do
          let(:selection) do
            query.send(tested_method, { first: [ 1, 2 ] }, { first: [ 3, 4 ] })
          end

          it_behaves_like 'returns a cloned query'

          it "appends both $or/$nor expressions" do
            expect(selection.selector).to eq({
              expected_operator => [
                { "first" => [ 1, 2 ] },
                { "first" => [ 3, 4 ] }
              ]
            })
          end
        end

        context 'Key criteria as one argument' do
          let(:selection) do
            query.send(tested_method, :first.gt => 3, :first.lt => 5)
          end

          it_behaves_like 'returns a cloned query'

          it "adds all criteria" do
            expect(selection.selector).to eq({
              expected_operator => [
                { "first" => {'$gt' => 3, '$lt' => 5} },
              ]
            })
          end
        end

        context 'Key criteria as multiple arguments' do
          let(:selection) do
            query.send(tested_method, {:first.gt => 3}, {:first.lt => 5})
          end

          it_behaves_like 'returns a cloned query'

          it "adds all criteria" do
            expect(selection.selector).to eq({
              expected_operator => [
                { "first" => {'$gt' => 3} },
                { "first" => {'$lt' => 5} },
              ]
            })
          end
        end
      end
    end

    context "when chaining the criterion" do

      context "when the criterion are for different fields" do

        let(:selection) do
          query.send(tested_method, first: [ 1, 2 ]).send(tested_method, second: [ 3, 4 ])
        end

        it_behaves_like 'returns a cloned query'

        it "adds the $or/$nor selectors" do
          expect(selection.selector).to eq({
            expected_operator => [
              { "first" => [ 1, 2 ] },
              { "second" => [ 3, 4 ] }
            ]
          })
        end
      end

      context "when the criterion are on the same field" do

        let(:selection) do
          query.send(tested_method, first: [ 1, 2 ]).send(tested_method, first: [ 3, 4 ])
        end

        it_behaves_like 'returns a cloned query'

        it "appends both $or/$nor expressions" do
          expect(selection.selector).to eq({
            expected_operator => [
              { "first" => [ 1, 2 ] },
              { "first" => [ 3, 4 ] }
            ]
          })
        end
      end
    end

    context 'when giving multiple conditions in one call on the same key with symbol operator' do

      context 'non-regexp argument' do
        let(:selection) do
          query.send(tested_method, field: 1, :field.gt => 0)
        end

        it 'combines conditions with $eq' do
          selection.selector.should == {
            expected_operator => [
              'field' => {'$eq' => 1, '$gt' => 0},
            ]
          }
        end
      end

      context 'regexp argument' do
        let(:selection) do
          query.send(tested_method, field: /t/, :field.gt => 0)
        end

        it 'combines conditions with $regex' do
          selection.selector.should == {
            expected_operator => [
              'field' => {'$regex' => /t/, '$gt' => 0},
            ]
          }
        end
      end

    end
  end

  describe "#or" do

    let(:tested_method) { :or }
    let(:expected_operator) { '$or' }

    it_behaves_like '$or/$nor'
  end

  describe "#nor" do

    let(:tested_method) { :nor }
    let(:expected_operator) { '$nor' }

    it_behaves_like '$or/$nor'
  end

  describe "#any_of" do

    let(:tested_method) { :any_of }
    let(:expected_operator) { '$or' }

    it_behaves_like 'a hoisting logical operation'

    # When multiple arguments are given to any_of, it behaves differently
    # from and.
    context 'when argument is a mix of Criteria and hashes' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other1) do
        Mongoid::Query.new.where(foo: 'bar')
      end

      let(:other2) do
        {bar: 42}
      end

      let(:other3) do
        Mongoid::Query.new.where(a: 2)
      end

      let(:result) { query.send(tested_method, other1, other2, other3) }

      it 'combines' do
        expect(result.selector).to eq(
          'hello' => 'world',
          expected_operator => [
            {'foo' => 'bar'},
            {'bar' => 42},
            {'a' => 2},
          ],
        )
      end
    end

    context "when provided no arguments" do

      let(:selection) do
        query.any_of
      end

      it_behaves_like 'returns a cloned query'

      it "does not add any criteria" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end
    end

    context "when provided nil" do

      let(:selection) do
        query.any_of(nil)
      end

      it_behaves_like 'returns a cloned query'

      it "does not add any criteria" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end
    end

    context "when provided a single criterion" do

      let(:selection) do
        query.any_of(field: [ 1, 2 ])
      end

      it_behaves_like 'returns a cloned query'

      it "adds the $or selector" do
        expect(selection.selector).to eq(
          "field" => [ 1, 2 ],
        )
      end

      context 'when the criterion is wrapped in array' do

        let(:selection) do
          query.any_of([{ field: [ 1, 2 ] }])
        end

        it_behaves_like 'returns a cloned query'

        it "adds the condition" do
          expect(selection.selector).to eq(
            "field" => [ 1, 2 ],
          )
        end

        context 'when the array has nil as one of the elements' do

          let(:selection) do
            query.any_of([{ field: [ 1, 2 ] }, nil])
          end

          it_behaves_like 'returns a cloned query'

          it "adds the $or selector ignoring the nil element" do
            expect(selection.selector).to eq(
              "field" => [ 1, 2 ],
            )
          end
        end
      end

      context 'when query already has a condition on another field' do

        context 'when there is one argument' do

          let(:selection) do
            query.where(foo: 'bar').any_of(field: [ 1, 2 ])
          end

          it 'adds the new condition' do
            expect(selection.selector).to eq(
              'foo' => 'bar',
              'field' => [1, 2],
            )
          end
        end

        context 'when there are multiple arguments' do

          let(:selection) do
            query.where(foo: 'bar').any_of({field: [ 1, 2 ]}, {hello: 'world'})
          end

          it 'adds the new condition' do
            expect(selection.selector).to eq(
              'foo' => 'bar',
              '$or' => [
                {'field' => [1, 2]},
                {'hello' => 'world'},
              ],
            )
          end
        end
      end

      context 'when query already has an $or condition and another condition' do

        let(:selection) do
          query.or(field: [ 1, 2 ]).where(foo: 'bar').any_of(test: 1)
        end

        it 'adds the new condition' do
          expect(selection.selector).to eq(
            '$or' => [{'field' => [1, 2]}],
            'foo' => 'bar',
            'test' => 1,
          )
        end
      end

      context 'when any_of has multiple arguments' do

        let(:selection) do
          query.or(field: [ 1, 2 ]).where(foo: 'bar').any_of({a: 1}, {b: 2})
        end

        it 'adds the new condition to top level' do
          expect(selection.selector).to eq(
            '$or' => [{'field' => [1, 2]}],
            'foo' => 'bar',
            '$and' => [{'$or' => [{'a' => 1}, {'b' => 2}]}],
          )
        end

        context 'when query already has a top-level $and' do
          let(:selection) do
            query.or(field: [ 1, 2 ]).where('$and' => [foo: 'bar']).any_of({a: 1}, {b: 2})
          end

          it 'adds the new condition to top level $and' do
            expect(selection.selector).to eq(
              '$or' => [{'field' => [1, 2]}],
              '$and' => [{'foo' => 'bar'}, {'$or' => [{'a' => 1}, {'b' => 2}]}],
            )
          end
        end
      end
    end

    context "when provided multiple criteria" do

      context "when the criteria are for different fields" do

        let(:selection) do
          query.any_of({ first: [ 1, 2 ] }, { second: [ 3, 4 ] })
        end

        it_behaves_like 'returns a cloned query'

        it "adds the $or selector" do
          expect(selection.selector).to eq({
            "$or" => [
              { "first" => [ 1, 2 ] },
              { "second" => [ 3, 4 ] }
            ]
          })
        end
      end

      context "when the criteria uses a Key instance" do

        let(:selection) do
          query.any_of({ first: [ 1, 2 ] }, { :second.gt => 3 })
        end

        it "adds the $or selector" do
          expect(selection.selector).to eq({
            "$or" => [
              { "first" => [ 1, 2 ] },
              { "second" => { "$gt" => 3 }}
            ]
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      context 'when criteria are simple and handled via Key' do
        shared_examples_for 'adds conditions with $or' do

          it "adds conditions with $or" do
            expect(selection.selector).to eq({
              '$or' => [
                {'field' => 3},
                {'field' => {'$lt' => 5}},
              ],
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $eq' do

          it "combines conditions with $eq" do
            expect(selection.selector).to eq({
              'field' => {
                '$eq' => 3,
                '$lt' => 5,
              },
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $regex' do

          it "combines conditions with $regex" do
            expect(selection.selector).to eq({
              'field' => {
                '$regex' => /t/,
                '$lt' => 5,
              },
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        context 'criteria are provided in the same hash' do
          context 'non-regexp argument' do
            let(:selection) do
              query.send(tested_method, :field => 3, :field.lt => 5)
            end

            it_behaves_like 'combines conditions with $eq'
          end

          context 'regexp argument' do
            let(:selection) do
              query.send(tested_method, :field => /t/, :field.lt => 5)
            end

            it_behaves_like 'combines conditions with $regex'
          end
        end

        context 'criteria are provided in separate hashes' do
          let(:selection) do
            query.send(tested_method, {:field => 3}, {:field.lt => 5})
          end

          it_behaves_like 'adds conditions with $or'
        end

        context 'when the criterion is wrapped in an array' do
          let(:selection) do
            query.send(tested_method, [:field => 3], [:field.lt => 5])
          end

          it_behaves_like 'adds conditions with $or'
        end
      end

      context 'when criteria are handled via Key and simple' do
        shared_examples_for 'adds conditions with $or' do

          it "adds conditions with $or" do
            expect(selection.selector).to eq({
              '$or' => [
                {'field' => {'$gt' => 3}},
                {'field' => 5},
              ],
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $eq' do

          it "combines conditions with $eq" do
            expect(selection.selector).to eq(
              'field' => {'$gt' => 3, '$eq' => 5},
            )
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $regex' do

          it "combines conditions with $regex" do
            expect(selection.selector).to eq(
              'field' => {'$gt' => 3, '$regex' => /t/},
            )
          end

          it_behaves_like 'returns a cloned query'
        end

        context 'criteria are provided in the same hash' do
          context 'non-regexp argument' do
            let(:selection) do
              query.send(tested_method, :field.gt => 3, :field => 5)
            end

            it_behaves_like 'combines conditions with $eq'
          end

          context 'regexp argument' do
            let(:selection) do
              query.send(tested_method, :field.gt => 3, :field => /t/)
            end

            it_behaves_like 'combines conditions with $regex'
          end
        end

        context 'criteria are provided in separate hashes' do
          let(:selection) do
            query.send(tested_method, {:field.gt => 3}, {:field => 5})
          end

          it_behaves_like 'adds conditions with $or'
        end

        context 'when the criterion is wrapped in an array' do
          let(:selection) do
            query.send(tested_method, [:field.gt => 3], [:field => 5])
          end

          it_behaves_like 'adds conditions with $or'
        end
      end

      context "when a criterion has an aliased field" do

        let(:selection) do
          query.any_of({ id: 1 })
        end

        it "adds the $or selector and aliases the field" do
          expect(selection.selector).to eq(
            "_id" => 1,
          )
        end

        it_behaves_like 'returns a cloned query'
      end

      context "when a criterion is wrapped in an array" do

        let(:selection) do
          query.any_of([{ first: [ 1, 2 ] }, { :second.gt => 3 }])
        end

        it_behaves_like 'returns a cloned query'

        it "adds the $or selector" do
          expect(selection.selector).to eq({
            "$or" => [
              { "first" => [ 1, 2 ] },
              { "second" => { "$gt" => 3 }}
            ]
          })
        end
      end

      context "when the criteria are on the same field" do

        let(:selection) do
          query.any_of({ first: [ 1, 2 ] }, { first: [ 3, 4 ] })
        end

        it_behaves_like 'returns a cloned query'

        it "appends both $or expressions" do
          expect(selection.selector).to eq({
            "$or" => [
              { "first" => [ 1, 2 ] },
              { "first" => [ 3, 4 ] }
            ]
          })
        end
      end
    end

    context "when chaining the criteria" do

      context "when the criteria are for different fields" do

        let(:selection) do
          query.any_of(first: [ 1, 2 ]).any_of(second: [ 3, 4 ])
        end

        it_behaves_like 'returns a cloned query'

        it "adds the conditions separately" do
          expect(selection.selector).to eq(
            "first" => [ 1, 2 ],
            "second" => [ 3, 4 ],
          )
        end
      end

      context "when the criteria are on the same field" do

        let(:selection) do
          query.any_of(first: [ 1, 2 ]).any_of(first: [ 3, 4 ])
        end

        it_behaves_like 'returns a cloned query'

        it "adds the conditions separately" do
          expect(selection.selector).to eq(
            "first" => [ 1, 2 ],
            '$and' => [{"first" => [ 3, 4 ]}],
          )
        end
      end
    end

    context 'when using multiple criteria and symbol operators' do
      context 'when using fields that meaningfully evolve values' do

        let(:query) do
          Dictionary.any_of({a: 1}, :published.gt => Date.new(2020, 2, 3))
        end

        it 'generates the expected query' do
          query.selector.should == {'$or' => [
            {'a' => 1},
            # Date instance is converted to a Time instance in local time,
            # because we are querying on a Time field and dates are interpreted
            # in local time when assigning to Time fields
            {'published' => {'$gt' => Time.zone.local(2020, 2, 3)}},
          ]}
        end
      end

      context 'when using fields that do not meaningfully evolve values' do

        let(:query) do
          Dictionary.any_of({a: 1}, :submitted_on.gt => Date.new(2020, 2, 3))
        end

        it 'generates the expected query' do
          query.selector.should == {'$or' => [
            {'a' => 1},
            # Date instance is converted to a Time instance in UTC,
            # because we are querying on a Date field and dates are interpreted
            # in UTC when persisted as dates by Mongoid
            {'submitted_on' => {'$gt' => Time.utc(2020, 2, 3)}},
          ]}
        end
      end
    end
  end

  describe "#not" do

    context "when provided no criterion" do

      let(:selection) do
        query.not
      end

      it "does not add any criterion" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end

      it "returns a cloned query" do
        expect(selection).not_to equal(query)
      end

      it 'does not mutate receiver' do
        expect(query.negating).to be nil

        selection
        expect(query.negating).to be nil
      end

      shared_examples_for 'negates the next condition' do
        let(:selection) do
          query.not.send(query_method, field: [ 1, 2 ])
        end

        it "negates the next condition" do
          expect(selection.selector).to eq(
            { "field" => { "$not" => { operator => [ 1, 2 ] }}}
          )
        end

        it_behaves_like 'returns a cloned query'

        it "removes the negation on the clone" do
          expect(selection).to_not be_negating
        end
      end

      context "when the next condition is #all" do
        let(:query_method) { :all }
        let(:operator) { '$all' }

        it_behaves_like 'negates the next condition'
      end

      context "when the next condition is #in" do
        let(:query_method) { :in }
        let(:operator) { '$in' }

        it_behaves_like 'negates the next condition'
      end

      context "when the next condition is #nin" do
        let(:query_method) { :nin }
        let(:operator) { '$nin' }

        it_behaves_like 'negates the next condition'
      end

      context "when the following criteria is a gt method" do

        let(:selection) do
          query.not.gt(age: 50)
        end

        it "negates the gt selection" do
          expect(selection.selector).to eq(
            { "age" => { "$not" => { "$gt" => 50 }}}
          )
        end

        it_behaves_like 'returns a cloned query'

        it "removes the negation on the clone" do
          expect(selection).to_not be_negating
        end
      end

      context "when the criteria uses Key" do

        let(:selection) do
          query.not(:age.gt => 50)
        end

        it "negates the gt selection" do
          expect(selection.selector).to eq(
            '$and' => ['$nor' => ['age' => {'$gt' => 50}]]
          )
        end

        it_behaves_like 'returns a cloned query'

        it "removes the negation on the clone" do
          expect(selection).to_not be_negating
        end
      end

      context "when the following criteria is a where" do

        let(:selection) do
          query.not.where(field: 1, :other.in => [ 1, 2 ])
        end

        it "negates the selection with an operator" do
          expect(selection.selector).to eq(
            { "field" => { "$ne" => 1 }, "other" => { "$not" => { "$in" => [ 1, 2 ] }}}
          )
        end

        it_behaves_like 'returns a cloned query'

        it "removes the negation on the clone" do
          expect(selection).to_not be_negating
        end
      end

      context "when the following criteria is a where with a regexp" do

        let(:selection) do
          query.not.where(field: 1, other: /test/)
        end

        it "negates the selection with an operator" do
          expect(selection.selector).to eq(
            { "field" => { "$ne" => 1 }, "other" => { "$not" => /test/ } }
          )
        end

        it_behaves_like 'returns a cloned query'

        it "removes the negation on the clone" do
          expect(selection).to_not be_negating
        end
      end

      context 'when the following criteria uses string were form' do

        let(:selection) do
          query.not.where('hello world')
        end

        it "negates the selection with an operator" do
          expect(selection.selector).to eq(
            '$and' => [{'$nor' => [{'$where' => 'hello world'}]}]
          )
        end

        it_behaves_like 'returns a cloned query'

        it "removes the negation on the clone" do
          expect(selection).to_not be_negating
        end

      end
    end

    context "when provided nil" do

      let(:selection) do
        query.not(nil)
      end

      it "does not add any criterion" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end

      it_behaves_like 'returns a cloned query'
    end

    context "when provided a single criterion" do

      let(:selection) do
        query.not(field: /test/)
      end

      it "adds the $not selector" do
        expect(selection.selector).to eq({
          "field" => { "$not" => /test/ }
        })
      end

      it_behaves_like 'returns a cloned query'
    end

    context "when negating a field in the original selector" do
      let(:query) do
        Mongoid::Query.new("id" => "_id").where(field: 'foo')
      end

      let(:selection) do
        query.not(field: 'bar')
      end

      it "combines the conditions" do
        expect(selection.selector).to eq({
          "field" => 'foo',
          '$and' => [{'$nor' => [{ "field" => 'bar' }]}],
        })
      end

      it_behaves_like 'returns a cloned query'
    end

    context "when provided multiple criteria" do

      context "when the criteria are for different fields" do

        let(:selection) do
          query.not(first: /1/, second: /2/)
        end

        it "adds the $not selectors" do
          expect(selection.selector).to eq({
            "first" => { "$not" => /1/ },
            "second" => { "$not" => /2/ }
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      context "when the criteria are given in separate arguments" do

        let(:selection) do
          query.not({first: /1/}, {second: /2/})
        end

        it "adds the $not selectors" do
          expect(selection.selector).to eq({
            "first" => { "$not" => /1/ },
            "second" => { "$not" => /2/ }
          })
        end

        it_behaves_like 'returns a cloned query'
      end
    end

    context "when chaining the criterion" do

      context "when the criterion are for different fields" do

        let(:selection) do
          query.not(first: /1/).not(second: /2/)
        end

        it "adds the $not selectors" do
          expect(selection.selector).to eq({
            "first" => { "$not" => /1/ },
            "second" => { "$not" => /2/ }
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      context "when the criterion are on the same field" do

        let(:selection) do
          query.not(first: /1/).not(first: /2/)
        end

        it "combines conditions" do
          expect(selection.selector).to eq(
            "first" =>  { "$not" => /1/ },
            '$and' => [{'$nor' => [{'first' => /2/}]}],
          )
        end

        it_behaves_like 'returns a cloned query'
      end

      context "when the criterion are a double negative" do

        let(:selection) do
          query.not.where(:first.not => /1/)
        end

        it "does not double the $not selector" do
          expect(selection.selector).to eq({
            "first" =>  { "$not" => /1/ }
          })
        end

        it_behaves_like 'returns a cloned query'
      end
    end

    context 'when argument is a Criteria' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other) do
        Mongoid::Query.new.where(foo: 'bar')
      end

      let(:result) { query.not(other) }

      it 'combines' do
        expect(result.selector).to eq('hello' => 'world', 'foo' => {'$ne' => 'bar'})
      end
    end

    context 'when argument is a simple Criteria with multiple fields' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other) do
        Mongoid::Query.new.where(a: 1, b: 2)
      end

      let(:result) { query.not(other) }

      it 'combines fields into top level criteria' do
        expect(result.selector).to eq('hello' => 'world',
          'a' => {'$ne' => 1}, 'b' => {'$ne' => 2})
      end
    end

    context 'when argument is a complex Criteria' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other) do
        Mongoid::Query.new.where('$nor' => [{a: 1, b: 2}])
      end

      let(:result) { query.not(other) }

      it 'combines with $and of $nor' do
        expect(result.selector).to eq('hello' => 'world', '$and' => [{'$nor' => [{
          '$nor' => [{'a' => 1, 'b' => 2}]}]}])
      end
    end

    context 'when argument is a mix of Criteria and hashes' do
      let(:query) do
        Mongoid::Query.new.where(hello: 'world')
      end

      let(:other1) do
        Mongoid::Query.new.where(foo: 'bar')
      end

      let(:other2) do
        {bar: 42}
      end

      let(:other3) do
        Mongoid::Query.new.where(a: 2)
      end

      let(:result) { query.not(other1, other2, other3) }

      it 'combines' do
        expect(result.selector).to eq('hello' => 'world',
          'foo' => {'$ne' => 'bar'},
          'bar' => {'$ne' => 42},
          'a' => {'$ne' => 2},
        )
      end
    end

    context 'when giving multiple conditions in one call on the same key with symbol operator' do

      context 'non-regexp argument' do
        let(:selection) do
          query.not(field: 1, :field.gt => 0)
        end

        it 'combines conditions with $eq' do
          selection.selector.should == {
            '$and' => ['$nor' => [
              'field' => {'$eq' => 1, '$gt' => 0},
            ]]
          }
        end
      end

      context 'regexp argument' do
        let(:selection) do
          query.not(field: /t/, :field.gt => 0)
        end

        it 'combines conditions with $regex' do
          selection.selector.should == {
            '$and' => ['$nor' => [
              'field' => {'$regex' => /t/, '$gt' => 0},
            ]]
          }
        end
      end
    end

    # This test confirms that MONGOID-5097 has been repaired.
    context "when using exists on a field of type Time" do
      let(:criteria) do
        Dictionary.any_of({:published.exists => true}, published: nil)
      end

      it "doesn't raise an error" do
        expect do
          criteria
        end.to_not raise_error
      end

      it "generates the correct selector" do
        expect(criteria.selector).to eq({
          "$or" => [ {
            "published" => { "$exists" => true }
          }, {
            "published" => nil
          } ] } )
      end
    end
  end

  describe "#none_of" do
    context 'when argument is a mix of Criteria and hashes' do
      let(:query) { Mongoid::Query.new.where(hello: 'world') }
      let(:other1) { Mongoid::Query.new.where(foo: 'bar') }
      let(:other2) { { bar: 42 } }
      let(:other3) { Mongoid::Query.new.where(a: 2) }

      let(:result) { query.none_of(other1, other2, other3) }

      it 'combines' do
        expect(result.selector).to eq(
          'hello' => 'world',
          '$nor' => [
            {'foo' => 'bar'},
            {'bar' => 42},
            {'a' => 2},
          ],
        )
      end
    end

    context "when provided no arguments" do
      let(:selection) { query.none_of }

      it_behaves_like 'returns a cloned query'

      it "does not add any criteria" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end
    end

    context "when provided nil" do
      let(:selection) { query.none_of(nil) }

      it_behaves_like 'returns a cloned query'

      it "does not add any criteria" do
        expect(selection.selector).to eq({})
      end

      it "returns the query" do
        expect(selection).to eq(query)
      end
    end

    context "when provided a single criterion" do
      let(:selection) { query.none_of(field: [ 1, 2 ]) }

      it_behaves_like 'returns a cloned query'

      it 'adds the $nor selector' do
        expect(selection.selector).to eq(
          '$nor' => [ { 'field' => [ 1, 2 ] } ],
        )
      end

      context 'when the criterion is wrapped in array' do
        let(:selection) { query.none_of([{ field: [ 1, 2 ] }]) }

        it_behaves_like 'returns a cloned query'

        it 'adds the condition' do
          expect(selection.selector).to eq(
            '$nor' => [ { 'field' => [ 1, 2 ] } ],
          )
        end

        context 'when the array has nil as one of the elements' do
          let(:selection) { query.none_of([{ field: [ 1, 2 ] }, nil]) }

          it_behaves_like 'returns a cloned query'

          it 'adds the $nor selector ignoring the nil element' do
            expect(selection.selector).to eq(
              '$nor' => [ { 'field' => [ 1, 2 ] } ],
            )
          end
        end
      end

      context 'when query already has a condition on another field' do
        context 'when there is one argument' do
          let(:selection) { query.where(foo: 'bar').none_of(field: [ 1, 2 ]) }

          it 'adds the new condition' do
            expect(selection.selector).to eq(
              'foo' => 'bar',
              '$nor' => [ { 'field' => [1, 2] } ],
            )
          end
        end

        context 'when there are multiple arguments' do
          let(:selection) do
            query.where(foo: 'bar').none_of({ field: [ 1, 2 ] }, { hello: 'world' })
          end

          it 'adds the new condition' do
            expect(selection.selector).to eq(
              'foo' => 'bar',
              '$nor' => [
                { 'field' => [1, 2] },
                { 'hello' => 'world' },
              ],
            )
          end
        end
      end

      context 'when query already has a $nor condition and another condition' do
        let(:selection) do
          query.nor(field: [ 1, 2 ]).where(foo: 'bar').none_of(test: 1)
        end

        it 'adds the new condition' do
          expect(selection.selector).to eq(
            '$nor' => [ { 'field' => [1, 2] } ],
            'foo' => 'bar',
            '$and' => [ { '$nor' => [ { 'test' => 1 } ] } ]
          )
        end
      end

      context 'when none_of has multiple arguments' do
        let(:selection) do
          query.nor(field: [ 1, 2 ]).where(foo: 'bar').none_of({a: 1}, {b: 2})
        end

        it 'adds the new condition to top level' do
          expect(selection.selector).to eq(
            'foo' => 'bar',
            '$nor' => [ { 'field' => [1, 2] } ],
            '$and' => [ { '$nor' => [ { 'a' => 1 }, { 'b' => 2 } ] } ]
          )
        end

        context 'when query already has a top-level $and' do
          let(:selection) do
            query.nor(field: [ 1, 2 ]).where('$and' => [foo: 'bar']).none_of({a: 1}, {b: 2})
          end

          it 'adds the new condition to top level $and' do
            expect(selection.selector).to eq(
              '$nor' => [ { 'field' => [1, 2] } ],
              '$and' => [
                { 'foo' => 'bar' },
                { '$nor' => [ { 'a' => 1 }, { 'b' => 2 } ] }
              ],
            )
          end
        end
      end
    end

    context "when provided multiple criteria" do
      context "when the criteria are for different fields" do
        let(:selection) do
          query.none_of({ first: [ 1, 2 ] }, { second: [ 3, 4 ] })
        end

        it_behaves_like 'returns a cloned query'

        it "adds the $nor selector" do
          expect(selection.selector).to eq({
            "$nor" => [
              { "first" => [ 1, 2 ] },
              { "second" => [ 3, 4 ] }
            ]
          })
        end
      end

      context "when the criteria uses a Key instance" do
        let(:selection) do
          query.none_of({ first: [ 1, 2 ] }, { :second.gt => 3 })
        end

        it "adds the $nor selector" do
          expect(selection.selector).to eq({
            "$nor" => [
              { "first" => [ 1, 2 ] },
              { "second" => { "$gt" => 3 }}
            ]
          })
        end

        it_behaves_like 'returns a cloned query'
      end

      context 'when criteria are simple and handled via Key' do
        shared_examples_for 'adds conditions with $nor' do
          it "adds conditions with $nor" do
            expect(selection.selector).to eq({
              '$nor' => [
                {'field' => 3},
                {'field' => {'$lt' => 5}},
              ],
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $eq' do
          it "combines conditions with $eq" do
            expect(selection.selector).to eq({
              '$nor' => [ { 'field' => { '$eq' => 3, '$lt' => 5 } } ]
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $regex' do
          it 'combines conditions with $regex' do
            expect(selection.selector).to eq({
              '$nor' => [ { 'field' => { '$regex' => /t/, '$lt' => 5 } } ]
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        context 'criteria are provided in the same hash' do
          context 'non-regexp argument' do
            let(:selection) { query.none_of(:field => 3, :field.lt => 5) }
            it_behaves_like 'combines conditions with $eq'
          end

          context 'regexp argument' do
            let(:selection) { query.none_of(:field => /t/, :field.lt => 5) }
            it_behaves_like 'combines conditions with $regex'
          end
        end

        context 'criteria are provided in separate hashes' do
          let(:selection) { query.none_of({:field => 3}, {:field.lt => 5}) }
          it_behaves_like 'adds conditions with $nor'
        end

        context 'when the criterion is wrapped in an array' do
          let(:selection) { query.none_of([:field => 3], [:field.lt => 5]) }
          it_behaves_like 'adds conditions with $nor'
        end
      end

      context 'when criteria are handled via Key and simple' do
        shared_examples_for 'adds conditions with $nor' do
          it 'adds conditions with $nor' do
            expect(selection.selector).to eq({
              '$nor' => [
                { 'field' => { '$gt' => 3 } },
                { 'field' => 5 },
              ],
            })
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $eq' do
          it 'combines conditions with $eq' do
            expect(selection.selector).to eq(
              '$nor' => [ { 'field' => {'$gt' => 3, '$eq' => 5} } ],
            )
          end

          it_behaves_like 'returns a cloned query'
        end

        shared_examples_for 'combines conditions with $regex' do
          it 'combines conditions with $regex' do
            expect(selection.selector).to eq(
              '$nor' => [ { 'field' => {'$gt' => 3, '$regex' => /t/} } ],
            )
          end

          it_behaves_like 'returns a cloned query'
        end

        context 'criteria are provided in the same hash' do
          context 'non-regexp argument' do
            let(:selection) { query.none_of(:field.gt => 3, :field => 5) }
            it_behaves_like 'combines conditions with $eq'
          end

          context 'regexp argument' do
            let(:selection) { query.none_of(:field.gt => 3, :field => /t/) }
            it_behaves_like 'combines conditions with $regex'
          end
        end

        context 'criteria are provided in separate hashes' do
          let(:selection) { query.none_of({:field.gt => 3}, {:field => 5}) }
          it_behaves_like 'adds conditions with $nor'
        end

        context 'when the criterion is wrapped in an array' do
          let(:selection) { query.none_of([:field.gt => 3], [:field => 5]) }
          it_behaves_like 'adds conditions with $nor'
        end
      end

      context 'when a criterion has an aliased field' do
        let(:selection) { query.none_of({ id: 1 }) }

        it 'adds the $nor selector and aliases the field' do
          expect(selection.selector).to eq('$nor' => [{ '_id' => 1 }])
        end

        it_behaves_like 'returns a cloned query'
      end

      context 'when a criterion is wrapped in an array' do
        let(:selection) do
          query.none_of([{ first: [ 1, 2 ] }, { :second.gt => 3 }])
        end

        it_behaves_like 'returns a cloned query'

        it 'adds the $ nor selector' do
          expect(selection.selector).to eq({
            '$nor' => [
              { 'first' => [ 1, 2 ] },
              { 'second' => { '$gt' => 3 }}
            ]
          })
        end
      end

      context "when the criteria are on the same field" do
        let(:selection) do
          query.none_of({ first: [ 1, 2 ] }, { first: [ 3, 4 ] })
        end

        it_behaves_like 'returns a cloned query'

        it 'appends both $nor expressions' do
          expect(selection.selector).to eq({
            "$nor" => [
              { "first" => [ 1, 2 ] },
              { "first" => [ 3, 4 ] }
            ]
          })
        end
      end
    end

    context 'when chaining the criteria' do
      context 'when the criteria are for different fields' do
        let(:selection) do
          query.none_of(first: [ 1, 2 ]).none_of(second: [ 3, 4 ])
        end

        it_behaves_like 'returns a cloned query'

        it 'adds the conditions separately' do
          expect(selection.selector).to eq(
            '$nor' => [ { 'first' => [ 1, 2 ] } ],
            '$and' => [ { '$nor' => [ { 'second' => [ 3, 4 ] } ] } ],
          )
        end
      end

      context "when the criteria are on the same field" do
        let(:selection) do
          query.none_of(first: [ 1, 2 ]).none_of(first: [ 3, 4 ])
        end

        it_behaves_like 'returns a cloned query'

        it 'adds the conditions separately' do
          expect(selection.selector).to eq(
            '$nor' => [ { 'first' => [ 1, 2 ] } ],
            '$and' => [ { '$nor' => [ { 'first' => [ 3, 4 ] } ] } ]
          )
        end
      end
    end

    context 'when using multiple criteria and symbol operators' do
      context 'when using fields that meaningfully evolve values' do
        let(:query) do
          Dictionary.none_of({a: 1}, :published.gt => Date.new(2020, 2, 3))
        end

        it 'generates the expected query' do
          query.selector.should == {'$nor' => [
            {'a' => 1},
            # Date instance is converted to a Time instance in local time,
            # because we are querying on a Time field and dates are interpreted
            # in local time when assigning to Time fields
            {'published' => {'$gt' => Time.zone.local(2020, 2, 3) } },
          ] }
        end
      end

      context 'when using fields that do not meaningfully evolve values' do
        let(:query) do
          Dictionary.none_of({a: 1}, :submitted_on.gt => Date.new(2020, 2, 3))
        end

        it 'generates the expected query' do
          query.selector.should == {'$nor' => [
            {'a' => 1},
            # Date instance is converted to a Time instance in UTC,
            # because we are querying on a Date field and dates are interpreted
            # in UTC when persisted as dates by Mongoid
            {'submitted_on' => {'$gt' => Time.utc(2020, 2, 3)}},
          ]}
        end
      end
    end
  end
end
