# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"

describe Mongoid::Contextual::Mongo do

  [ :blank?, :empty? ].each do |method|

    describe "##{method}" do

      before do
        Band.create!(name: "Depeche Mode")
      end

      context "when the count is zero" do

        let(:criteria) do
          Band.where(name: "New Order")
        end

        let(:context) do
          described_class.new(criteria)
        end

        it "returns true" do
          expect(context.send(method)).to be true
        end
      end

      context "when the count is greater than zero" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        it "returns false" do
          expect(context.send(method)).to be false
        end
      end
    end
  end

  describe "#count" do

    let!(:depeche) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.where(name: "Depeche Mode")
    end

    context "when no arguments are provided" do

      let(:context) do
        described_class.new(criteria)
      end

      it "returns the number of documents that match" do
        expect(context.count).to eq(1)
      end
    end

    context "when the query cache is enabled" do
      query_cache_enabled

      let(:context) do
        described_class.new(criteria)
      end

      it "only executes the count query once" do
        expect_query(1) do
          2.times { expect(context.count).to eq(1) }
        end
      end
    end

    context "when provided a block" do

      let(:context) do
        described_class.new(criteria)
      end

      let(:count) do
        context.count do |doc|
          doc.likes.nil?
        end
      end

      it "returns the number of documents that match" do
        expect(count).to eq(1)
      end

      context "and a limit true" do

        before do
          2.times { Band.create!(name: "Depeche Mode", likes: 1) }
        end

        let(:count) do
          context.count(true) do |doc|
            doc.likes.nil?
          end
        end

        it "returns the number of documents that match" do
          expect(count).to eq(1)
        end
      end
    end

    context "when provided limit" do

      before do
        2.times { Band.create!(name: "Depeche Mode") }
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:count) do
        context.count(limit: 2)
      end

      it "returns the number of documents that match" do
        expect(count).to eq(2)
      end
    end

    context 'when a collation is specified' do

      let(:context) do
        described_class.new(criteria)
      end

      context 'when the collation is specified on the criteria' do

        let(:criteria) do
          Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
        end

        let(:count) do
          context.count
        end

        it 'applies the collation' do
          expect(count).to eq(1)
        end
      end
    end

    context 'when for_js is present' do
      let(:context) do
        Band.for_js('this.name == "Depeche Mode"')
      end

      it 'counts the expected records' do
        expect(context.count).to eq(1)
      end
    end
  end

  describe "#estimated_count" do

    let!(:depeche) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.where
    end

    context "when not providing options" do
      it 'returns the correct count' do
        expect(criteria.estimated_count).to eq(2)
      end
    end

    context "when providing options" do
      it 'returns the correct count' do
        expect(criteria.estimated_count(maxTimeMS: 1000)).to eq(2)
      end
    end

    context "when the query cache is enabled" do
      query_cache_enabled

      let(:context) do
        described_class.new(criteria)
      end

      it "the results are not cached" do
        expect_query(2) do
          2.times do
            context.estimated_count
          end
        end
      end
    end

    context "when the criteria contains a selector", :focus do
      let(:criteria) do
        Band.where(name: "New Order")
      end

      context "when not providing options" do
        it 'raises an error' do
          expect do
            criteria.estimated_count
          end.to raise_error(Mongoid::Errors::InvalidEstimatedCountCriteria)
        end
      end

      context "when providing options" do
        it 'raises an error' do
          expect do
            criteria.estimated_count(maxTimeMS: 1000)
          end.to raise_error(Mongoid::Errors::InvalidEstimatedCountCriteria)
        end
      end
    end

    context "when including a default scope" do

      let(:criteria) do
        Band.where(name: "New Order")
      end

      before do
        5.times { Band.create! }
        Band.default_scope ->{ criteria }
      end

      after do
        Band.default_scoping = nil
      end

      it 'raises an error' do
        expect do
          Band.estimated_count
        end.to raise_error(Mongoid::Errors::InvalidEstimatedCountScoping)
      end

      it "does not raise an error on unscoped" do
        expect do
          expect(Band.unscoped.estimated_count).to eq(5)
        end
      end
    end
  end



  [ :delete, :delete_all ].each do |method|

    describe "##{method}" do

      let!(:depeche_mode) do
        Band.create!(name: "Depeche Mode")
      end

      let!(:new_order) do
        Band.create!(name: "New Order")
      end

      context "when the selector is constraining" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:deleted) do
          context.send(method)
        end

        it "deletes the matching documents" do
          expect(Band.find(new_order.id)).to eq(new_order)
        end

        it "deletes the correct number of documents" do
          expect(Band.count).to eq(1)
        end

        it "returns the number of documents deleted" do
          expect(deleted).to eq(1)
        end

        context 'when the criteria has a collation' do

          let(:criteria) do
            Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
          end

          let(:context) do
            described_class.new(criteria)
          end

          let!(:deleted) do
            context.send(method)
          end

          it "deletes the matching documents" do
            expect(Band.find(new_order.id)).to eq(new_order)
          end

          it "deletes the correct number of documents" do
            expect(Band.count).to eq(1)
          end

          it "returns the number of documents deleted" do
            expect(deleted).to eq(1)
          end
        end
      end

      context "when the selector is not constraining" do

        let(:criteria) do
          Band.all
        end

        let(:context) do
          described_class.new(criteria)
        end

        before do
          context.send(method)
        end

        it "deletes all the documents" do
          expect(Band.count).to eq(0)
        end
      end

      context 'when the write concern is unacknowledged' do

        let(:criteria) do
          Band.all
        end

        let!(:deleted) do
          criteria.with(write: { w: 0 }) do |crit|
            crit.send(method)
          end
        end

        it 'returns 0' do
          expect(deleted).to eq(0)
        end
      end
    end
  end

  [ :destroy, :destroy_all ].each do |method|

    describe "##{method}" do

      let!(:depeche_mode) do
        Band.create!(name: "Depeche Mode")
      end

      let!(:new_order) do
        Band.create!(name: "New Order")
      end

      context "when the selector is constraining" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:destroyed) do
          context.send(method)
        end

        it "destroys the matching documents" do
          expect(Band.find(new_order.id)).to eq(new_order)
        end

        it "destroys the correct number of documents" do
          expect(Band.count).to eq(1)
        end

        it "returns the number of documents destroyed" do
          expect(destroyed).to eq(1)
        end

        context 'when the criteria has a collation' do

          let(:criteria) do
            Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
          end

          let(:context) do
            described_class.new(criteria)
          end

          let!(:destroyed) do
            context.send(method)
          end

          it "destroys the matching documents" do
            expect(Band.find(new_order.id)).to eq(new_order)
          end

          it "destroys the correct number of documents" do
            expect(Band.count).to eq(1)
          end

          it "returns the number of documents destroyed" do
            expect(destroyed).to eq(1)
          end
        end
      end

      context "when the selector is not constraining" do

        let(:criteria) do
          Band.all
        end

        let(:context) do
          described_class.new(criteria)
        end

        before do
          context.send(method)
        end

        it "destroys all the documents" do
          expect(Band.count).to eq(0)
        end
      end
    end

    context 'when the write concern is unacknowledged' do

      before do
        2.times { Band.create! }
      end

      let(:criteria) do
        Band.all
      end

      let!(:deleted) do
        criteria.with(write: { w: 0 }) do |crit|
          crit.send(method)
        end
      end

      it 'returns 0' do
        expect(deleted).to eq(0)
      end
    end
  end

  describe "#distinct" do

    before do
      Band.create!(name: "Depeche Mode", years: 30, sales: "1E2")
      Band.create!(name: "New Order", years: 25, sales: "2E3")
      Band.create!(name: "10,000 Maniacs", years: 20, sales: "1E2")
    end

    context "when limiting the result set" do

      let(:criteria) do
        Band.where(name: "Depeche Mode")
      end

      let(:context) do
        described_class.new(criteria)
      end

      it "returns the distinct matching fields" do
        expect(context.distinct(:name)).to eq([ "Depeche Mode" ])
      end
    end

    context "when not limiting the result set" do

      let(:criteria) do
        Band.criteria
      end

      let(:context) do
        described_class.new(criteria)
      end

      it "returns the distinct field values" do
        expect(context.distinct(:name).sort).to eq([ "10,000 Maniacs", "Depeche Mode", "New Order" ].sort)
      end
    end

    context "when providing an aliased field" do

      let(:criteria) do
        Band.criteria
      end

      let(:context) do
        described_class.new(criteria)
      end

      it "returns the distinct field values" do
        expect(context.distinct(:years).sort).to eq([ 20, 25, 30 ])
      end
    end

    context 'when a collation is specified' do
      before do
        Band.create!(name: 'DEPECHE MODE')
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:expected_results) do
        ["10,000 Maniacs", "Depeche Mode", "New Order"]
      end

      let(:criteria) do
        Band.where({}).collation(locale: 'en_US', strength: 2)
      end

      it 'applies the collation' do
        expect(context.distinct(:name).sort).to eq(expected_results.sort)
      end
    end

    context "when providing a demongoizable field" do
      let(:criteria) do
        Band.criteria
      end

      let(:context) do
        described_class.new(criteria)
      end

      it "returns the non-demongoized distinct field values" do
        expect(context.distinct(:sales).sort).to eq([ BigDecimal("1E2"), BigDecimal("2E3") ])
      end
    end

    context "when getting a localized field" do
      with_default_i18n_configs

      before do
        I18n.locale = :en
        d = Dictionary.create!(description: 'english-text')
        I18n.locale = :de
        d.description = 'deutsch-text'
        d.save!
      end

      let(:criteria) do
        Dictionary.criteria
      end

      let(:context) do
        described_class.new(criteria)
      end

      context "when getting the field without _translations" do
        it "gets the demongoized localized field" do
          expect(context.distinct(:description)).to eq([ 'deutsch-text' ])
        end
      end

      context "when getting the field with _translations" do
        it "gets the full hash" do
          expect(context.distinct(:description_translations)).to eq([ { "de" => "deutsch-text", "en" => "english-text" } ])
        end
      end

      context 'when plucking a specific locale' do

        let(:distinct) do
          context.distinct(:'description.de')
        end

        it 'returns the specific translation' do
          expect(distinct).to eq([ "deutsch-text" ])
        end
      end

      context 'when plucking a specific locale from _translations field' do

        let(:distinct) do
          context.distinct(:'description_translations.de')
        end

        it 'returns the specific translations' do
          expect(distinct).to eq(['deutsch-text'])
        end
      end

      context 'when fallbacks are enabled with a locale list' do
        require_fallbacks
        with_default_i18n_configs

        before do
          I18n.fallbacks[:he] = [ :en ]
        end

        let(:distinct) do
          context.distinct(:description).first
        end

        it "correctly uses the fallback" do
          I18n.locale = :en
          Dictionary.create!(description: 'english-text')
          I18n.locale = :he
          distinct.should == "english-text"
        end
      end

      context "when the localized field is embedded" do
        with_default_i18n_configs

        before do
          p = Passport.new
          I18n.locale = :en
          p.name = "Neil"
          I18n.locale = :he
          p.name = "Nissim"

          Person.create!(passport: p, employer_id: 12345)
        end

        let(:criteria) do
          Person.where(employer_id: 12345)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let(:distinct) do
          context.distinct("pass.name").first
        end

        let(:distinct_translations) do
          context.distinct("pass.name_translations").first
        end

        let(:distinct_translations_field) do
          context.distinct("pass.name_translations.en").first
        end

        it "returns the translation for the current locale" do
          expect(distinct).to eq("Nissim")
        end

        it "returns the full _translation hash" do
          expect(distinct_translations).to eq({ "en" => "Neil", "he" => "Nissim" })
        end

        it "returns the translation for the requested locale" do
          expect(distinct_translations_field).to eq("Neil")
        end
      end
    end

    context "when getting an embedded field" do

      let(:label) { Label.new(sales: "1E2") }
      let!(:band) { Band.create!(label: label) }
      let(:criteria) { Band.where(_id: band.id) }
      let(:context) { described_class.new(criteria) }

      it "returns the distinct matching fields" do
        expect(context.distinct("label.sales")).to eq([ BigDecimal("1E2") ])
      end
    end
  end

  describe "#tally" do
    let(:fans1) { [ Fanatic.new(age:1), Fanatic.new(age:2) ] }
    let(:fans2) { [ Fanatic.new(age:1), Fanatic.new(age:2) ] }
    let(:fans3) { [ Fanatic.new(age:1), Fanatic.new(age:3) ] }

    let(:genres1) { [ { x: 1, y: { z: 1 } }, { x: 2, y: { z: 2 } }, { y: 3 } ]}
    let(:genres2) { [ { x: 1, y: { z: 1 } }, { x: 2, y: { z: 2 } }, { y: 4 } ]}
    let(:genres3) { [ { x: 1, y: { z: 1 } }, { x: 3, y: { z: 3 } }, { y: 5 } ]}

    let(:label1) {  Label.new(name: "Atlantic") }
    let(:label2) {  Label.new(name: "Atlantic") }
    let(:label3) {  Label.new(name: "Columbia") }

    before do
      Band.create!(origin: "tally", name: "Depeche Mode", years: 30, sales: "1E2", label: label1, genres: genres1)
      Band.create!(origin: "tally", name: "New Order", years: 30, sales: "2E3", label: label2, genres: genres2)
      Band.create!(origin: "tally", name: "10,000 Maniacs", years: 30, sales: "1E2", label: label3, genres: genres3)
      Band.create!(origin: "tally2", fanatics: fans1, genres: [1, 2])
      Band.create!(origin: "tally2", fanatics: fans2, genres: [1, 2])
      Band.create!(origin: "tally2", fanatics: fans3, genres: [1, 3])
    end

    let(:criteria) { Band.where(origin: "tally") }

    context "when tallying a string" do
      let(:tally) do
        criteria.tally(:name)
      end

      it "returns the correct hash" do
        expect(tally).to eq("Depeche Mode" => 1, "New Order" => 1, "10,000 Maniacs" => 1)
      end
    end

    context "using an aliased field" do
      let(:tally) do
        criteria.tally(:years)
      end

      it "returns the correct hash" do
        expect(tally).to eq(30 => 3)
      end
    end

    context "when tallying a demongoizable field" do
      let(:tally) do
        criteria.tally(:sales)
      end

      it "returns the correct hash" do
        expect(tally).to eq(BigDecimal("1E2") => 2, BigDecimal("2E3") => 1)
      end
    end

    context "when tallying a localized field" do
      with_default_i18n_configs

      before do
        I18n.locale = :en
        d1 = Dictionary.create!(description: 'en1')
        d2 = Dictionary.create!(description: 'en1')
        d3 = Dictionary.create!(description: 'en1')
        d4 = Dictionary.create!(description: 'en2')
        I18n.locale = :de
        d1.description = 'de1'
        d2.description = 'de1'
        d3.description = 'de2'
        d4.description = 'de3'
        d1.save!
        d2.save!
        d3.save!
        d4.save!
        I18n.locale = :en
      end

      context "when getting the demongoized field" do
        let(:tallied) do
          Dictionary.tally(:description)
        end

        it "returns the translation for the current locale" do
          expect(tallied).to eq("en1" => 3, "en2" => 1)
        end
      end

      context "when getting a specific locale" do
        let(:tallied) do
          Dictionary.tally("description.de")
        end

        it "returns the translation for the the specific locale" do
          expect(tallied).to eq("de1" => 2, "de2" => 1, "de3" => 1)
        end
      end

      context "when getting the full hash" do
        let(:tallied) do
          Dictionary.tally("description_translations")
        end

        it "returns the correct hash" do
          expect(tallied).to eq(
            {"de" => "de1", "en" => "en1" } => 2,
            {"de" => "de2", "en" => "en1" } => 1,
            {"de" => "de3", "en" => "en2" } => 1
          )
        end
      end
    end

    context "when tallying an embedded localized field" do
      with_default_i18n_configs

      before do
        I18n.locale = :en
        address1a = Address.new(name: "en1")
        address1b = Address.new(name: "en2")
        address2a = Address.new(name: "en1")
        address2b = Address.new(name: "en3")
        I18n.locale = :de
        address1a.name = "de1"
        address1b.name = "de2"
        address2a.name = "de1"
        address2b.name = "de3"
        Person.create!(addresses: [ address1a, address1b ])
        Person.create!(addresses: [ address2a, address2b ])
        I18n.locale = :en
      end

      context "when getting the demongoized field" do
        let(:tallied) do
          Person.tally("addresses.name")
        end

        it "returns the translation for the current locale" do
          expect(tallied).to eq(
            [ "en1", "en2" ] => 1,
            [ "en1", "en3" ] => 1,
          )
        end
      end

      context "when getting a specific locale" do
        let(:tallied) do
          Person.tally("addresses.name.de")
        end

        it "returns the translation for the the specific locale" do
          expect(tallied).to eq(
            [ "de1", "de2" ] => 1,
            [ "de1", "de3" ] => 1,
          )
        end
      end

      context "when getting the full hash" do
        let(:tallied) do
          Person.tally("addresses.name_translations")
        end

        it "returns the correct hash" do
          expect(tallied).to eq(
            [{ "de" => "de1", "en" => "en1" }, { "de" => "de2", "en" => "en2" }] => 1,
            [{ "de" => "de1", "en" => "en1" }, { "de" => "de3", "en" => "en3" }] => 1,
          )
        end
      end

    end

    context "when tallying an embedded field" do
      let(:tally) do
        criteria.tally("label.name")
      end

      it "returns the correct hash" do
        expect(tally).to eq("Atlantic" => 2, "Columbia" => 1)
      end
    end

    context "when tallying an element in an embeds_many field" do
      let(:criteria) { Band.where(origin: "tally2") }

      let(:tally) do
        criteria.tally("fanatics.age")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [1, 2] => 2,
          [1, 3] => 1
        )
      end
    end

    context "when tallying an embeds_many field" do
      let(:criteria) { Band.where(origin: "tally2") }

      let(:tally) do
        criteria.tally("fanatics")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          fans1.map(&:attributes) => 1,
          fans2.map(&:attributes) => 1,
          fans3.map(&:attributes) => 1,
        )
      end
    end

    context "when tallying a field of type array" do
      let(:criteria) { Band.where(origin: "tally2") }

      let(:tally) do
        criteria.tally("genres")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [1, 2] => 2,
          [1, 3] => 1
        )
      end
    end

    context "when tallying an element from an array of hashes" do
      let(:criteria) { Band.where(origin: "tally") }

      let(:tally) do
        criteria.tally("genres.x")
      end

      it "returns the correct hash without the nil keys" do
        expect(tally).to eq(
          [1, 2] => 2,
          [1, 3] => 1
        )
      end
    end

    context "when tallying an element from an array of hashes; with duplicate" do

      before do
        Band.create!(origin: "tally", genres: [ { x: 1 }, {x: 1} ] )
      end

      let(:criteria) { Band.where(origin: "tally") }

      let(:tally) do
        criteria.tally("genres.x")
      end

      it "returns the correct hash without the nil keys" do
        expect(tally).to eq(
          [1, 2] => 2,
          [1, 3] => 1,
          [1, 1] => 1,
        )
      end
    end

    context "when tallying an aliased field of type array" do

      before do
        Person.create!(array: [ 1, 2 ])
        Person.create!(array: [ 1, 3 ])
      end

      let(:tally) do
        Person.tally("array")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [1, 2] => 1,
          [1, 3] => 1
        )
      end
    end

    context "when going multiple levels deep in arrays" do
      let(:criteria) { Band.where(origin: "tally") }

      let(:tally) do
        criteria.tally("genres.y.z")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [1, 2] => 2,
          [1, 3] => 1
        )
      end
    end

    context "when going multiple levels deep in an array" do
      let(:criteria) { Band.where(origin: "tally") }

      let(:tally) do
        criteria.tally("genres.y.z")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [1, 2] => 2,
          [1, 3] => 1
        )
      end
    end

    context "when tallying deeply nested arrays/embedded associations" do

      before do
        Person.create!(addresses: [ Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 2 } } ]))) ])
        Person.create!(addresses: [ Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 2 } } ]))) ])
        Person.create!(addresses: [ Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 3 } } ]))) ])
      end

      let(:tally) do
        Person.tally("addresses.code.deepest.array.y.z")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [ [ 1, 2 ] ] => 2,
          [ [ 1, 3 ] ] => 1
        )
      end
    end

    context "when tallying deeply nested arrays/embedded associations" do

      before do
        Person.create!(addresses: [ Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 2 } } ]))),
                                    Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 2 } } ]))) ])
        Person.create!(addresses: [ Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 2 } } ]))),
                                    Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 2 } } ]))) ])
        Person.create!(addresses: [ Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 3 } } ]))),
                                    Address.new(code: Code.new(deepest: Deepest.new(array: [ { y: { z: 1 } }, { y: { z: 3 } } ]))) ])
      end

      let(:tally) do
        Person.tally("addresses.code.deepest.array.y.z")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [ [ 1, 2 ], [ 1, 2 ] ] => 2,
          [ [ 1, 3 ], [ 1, 3 ] ] => 1
        )
      end
    end

    context "when some keys are missing" do
      before do
        3.times { Band.create!(origin: "tally") }
      end

      let(:tally) do
        criteria.tally(:name)
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          "Depeche Mode" => 1,
          "New Order" => 1,
          "10,000 Maniacs" => 1,
          nil => 3
        )
      end
    end

    context "when the first element is an embeds_one" do
      before do
        Person.create!(name: Name.new(translations: [ Translation.new(language: 1), Translation.new(language: 2) ]))
        Person.create!(name: Name.new(translations: [ Translation.new(language: 1), Translation.new(language: 2) ]))
        Person.create!(name: Name.new(translations: [ Translation.new(language: 1), Translation.new(language: 3) ]))
      end

      let(:tally) do
        Person.tally("name.translations.language")
      end

      it "returns the correct hash" do
        expect(tally).to eq(
          [1, 2] => 2,
          [1, 3] => 1
        )
      end
    end

    context "when tallying demongoizable values from typeless fields" do

      let!(:person1) { Person.create!(ssn: /hello/) }
      let!(:person2) { Person.create!(ssn: BSON::Decimal128.new("1")) }
      let(:tally) { Person.tally("ssn") }

      let(:tallied_classes) do
        tally.keys.map(&:class).sort do |a, b|
          a.to_s.casecmp(b.to_s)
        end
      end

      context "< BSON 5" do
        max_bson_version '4.99.99'

        it "stores the correct types in the database" do
          expect(Person.find(person1.id).attributes["ssn"]).to be_a BSON::Regexp::Raw
          expect(Person.find(person2.id).attributes["ssn"]).to be_a BSON::Decimal128
        end

        it "tallies the correct type" do
          expect(tallied_classes).to be == [ BSON::Decimal128, BSON::Regexp::Raw ]
        end
      end

      context '>= BSON 5' do
        min_bson_version "5.0"

        it "stores the correct types in the database" do
          expect(Person.find(person1.id).ssn).to be_a BSON::Regexp::Raw
          expect(Person.find(person2.id).ssn).to be_a BigDecimal
        end

        it "tallies the correct type" do
          expect(tallied_classes).to be == [ BigDecimal, BSON::Regexp::Raw ]
        end
      end

      context '>= BSON 5 with decimal128 allowed' do
        min_bson_version "5.0"
        config_override :allow_bson5_decimal128, true

        it "stores the correct types in the database" do
          expect(Person.find(person1.id).ssn).to be_a BSON::Regexp::Raw
          expect(Person.find(person2.id).ssn).to be_a BSON::Decimal128
        end

        it "tallies the correct type" do
          expect(tallied_classes).to be == [ BSON::Decimal128, BSON::Regexp::Raw ]
        end
      end
    end
  end

  describe "#each" do

    before do
      Band.create!(name: "Depeche Mode")
    end

    let(:criteria) do
      Band.where(name: "Depeche Mode")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context 'when the criteria has a collation' do

      let(:criteria) do
        Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
      end

      it "yields mongoid documents to the block" do
        context.each do |doc|
          expect(doc).to be_a(Mongoid::Document)
        end
      end

      it "iterates over the matching documents" do
        context.each do |doc|
          expect(doc.name).to eq("Depeche Mode")
        end
      end

      it "returns self" do
        expect(context.each{}).to be(context)
      end
    end

    context "when providing a block" do

      it "yields mongoid documents to the block" do
        context.each do |doc|
          expect(doc).to be_a(Mongoid::Document)
        end
      end

      it "iterates over the matching documents" do
        context.each do |doc|
          expect(doc.name).to eq("Depeche Mode")
        end
      end

      it "returns self" do
        expect(context.each{}).to be(context)
      end
    end

    context "when no block is provided" do

      let(:enum) do
        context.each
      end

      it "returns an enumerator" do
        expect(enum).to be_a(Enumerator)
      end

      context "when iterating over the enumerator" do

        context "when iterating with each" do

          it "yields mongoid documents to the block" do
            enum.each do |doc|
              expect(doc).to be_a(Mongoid::Document)
            end
          end
        end

        context "when iterating with next" do

          before do
            10.times { |i| Band.create!(name: "Test #{i}") }
          end

          let(:criteria) do
            Band.batch_size(5)
          end

          it "yields mongoid documents" do
            expect(enum.next).to be_a(Mongoid::Document)
          end

          it "does not load all documents" do
            subscriber = Mrss::EventSubscriber.new
            context.view.client.subscribe(Mongo::Monitoring::COMMAND, subscriber)

            # first batch
            5.times { enum.next }

            find_events = subscriber.all_events.select do |evt|
              evt.command_name == 'find'
            end
            expect(find_events.length).to be > 0
            get_more_events = subscriber.all_events.select do |evt|
              evt.command_name == 'getMore'
            end
            expect(get_more_events.length).to be == 0

            # force the second batch to be loaded
            enum.next

            get_more_events = subscriber.all_events.select do |evt|
              evt.command_name == 'getMore'
            end
            expect(get_more_events.length).to be > 0

          ensure
            context.view.client.unsubscribe(Mongo::Monitoring::COMMAND, subscriber)
          end
        end
      end
    end

    context 'when the criteria has a parent document' do

      before do
        Post.create!(person: person)
        Post.create!(person: person)
        Post.create!(person: person)
      end

      let(:person) do
        Person.new
      end

      let(:criteria) do
        person.posts.all
      end

      let(:persons) do
        criteria.collect(&:person)
      end

      it 'sets the same parent object on each related object' do
        expect(persons.uniq.size).to eq(1)
      end
    end
  end

  describe "#eager_load" do

    let(:criteria) do
      Person.includes(:game)
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when no documents are returned" do

      let(:game_association) do
        Person.reflect_on_association(:game)
      end

      it "does not make any additional database queries" do
        expect(game_association).to receive(:eager_load).never
        context.send(:eager_load, [])
      end
    end
  end

  describe "#exists?" do

    let!(:band) do
      Band.create!(name: "Depeche Mode", active: true)
    end

    context "when not passing options" do

      context "when the count is zero" do

        let(:criteria) do
          Band.where(name: "New Order")
        end

        let(:context) do
          described_class.new(criteria)
        end

        it "returns false" do
          expect(context).to_not be_exists
        end
      end

      context "when the count is greater than zero" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        it "returns true" do
          expect(context).to be_exists
        end
      end

      context "when caching is not enabled" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        context "when exists? already called and query cache is enabled" do
          query_cache_enabled

          before do
            context.exists?
          end

          it "does not hit the database again" do
            expect_no_queries do
              expect(context).to be_exists
            end
          end
        end
      end
    end

    context "when passing an _id" do

      context "when its of type BSON::ObjectId" do

        context "when calling it on the class" do

          it "returns true" do
            expect(Band.exists?(band._id)).to be true
          end
        end

        context "when calling it on a criteria that includes the object" do

          it "returns true" do
            expect(Band.where(name: band.name).exists?(band._id)).to be true
          end
        end

        context "when calling it on a criteria that does not include the object" do

          it "returns false" do
            expect(Band.where(name: "bogus").exists?(band._id)).to be false
          end
        end

        context "when the id does not exist" do

          it "returns false" do
            expect(Band.exists?(BSON::ObjectId.new)).to be false
          end
        end
      end

      context "when its of type String" do

        context "when the id exists" do

          it "returns true" do
            expect(Band.exists?(band._id.to_s)).to be true
          end
        end

        context "when the id does not exist" do

          it "returns false" do
            expect(Band.exists?(BSON::ObjectId.new.to_s)).to be false
          end
        end
      end
    end

    context "when passing a hash" do

      context "when calling it on the class" do

        it "returns true" do
          expect(Band.exists?(name: band.name)).to be true
        end
      end

      context "when calling it on a criteria that includes the object" do

        it "returns true" do
          expect(Band.where(active: true).exists?(name: band.name)).to be true
        end
      end

      context "when calling it on a criteria that does not include the object" do

        it "returns false" do
          expect(Band.where(active: false).exists?(name: band.name)).to be false
        end
      end

      context "when the conditions don't match" do

        it "returns false" do
          expect(Band.exists?(name: "bogus")).to be false
        end
      end
    end

    context "when passing false" do

      it "returns false" do
        expect(Band.exists?(false)).to be false
      end
    end

    context "when passing nil" do

      it "returns false" do
        expect(Band.exists?(nil)).to be false
      end
    end

    context "when the limit is 0" do

      it "returns false" do
        expect(Band.limit(0).exists?).to be false
      end
    end

    context "when the criteria limit is 0" do

      it "returns false" do
        expect(Band.criteria.limit(0).exists?).to be false
      end
    end
  end

  describe "#explain" do

    let(:criteria) do
      Band.where(name: "Depeche Mode")
    end

    let(:context) do
      described_class.new(criteria)
    end

    it "returns the criteria explain path" do
      explain = context.explain
      expect(explain).to_not be_empty
      expect(explain.keys).to include("queryPlanner", "executionStats", "serverInfo")
    end

    it "respects options passed to explain" do
      explain = context.explain(verbosity: :query_planner)
      expect(explain).to_not be_empty
      expect(explain.keys).to include("queryPlanner", "serverInfo")
      expect(explain.keys).not_to include("executionStats")
    end
  end

  describe "#find_one_and_replace" do

    let!(:depeche) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:tool) do
      Band.create!(name: "Tool")
    end

    context "when the selector matches" do

      context "when not providing options" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_replace(name: 'FKA Twigs')
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "updates the document in the database" do
          expect(depeche.reload.name).to eq('FKA Twigs')
        end
      end

      context "when sorting" do

        let(:criteria) do
          Band.desc(:name)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_replace(likes: 1)
        end

        it "returns the first matching document" do
          expect(result).to eq(tool)
        end

        it "updates the document in the database" do
          expect(tool.reload.likes).to eq(1)
          expect(tool.reload.name).to be_nil
        end
      end

      context "when limiting fields" do

        let(:criteria) do
          Band.only(:_id)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_replace(name: 'FKA Twigs', likes: 1)
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "limits the returned fields" do
          expect(result.name).to be_nil
        end

        it "updates the document in the database" do
          expect(depeche.reload.likes).to eq(1)
        end
      end

      context "when returning new" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_replace({ likes: 1 }, return_document: :after)
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "returns the updated document" do
          expect(result.name).to be_nil
          expect(result.likes).to eq(1)
        end
      end

      context 'when a collation is specified on the criteria' do

        let(:criteria) do
          Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_replace({ likes: 1 }, return_document: :after)
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "returns the updated document" do
          expect(result.likes).to eq(1)
          expect(result.name).to be_nil
        end
      end
    end

    context "when the selector does not match" do

      let(:criteria) do
        Band.where(name: "DEPECHE MODE")
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:result) do
        context.find_one_and_replace(name: 'FKA Twigs')
      end

      it "returns nil" do
        expect(result).to be_nil
      end
    end
  end

  describe "#find_one_and_update" do

    let!(:depeche) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:tool) do
      Band.create!(name: "Tool")
    end

    context "when the selector matches" do

      context "when not providing options" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_update("$inc" => { likes: 1 })
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "updates the document in the database" do
          expect(depeche.reload.likes).to eq(1)
        end
      end

      context "when sorting" do

        let(:criteria) do
          Band.desc(:name)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_update("$inc" => { likes: 1 })
        end

        it "returns the first matching document" do
          expect(result).to eq(tool)
        end

        it "updates the document in the database" do
          expect(tool.reload.likes).to eq(1)
        end
      end

      context "when limiting fields" do

        let(:criteria) do
          Band.only(:_id)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_update("$inc" => { likes: 1 })
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "limits the returned fields" do
          expect(result.name).to be_nil
        end

        it "updates the document in the database" do
          expect(depeche.reload.likes).to eq(1)
        end
      end

      context "when returning new" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_update({ "$inc" => { likes: 1 }}, return_document: :after)
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "returns the updated document" do
          expect(result.likes).to eq(1)
        end
      end

      context 'when a collation is specified on the criteria' do

        let(:criteria) do
          Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_update({ "$inc" => { likes: 1 }}, return_document: :after)
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "returns the updated document" do
          expect(result.likes).to eq(1)
        end
      end
    end

    context "when the selector does not match" do

      let(:criteria) do
        Band.where(name: "Placebo")
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:result) do
        context.find_one_and_update("$inc" => { likes: 1 })
      end

      it "returns nil" do
        expect(result).to be_nil
      end
    end
  end

  describe "#find_one_and_delete" do

    let!(:depeche) do
      Band.create!(name: "Depeche Mode")
    end

    let(:criteria) do
      Band.where(name: "Depeche Mode")
    end

    let(:context) do
      described_class.new(criteria)
    end

    let!(:result) do
      context.find_one_and_delete
    end

    context 'when the selector matches a document' do

      it "returns the first matching document" do
        expect(result).to eq(depeche)
      end

      it "deletes the document from the database" do
        expect {
          depeche.reload
        }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/)
      end

      context 'when a collation is specified on the criteria' do

        let(:criteria) do
          Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
        end

        let(:context) do
          described_class.new(criteria)
        end

        let!(:result) do
          context.find_one_and_delete
        end

        it "returns the first matching document" do
          expect(result).to eq(depeche)
        end

        it "deletes the document from the database" do
          expect {
            depeche.reload
          }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/)
        end
      end
    end

    context 'when the selector does not match a document' do

      let(:criteria) do
        Band.where(name: "Placebo")
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:result) do
        context.find_one_and_delete
      end

      it "returns nil" do
        expect(result).to be_nil
      end
    end
  end

  [ :first, :one ].each do |method|

    describe "##{method}" do

      let!(:depeche_mode) do
        Band.create!(name: "Depeche Mode")
      end

      let!(:new_order) do
        Band.create!(name: "New Order")
      end

      let!(:rolling_stones) do
        Band.create!(name: "The Rolling Stones")
      end

      context "when the context is not cached" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        it "returns the first matching document" do
          expect(context.send(method)).to eq(depeche_mode)
        end

        context 'when the criteria has a collation' do

          let(:criteria) do
            Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
          end

          it "returns the first matching document" do
            expect(context.send(method)).to eq(depeche_mode)
          end
        end
      end

      context "when using .desc" do

        let(:criteria) do
          Band.desc(:name)
        end

        let(:context) do
          described_class.new(criteria)
        end

        context "when there is sort on the context" do

          it "follows the main sort" do
            expect(context.send(method)).to eq(rolling_stones)
          end
        end

        context "when subsequently calling #last" do

          it "returns the correct document" do
            expect(context.send(method)).to eq(rolling_stones)
            expect(context.last).to eq(depeche_mode)
          end
        end
      end

      context 'when the criteria has no sort' do

        let(:criteria) do
          Band.all
        end

        let(:context) do
          described_class.new(criteria)
        end


        it 'applies a sort on _id' do
          expect(context.send(method)).to eq(depeche_mode)
        end

        context 'when calling #last' do

          it 'returns the last document, sorted by _id' do
            expect(context.send(method)).to eq(depeche_mode)
            expect(context.last).to eq(rolling_stones)
          end
        end
      end

      context 'when the criteria has a sort' do

        let(:criteria) do
          Band.desc(:name)
        end

        let(:context) do
          described_class.new(criteria)
        end

        it 'applies the criteria sort' do
          expect(context.send(method)).to eq(rolling_stones)
        end

        context 'when calling #last' do

          it 'applies the criteria sort' do
            expect(context.send(method)).to eq(rolling_stones)
            expect(context.last).to eq(depeche_mode)
          end
        end
      end

      context "when using .sort" do

        let(:criteria) do
          Band.all.sort(:name => -1).criteria
        end

        let(:context) do
          described_class.new(criteria)
        end

        context "when there is sort on the context" do

          it "follows the main sort" do
            expect(context.send(method)).to eq(rolling_stones)
          end
        end

        context "when subsequently calling #last" do

          it "returns the correct document" do
            expect(context.send(method)).to eq(rolling_stones)
            expect(context.last).to eq(depeche_mode)
          end
        end
      end

      context "when the query cache is enabled" do
        query_cache_enabled

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        context "when first method was called before" do

          before do
            context.first
          end

          it "returns the first document without touching the database" do
            expect_no_queries do
              expect(context.send(method)).to eq(depeche_mode)
            end
          end
        end
      end

      context "when including a limit" do

        context "when the context is not cached" do

          let(:context) do
            described_class.new(criteria)
          end

          context "when the limit is 1" do
            let(:criteria) do
              Band.criteria
            end

            let(:docs) do
              context.send(method, 1)
            end

            it "returns an array of documents" do
              expect(docs).to eq([ depeche_mode ])
            end
          end

          context "when the limit is >1" do
            let(:criteria) do
              Band.criteria
            end

            let(:docs) do
              context.send(method, 2)
            end

            it "returns the number of documents in order" do
              expect(docs).to eq([ depeche_mode, new_order ])
            end
          end

          context 'when the criteria has a collation' do

            let(:criteria) do
              Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
            end

            it "returns the first matching document" do
              expect(context.send(method, 1)).to eq([ depeche_mode ])
            end
          end
        end

        context "when the query cache is enabled" do

          let(:context) do
            described_class.new(criteria)
          end

          context "when calling first beforehand" do
            query_cache_enabled

            let(:context) do
              described_class.new(criteria)
            end

            let(:criteria) do
              Band.all
            end

            before do
              context.first(before_limit)
            end

            let(:docs) do
              context.send(method, limit)
            end

            context "when getting all of the documents before" do
              let(:before_limit) { 3 }

              context "when getting all of the documents" do
                let(:limit) { 3 }

                it "returns all documents without touching the database" do
                  expect_no_queries do
                    expect(docs).to eq([ depeche_mode, new_order, rolling_stones ])
                  end
                end
              end

              context "when getting fewer documents" do
                let(:limit) { 2 }

                it "returns the correct documents without touching the database" do
                  expect_no_queries do
                    expect(docs).to eq([ depeche_mode, new_order ])
                  end
                end
              end
            end

            context "when getting fewer documents before" do
              let(:before_limit) { 2 }

              context "when getting the same number of documents" do
                let(:limit) { 2 }

                it "returns the correct documents without touching the database" do
                  expect_no_queries do
                    expect(docs).to eq([ depeche_mode, new_order ])
                  end
                end
              end

              context "when getting more documents" do
                let(:limit) { 3 }

                it "returns the correct documents and touches the database" do
                  expect_query(1) do
                    expect(docs).to eq([ depeche_mode, new_order, rolling_stones ])
                  end
                end
              end
            end

            context "when getting one document before" do
              let(:before_limit) { 1 }

              context "when getting one document" do
                let(:limit) { 1 }

                it "returns the correct documents without touching the database" do
                  expect_no_queries do
                    expect(docs).to eq([ depeche_mode ])
                  end
                end
              end

              context "when getting more than one document" do
                let(:limit) { 3 }

                it "returns the correct documents and touches the database" do
                  expect_query(1) do
                    expect(docs).to eq([ depeche_mode, new_order, rolling_stones ])
                  end
                end
              end
            end
          end
        end
      end

      context "when calling #first then #last and the query cache is enabled" do
        query_cache_enabled

        let(:context) do
          described_class.new(criteria)
        end

        let(:criteria) do
          Band.all
        end

        before do
          context.first(before_limit)
        end

        let(:docs) do
          context.last(limit)
        end

        context "when getting one from the beginning and one from the end" do
          let(:before_limit) { 2 }
          let(:limit) { 1 }

          it "gets the correct document and hits the database" do
            expect_query(1) do
              expect(docs).to eq([rolling_stones])
            end
          end
        end
      end
    end
  end

  describe "#last" do
    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    context "when the context is not cached" do

      let(:criteria) do
        Band.where(name: "Depeche Mode")
      end

      let(:context) do
        described_class.new(criteria)
      end

      it "returns the last matching document" do
        expect(context.last).to eq(depeche_mode)
      end

      context 'when the criteria has a collation' do

        let(:criteria) do
          Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
        end

        it "returns the last matching document" do
          expect(context.last).to eq(depeche_mode)
        end
      end
    end

    context "when using .desc" do

      let(:criteria) do
        Band.desc(:name)
      end

      let(:context) do
        described_class.new(criteria)
      end

      context "when there is sort on the context" do

        it "follows the main sort" do
          expect(context.last).to eq(depeche_mode)
        end
      end

      context "when subsequently calling #first" do

        it "returns the correct document" do
          expect(context.last).to eq(depeche_mode)
          expect(context.first).to eq(rolling_stones)
        end
      end
    end

    context 'when the criteria has no sort' do

      let(:criteria) do
        Band.all
      end

      let(:context) do
        described_class.new(criteria)
      end

      it 'applies a sort on _id' do
        expect(context.last).to eq(rolling_stones)
      end

      context 'when calling #first' do

        it 'returns the first document, sorted by _id' do
          expect(context.last).to eq(rolling_stones)
          expect(context.first).to eq(depeche_mode)
        end
      end
    end

    context 'when the criteria has a sort' do

      let(:criteria) do
        Band.desc(:name)
      end

      let(:context) do
        described_class.new(criteria)
      end


      it 'applies the criteria sort' do
        expect(context.last).to eq(depeche_mode)
      end

      context 'when calling #first' do

        it 'applies the criteria sort' do
          expect(context.last).to eq(depeche_mode)
          expect(context.first).to eq(rolling_stones)
        end
      end
    end

    context "when using .sort" do

      let(:criteria) do
        Band.all.sort(:name => -1).criteria
      end

      let(:context) do
        described_class.new(criteria)
      end

      context "when there is sort on the context" do

        it "follows the main sort" do
          expect(context.last).to eq(depeche_mode)
        end
      end

      context "when subsequently calling #first" do

        it "returns the correct document" do
          expect(context.last).to eq(depeche_mode)
          expect(context.first).to eq(rolling_stones)
        end
      end
    end

    context "when the query cache is enabled" do
      query_cache_enabled

      let(:criteria) do
        Band.where(name: "Depeche Mode")
      end

      let(:context) do
        described_class.new(criteria)
      end

      context "when last method was called before" do

        before do
          context.last
        end

        it "returns the last document without touching the database" do
          expect_no_queries do
            expect(context.last).to eq(depeche_mode)
          end
        end
      end
    end

    context "when including a limit" do

      context "when the context is not cached" do

        let(:context) do
          described_class.new(criteria)
        end

        context "when the limit is 1" do
          let(:criteria) do
            Band.criteria
          end

          let(:docs) do
            context.last(1)
          end

          it "returns an array of documents" do
            expect(docs).to eq([ rolling_stones ])
          end
        end

        context "when the limit is >1" do
          let(:criteria) do
            Band.criteria
          end

          let(:docs) do
            context.last(2)
          end

          it "returns the number of documents in order" do
            expect(docs).to eq([ new_order, rolling_stones ])
          end
        end

        context 'when the criteria has a collation' do

          let(:criteria) do
            Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2)
          end

          it "returns the first matching document" do
            expect(context.last(1)).to eq([ depeche_mode ])
          end
        end
      end

      context "when the context is cached" do

        let(:context) do
          described_class.new(criteria)
        end

        context "when query cache is enabled" do
          query_cache_enabled

          let(:context) do
            described_class.new(criteria)
          end

          let(:criteria) do
            Band.all
          end

          before do
            context.last(before_limit)
          end

          let(:docs) do
            context.last(limit)
          end

          context "when getting all of the documents before" do
            let(:before_limit) { 3 }

            context "when getting all of the documents" do
              let(:limit) { 3 }

              it "returns all documents without touching the db" do
                expect_no_queries do
                  expect(docs).to eq([ depeche_mode, new_order, rolling_stones ])
                end
              end
            end

            context "when getting fewer documents" do
              let(:limit) { 2 }

              it "returns the correct documents without touching the db" do
                expect_no_queries do
                  expect(docs).to eq([ new_order, rolling_stones ])
                end
              end
            end
          end

          context "when getting fewer documents before" do
            let(:before_limit) { 2 }

            context "when getting the same number of documents" do
              let(:limit) { 2 }

              it "returns the correct documents without touching the db" do
                expect_no_queries do
                  expect(docs).to eq([ new_order, rolling_stones ])
                end
              end
            end

            context "when getting more documents" do
              let(:limit) { 3 }

              it "returns the correct documents and touches the database" do
                expect_query(1) do
                  expect(docs).to eq([ depeche_mode, new_order, rolling_stones ])
                end
              end
            end
          end

          context "when getting one document before" do
            let(:before_limit) { 1 }

            context "when getting one document" do
              let(:limit) { 1 }

              it "returns the correct documents without touching the database" do
                expect_no_queries do
                  expect(docs).to eq([ rolling_stones ])
                end
              end
            end

            context "when getting more than one document" do
              let(:limit) { 3 }

              it "returns the correct documents and touches the database" do
                expect_query(1) do
                  expect(docs).to eq([ depeche_mode, new_order, rolling_stones ])
                end
              end
            end
          end
        end
      end
    end

    context "when calling #last then #first and the query cache is enabled" do
      query_cache_enabled

      let(:context) do
        described_class.new(criteria)
      end

      let(:criteria) do
        Band.all
      end

      before do
        context.last(before_limit)
      end

      let(:docs) do
        context.first(limit)
      end

      context "when getting one from the beginning and one from the end" do
        let(:before_limit) { 2 }
        let(:limit) { 1 }

        it "hits the database" do
          expect_query(1) do
            docs
          end
        end

        it "gets the correct document" do
          expect(docs).to eq([ depeche_mode ])
        end
      end
    end
  end

  describe "#initialize" do

    let(:criteria) do
      Band.where(name: "Depeche Mode").no_timeout
    end

    let(:context) do
      described_class.new(criteria)
    end

    it "sets the criteria" do
      expect(context.criteria).to eq(criteria)
    end

    it "sets the klass" do
      expect(context.klass).to eq(Band)
    end

    it "sets the view" do
      expect(context.view).to be_a(Mongo::Collection::View)
    end

    it "sets the view selector" do
      expect(context.view.selector).to eq({ "name" => "Depeche Mode" })
    end
  end

  [ :length, :size ].each do |method|

    describe "##{method}" do

      before do
        Band.create!(name: "Depeche Mode")
        Band.create!(name: "New Order")
      end

      context "when the criteria has a limit" do

        let(:criteria) do
          Band.limit(1)
        end

        let(:context) do
          described_class.new(criteria)
        end

        context "when broken_view_options is false" do
          driver_config_override :broken_view_options, false

          it "returns the number of documents that match" do
            expect(context.send(method)).to eq(1)
          end
        end

        context "when broken_view_options is true" do
          driver_config_override :broken_view_options, true

          it "returns the number of documents that match" do
            expect(context.send(method)).to eq(2)
          end
        end

        context "when calling more than once with different limits" do
          driver_config_override :broken_view_options, false

          it "does not cache the value" do
            expect(context.limit(1).send(method)).to eq(1)
            expect(context.limit(2).send(method)).to eq(2)
          end
        end
      end

      context "when the criteria has no limit" do

        let(:criteria) do
          Band.where(name: "Depeche Mode")
        end

        let(:context) do
          described_class.new(criteria)
        end

        it "returns the number of documents that match" do
          expect(context.send(method)).to eq(1)
        end

        context "when calling more than once with different skips" do
          driver_config_override :broken_view_options, false

          it "does not cache the value" do
            expect(context.skip(0).send(method)).to eq(1)
            expect(context.skip(1).send(method)).to eq(0)
          end
        end

        context "when the results have been iterated over" do

          before do
            context.entries
          end

          it "returns the cached value for all calls" do
            expect(context.view).to receive(:count_documents).once.and_return(1)
            expect(context.send(method)).to eq(1)
          end

          context "when the results have been iterated over multiple times" do

            before do
              context.entries
            end

            it "resets the length on each full iteration" do
              expect(context.size).to eq(1)
            end
          end
        end
      end
    end
  end

  describe "#limit" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    it "limits the results" do
      expect(context.limit(1).entries).to eq([ depeche_mode ])
    end
  end

  describe "#take" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    it "takes the correct number results" do
      expect(context.take(2)).to eq([ depeche_mode, new_order ])
    end

    it "returns an array when passing 1" do
      expect(context.take(1)).to eq([ depeche_mode ])
    end

    it "does not return an array when not passing an argument" do
      expect(context.take).to eq(depeche_mode)
    end

    it "returns all the documents taking more than whats in the db" do
      expect(context.take(5)).to eq([ depeche_mode, new_order, rolling_stones ])
    end
  end

  describe "#take!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    it "takes the first document" do
      expect(context.take!).to eq(depeche_mode)
    end

    context "when there are no documents" do
      it "raises an error" do
        expect do
          Person.take!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Person./)
      end
    end
  end

  describe "#map" do

    before do
      Band.create!(name: "Depeche Mode")
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when passed the symbol field name" do

      it "raises an error" do
        expect do
          context.map(:name)
        end.to raise_error(ArgumentError)
      end
    end

    context "when passed a block" do

      it "performs mapping" do
        expect(context.map(&:name)).to eq ["Depeche Mode", "New Order"]
      end
    end
  end

  describe "#map_reduce" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode", likes: 200)
    end

    let!(:tool) do
      Band.create!(name: "Tool", likes: 100)
    end

    let(:map) do
      %Q{
      function() {
        emit(this.name, { likes: this.likes });
      }}
    end

    let(:reduce) do
      %Q{
      function(key, values) {
        var result = { likes: 0 };
        values.forEach(function(value) {
          result.likes += value.likes;
        });
        return result;
      }}
    end

    let(:ordered_results) do
      results['results'].sort_by { |doc| doc['_id'] }
    end

    context "when no selection is provided" do

      let(:criteria) do
        Band.all
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce).out(inline: 1)
      end

      it "returns the first aggregate result" do
        expect(results).to include(
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        )
      end

      it "returns the second aggregate result" do
        expect(results).to include(
          { "_id" => "Tool", "value" => { "likes" => 100 }}
        )
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(2)
      end

      it "contains the entire raw results" do
        expect(ordered_results).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }},
          { "_id" => "Tool", "value" => { "likes" => 100 }}
        ])
      end

      context 'when statistics are available' do
        max_server_version '4.2'

        it "contains the execution time" do
          expect(results.time).to_not be_nil
        end

        it "contains the count statistics" do
          expect(results["counts"]).to eq({
            "input" => 2, "emit" => 2, "reduce" => 0, "output" => 2
          })
        end

        it "contains the input count" do
          expect(results.input).to eq(2)
        end

        it "contains the emitted count" do
          expect(results.emitted).to eq(2)
        end

        it "contains the reduced count" do
          expect(results.reduced).to eq(0)
        end

        it "contains the output count" do
          expect(results.output).to eq(2)
        end
      end
    end

    context "when selection is provided" do

      let(:criteria) do
        Band.where(name: "Depeche Mode")
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce).out(inline: 1)
      end

      it "includes the aggregate result" do
        expect(results).to include(
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        )
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(1)
      end

      it "contains the entire raw results" do
        expect(ordered_results).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        ])
      end

      context 'when statistics are available' do
        max_server_version '4.2'

        it "contains the execution time" do
          expect(results.time).to_not be_nil
        end

        it "contains the count statistics" do
          expect(results["counts"]).to eq({
            "input" => 1, "emit" => 1, "reduce" => 0, "output" => 1
          })
        end

        it "contains the input count" do
          expect(results.input).to eq(1)
        end

        it "contains the emitted count" do
          expect(results.emitted).to eq(1)
        end

        it "contains the reduced count" do
          expect(results.reduced).to eq(0)
        end

        it "contains the output count" do
          expect(results.output).to eq(1)
        end
      end
    end

    context "when sorting is provided" do

      before do
        Band.index(name: -1)
        Band.create_indexes
      end

      let(:criteria) do
        Band.desc(:name)
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce).out(inline: 1)
      end

      it "returns the first aggregate result" do
        expect(results).to include(
          { "_id" => "Tool", "value" => { "likes" => 100 }}
        )
      end

      it "returns the second aggregate result" do
        expect(results).to include(
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        )
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(2)
      end

      it "contains the entire raw results" do
        expect(ordered_results).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }},
          { "_id" => "Tool", "value" => { "likes" => 100 }}
        ])
      end
    end

    context "when limiting is provided" do
      # map/reduce with limit is not supported on sharded clusters:
      # https://jira.mongodb.org/browse/SERVER-2099
      require_topology :single, :replica_set

      let(:criteria) do
        Band.limit(1)
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce).out(inline: 1)
      end

      it "returns the first aggregate result" do
        expect(results).to include(
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        )
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(1)
      end

      it "contains the entire raw results" do
        expect(results["results"]).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        ])
      end
    end

    context "when the output is replace" do

      let(:criteria) do
        Band.limit(1)
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce).out(replace: "mr-output")
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(1)
      end

      it "contains the entire results" do
        expect(results).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        ])
      end
    end

    context "when the output is reduce" do

      let(:criteria) do
        Band.limit(1)
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce).out(reduce: :mr_output)
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(1)
      end

      it "contains the entire results" do
        expect(results).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        ])
      end
    end

    context "when the output is merge" do

      let(:criteria) do
        Band.limit(1)
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce).out(merge: :mr_output)
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(1)
      end

      it "contains the entire results" do
        expect(results).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
        ])
      end
    end

    context "when the output specifies a different db" do
      # Limit is not supported in sharded clusters
      require_topology :single, :replica_set

      let(:criteria) do
        Band.limit(1)
      end

      let(:context) do
        described_class.new(criteria)
      end

      after do
        Band.with(database: 'another-db') do |b|
          b.all.delete
        end
      end

      context 'when db is a string' do

        let(:results) do
          context.map_reduce(map, reduce).out(merge: :mr_output, db: 'another-db')
        end

        it "returns the correct number of documents" do
          expect(results.count).to eq(1)
        end

        it "contains the entire results" do
          expect(results).to eq([
                                    { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
                                ])
        end

        it 'writes to the specified db' do
          expect(Band.mongo_client.with(database: 'another-db')[:mr_output].find.count).to eq(1)
        end
      end

      context 'when db is a symbol' do

        let(:results) do
          context.map_reduce(map, reduce).out(merge: :mr_output, 'db' => 'another-db')
        end

        it "returns the correct number of documents" do
          expect(results.count).to eq(1)
        end

        it "contains the entire results" do
          expect(results).to eq([
                                    { "_id" => "Depeche Mode", "value" => { "likes" => 200 }}
                                ])
        end

        it 'writes to the specified db' do
          expect(Band.mongo_client.with(database: 'another-db')[:mr_output].find.count).to eq(1)
        end
      end
    end

    context "when providing no output" do

      let(:criteria) do
        Band.limit(1)
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:results) do
        context.map_reduce(map, reduce)
      end

      it "raises an error" do
        expect {
          results.entries
        }.to raise_error(Mongoid::Errors::NoMapReduceOutput)
      end
    end

    context "when providing a finalize" do

      let(:criteria) do
        Band.limit(1)
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:finalize) do
        %Q{
        function(key, value) {
          value.extra = true;
          return value;
        }}
      end

      let(:results) do
        context.map_reduce(map, reduce).out(inline: 1).finalize(finalize)
      end

      it "returns the correct number of documents" do
        expect(results.count).to eq(1)
      end

      it "contains the entire results" do
        expect(results).to eq([
          { "_id" => "Depeche Mode", "value" => { "likes" => 200, "extra" => true }}
        ])
      end
    end
  end

  describe "#skip" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    it "limits the results" do
      expect(context.skip(1).entries).to eq([ new_order ])
    end

    context "with #last" do
      it "returns the nth from last element" do
        expect(context.skip(1).last).to eq(depeche_mode)
      end
    end
  end

  describe "#sort" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when providing a spec" do

      it "sorts the results" do
        expect(context.sort(name: -1).entries).to eq([ new_order, depeche_mode ])
      end

      it "returns the context" do
        expect(context.sort(name: 1)).to eq(context)
      end
    end

    context "when providing a block" do

      let(:sorted) do
        context.sort do |a, b|
          b.name <=> a.name
        end
      end

      it "sorts the results in memory" do
        expect(sorted).to eq([ new_order, depeche_mode ])
      end
    end
  end

  describe "#update" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when adding an element to a HABTM set" do

      let(:person) do
        Person.create!
      end

      let(:preference) do
        Preference.create!
      end

      before do
        Person.where(id: person.id).
          update("$addToSet" => { preference_ids: preference.id })
      end

      it "adds a single element to the array" do
        expect(person.reload.preference_ids).to eq([ preference.id ])
      end
    end

    context "when providing attributes" do

      context "when the attributes are of the correct type" do

        before do
          context.update(name: "Smiths")
        end

        it "updates only the first matching document" do
          expect(depeche_mode.reload.name).to eq("Smiths")
        end

        it "does not update the last matching document" do
          expect(new_order.reload.name).to eq("New Order")
        end
      end

      context "when the attributes must be mongoized" do

        context "when coercing a string to integer" do

          before do
            context.update(member_count: "1")
          end

          it "updates the first matching document" do
            expect(depeche_mode.reload.member_count).to eq(1)
          end

          it "does not update the last matching document" do
            expect(new_order.reload.member_count).to be_nil
          end
        end

        context "when coercing a string to date" do

          before do
            context.update(founded: "1979/1/1")
          end

          it "updates the first matching document" do
            expect(depeche_mode.reload.founded).to eq(Date.new(1979, 1, 1))
          end

          it "does not update the last matching document" do
            expect(new_order.reload.founded).to be_nil
          end
        end
      end
    end

    context "when providing atomic operations" do

      context "when only atomic operations are provided" do

        context "when the attributes are in the correct type" do

          before do
            context.update("$set" => { name: "Smiths" })
          end

          it "updates the first matching document" do
            expect(depeche_mode.reload.name).to eq("Smiths")
          end

          it "does not update the last matching document" do
            expect(new_order.reload.name).to eq("New Order")
          end
        end

        context "when the attributes must be mongoized" do

          before do
            context.update("$set" => { member_count: "1" })
          end

          it "updates the first matching document" do
            expect(depeche_mode.reload.member_count).to eq(1)
          end

          it "does not update the last matching document" do
            expect(new_order.reload.member_count).to be_nil
          end
        end
      end

      context "when a mix are provided" do

        before do
          context.update("$set" => { name: "Smiths" }, likes: 100)
        end

        it "updates the first matching document's set" do
          expect(depeche_mode.reload.name).to eq("Smiths")
        end

        it "updates the first matching document's updates" do
          expect(depeche_mode.reload.likes).to eq(100)
        end

        it "does not update the last matching document's set" do
          expect(new_order.reload.name).to eq("New Order")
        end

        it "does not update the last matching document's updates" do
          expect(new_order.reload.likes).to be_nil
        end
      end
    end

    context "when providing no attributes" do

      it "returns false" do
        expect(context.update).to be false
      end
    end

    context 'when provided array filters' do

      before do
        Band.delete_all
        b = Band.new(name: 'Depeche Mode')
        b.labels << Label.new(name: 'Warner')
        b.labels << Label.new(name: 'Sony')
        b.labels << Label.new(name: 'Cbs')
        b.save!

        b = Band.new(name: 'FKA Twigs')
        b.labels << Label.new(name: 'Warner')
        b.labels << Label.new(name: 'Cbs')
        b.save!
      end


      let(:criteria) do
        Band.where(name: 'Depeche Mode')
      end

      let!(:update) do
        context.update({ '$set' => { 'labels.$[i].name' => 'Sony' } },
                       array_filters: [{ 'i.name' => 'Cbs' }])
      end

      it 'applies the array filters' do
        expect(Band.where(name: 'Depeche Mode').first.labels.collect(&:name)).to match_array(['Warner', 'Sony', 'Sony'])
      end

      it 'does not affect other documents' do
        expect(Band.where(name: 'FKA Twigs').first.labels.collect(&:name)).to match_array(['Warner', 'Cbs'])
      end
    end
  end

  describe "#update_all" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode", origin: "Essex")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let(:criteria) do
      Band.all
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when providing attributes" do

      context "when the attributes are of the correct type" do

        before do
          context.update_all(name: "Smiths")
        end

        it "updates the first matching document" do
          expect(depeche_mode.reload.name).to eq("Smiths")
        end

        it "does not clear out other attributes" do
          expect(depeche_mode.reload.origin).to eq("Essex")
        end

        it "updates the last matching document" do
          expect(new_order.reload.name).to eq("Smiths")
        end
      end

      context "when the attributes must be mongoized" do

        before do
          context.update_all(member_count: "1")
        end

        it "updates the first matching document" do
          expect(depeche_mode.reload.member_count).to eq(1)
        end

        it "updates the last matching document" do
          expect(new_order.reload.member_count).to eq(1)
        end
      end

      context "when using aliased field names" do

        before do
          context.update_all(years: 100)
        end

        it "updates the first matching document" do
          expect(depeche_mode.reload.years).to eq(100)
        end

        it "updates the last matching document" do
          expect(new_order.reload.years).to eq(100)
        end
      end
    end

    context "when providing atomic operations" do

      context "when only atomic operations are provided" do

        context "when the attributes are in the correct type" do

          context "when operation is $set" do

            before do
              context.update_all("$set" => { name: "Smiths" })
            end

            it "updates the first matching document" do
              expect(depeche_mode.reload.name).to eq("Smiths")
            end

            it "updates the last matching document" do
              expect(new_order.reload.name).to eq("Smiths")
            end
          end

          context "when operation is $push" do

            before do
              depeche_mode.update_attribute(:genres, ["electronic"])
              new_order.update_attribute(:genres, ["electronic"])
              context.update_all("$push" => { genres: "pop" })
            end

            it "updates the first matching document" do
              expect(depeche_mode.reload.genres).to eq(["electronic", "pop"])
            end

            it "updates the last matching document" do
              expect(new_order.reload.genres).to eq(["electronic", "pop"])
            end
          end

          context "when operation is $addToSet" do

            before do
              context.update_all("$addToSet" => { genres: "electronic" })
            end

            it "updates the first matching document" do
              expect(depeche_mode.reload.genres).to eq(["electronic"])
            end

            it "updates the last matching document" do
              expect(new_order.reload.genres).to eq(["electronic"])
            end
          end

          context "when operation is $pull" do
            context "when pulling single element" do

              before do
                depeche_mode.update_attribute(:genres, ["electronic", "pop"])
                new_order.update_attribute(:genres, ["electronic", "pop"])
                context.update_all("$pull" => { genres: "electronic" })
              end

              it "updates the first matching document" do
                expect(depeche_mode.reload.genres).to eq(["pop"])
              end

              it "updates the last matching document" do
                expect(new_order.reload.genres).to eq(["pop"])
              end
            end

            context "when pulling based on condition" do
              before do
                depeche_mode.update_attribute(:genres, ["electronic", "pop", "dance"])
                new_order.update_attribute(:genres, ["electronic", "pop", "dance"])
                context.update_all("$pull" => { genres: { '$in' => ["electronic", "pop"] } })
              end

              it "updates the first matching document" do
                expect(depeche_mode.reload.genres).to eq(["dance"])
              end

              it "updates the last matching document" do
                expect(new_order.reload.genres).to eq(["dance"])
              end
            end
          end

          context "when operation is $pop" do

            before do
              depeche_mode.update_attribute(:genres, ["pop", "electronic"])
            end

            it "removes first element in array" do
              context.update_all("$pop" => { genres: -1 })
              expect(depeche_mode.reload.genres).to eq(["electronic"])
            end

            it "removes last element in array" do
              context.update_all("$pop" => { genres: 1 })
              expect(depeche_mode.reload.genres).to eq(["pop"])
            end
          end

          context "when operation is $pullAll" do

            before do
              depeche_mode.update_attribute(:genres, ["pop", "electronic", "dance", "pop" ])
              new_order.update_attribute(:genres, ["electronic", "pop", "electronic", "dance"])
              context.update_all("$pullAll" => { genres: ["pop", "electronic"] })
            end

            it "updates the first matching document" do
              expect(depeche_mode.reload.genres).to eq(["dance"])
            end

            it "updates the last matching document" do
              expect(new_order.reload.genres).to eq(["dance"])
            end
          end
        end

        context 'when using aliased field names' do
          before do
            context.update_all('$set' => { years: 100 })
          end

          it "updates the first matching document" do
            expect(depeche_mode.reload.years).to eq(100)
          end

          it "updates the last matching document" do
            expect(new_order.reload.years).to eq(100)
          end
        end

        context "when the attributes must be mongoized" do

          before do
            context.update_all("$set" => { location: LatLng.new(52.30, 13.25) })
          end

          it "updates the first matching document" do
            expect(depeche_mode.reload.location).to eq(LatLng.new(52.30, 13.25))
          end

          it "updates the last matching document" do
            expect(new_order.reload.location).to eq(LatLng.new(52.30, 13.25))
          end
        end
      end

      context "when a mix are provided" do

        before do
          context.update_all("$set" => { name: "Smiths" }, likes: 100)
        end

        it "updates the first matching document's set" do
          expect(depeche_mode.reload.name).to eq("Smiths")
        end

        it "updates the first matching document's updates" do
          expect(depeche_mode.reload.likes).to eq(100)
        end

        it "updates the last matching document's set" do
          expect(new_order.reload.name).to eq("Smiths")
        end

        it "updates the last matching document's updates" do
          expect(new_order.reload.likes).to eq(100)
        end
      end
    end

    context "when providing no attributes" do

      it "returns false" do
        expect(context.update_all).to be false
      end
    end

    context 'when provided array filters' do

      before do
        Band.delete_all
        b = Band.new(name: 'Depeche Mode')
        b.labels << Label.new(name: 'Warner')
        b.labels << Label.new(name: 'Sony')
        b.labels << Label.new(name: 'Cbs')
        b.save!

        b = Band.new(name: 'FKA Twigs')
        b.labels << Label.new(name: 'Warner')
        b.labels << Label.new(name: 'Cbs')
        b.save!
      end


      let(:criteria) do
        Band.all
      end

      let!(:update) do
        context.update_all({ '$set' => { 'labels.$[i].name' => 'Sony' } },
                       array_filters: [{ 'i.name' => 'Cbs' }])
      end

      it 'applies the array filters' do
        expect(Band.where(name: 'Depeche Mode').first.labels.collect(&:name)).to match_array(['Warner', 'Sony', 'Sony'])
      end

      it 'updates all documents' do
        expect(Band.where(name: 'FKA Twigs').first.labels.collect(&:name)).to match_array(['Warner', 'Sony'])
      end
    end
  end

  describe '#pipeline' do

    context 'when the criteria has a selector' do

      before do
        Artist.index(name: "text")
        Artist.create_indexes
      end

      let(:criteria) do
        Artist.text_search("New Order")
      end

      let(:context) do
        described_class.new(criteria)
      end

      let(:pipeline_match) do
        context.send(:pipeline, :some_field).first['$match']
      end

      it 'creates a pipeline with the selector as one of the $match criteria' do
        expect(pipeline_match).to include({ '$text' => { '$search' => "New Order" } })
      end

      it 'creates a pipeline with the $exists operator as one of the $match criteria' do
        expect(pipeline_match).to include({ 'some_field' => { '$exists' => true } })
      end
    end
  end

  describe "#first!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the first document" do
        expect(context.first!).to eq(depeche_mode)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the first document" do
        expect(context.sort(name: 1).first!).to eq(death_cab)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.first!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe "#last!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the last document" do
        expect(context.last!).to eq(death_cab)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the last document" do
        expect(context.sort(name: 1).last!).to eq(rolling_stones)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.last!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe "#second" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second document" do
        expect(context.second).to eq(new_order)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second document" do
        expect(context.sort(name: 1).second).to eq(depeche_mode)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "returns nil" do
        expect(context.second).to be_nil
      end
    end
  end

  describe "#second!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second document" do
        expect(context.second!).to eq(new_order)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second document" do
        expect(context.sort(name: 1).second!).to eq(depeche_mode)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.second!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe "#third" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third document" do
        expect(context.third).to eq(rolling_stones)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third document" do
        expect(context.sort(name: 1).third).to eq(new_order)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "returns nil" do
        expect(context.third).to be_nil
      end
    end
  end

  describe "#third!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third document" do
        expect(context.third!).to eq(rolling_stones)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third document" do
        expect(context.sort(name: 1).third!).to eq(new_order)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.third!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe "#fourth" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fourth document" do
        expect(context.fourth).to eq(death_cab)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fourth document" do
        expect(context.sort(name: 1).fourth).to eq(rolling_stones)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "returns nil" do
        expect(context.fourth).to be_nil
      end
    end
  end

  describe "#fourth!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fourth document" do
        expect(context.fourth!).to eq(death_cab)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fourth document" do
        expect(context.sort(name: 1).fourth!).to eq(rolling_stones)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.fourth!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe "#fifth" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let!(:guns_and_roses) do
      Band.create!(name: "Guns and Roses")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fifth document" do
        expect(context.fifth).to eq(guns_and_roses)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fifth document" do
        expect(context.sort(name: 1).fifth).to eq(rolling_stones)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "returns nil" do
        expect(context.fifth).to be_nil
      end
    end
  end

  describe "#fifth!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let!(:guns_and_roses) do
      Band.create!(name: "Guns and Roses")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fifth document" do
        expect(context.fifth!).to eq(guns_and_roses)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the fifth document" do
        expect(context.sort(name: 1).fifth!).to eq(rolling_stones)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.fifth!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe "#second_to_last" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second_to_last document" do
        expect(context.second_to_last).to eq(rolling_stones)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second_to_last document" do
        expect(context.sort(name: 1).second_to_last).to eq(new_order)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "returns nil" do
        expect(context.second_to_last).to be_nil
      end
    end
  end

  describe "#second_to_last!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second_to_last document" do
        expect(context.second_to_last!).to eq(rolling_stones)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the second_to_last document" do
        expect(context.sort(name: 1).second_to_last!).to eq(new_order)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.second_to_last!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe "#third_to_last" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third_to_last document" do
        expect(context.third_to_last).to eq(new_order)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third_to_last document" do
        expect(context.sort(name: 1).third_to_last).to eq(depeche_mode)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "returns nil" do
        expect(context.third_to_last).to be_nil
      end
    end
  end

  describe "#third_to_last!" do

    let!(:depeche_mode) do
      Band.create!(name: "Depeche Mode")
    end

    let!(:new_order) do
      Band.create!(name: "New Order")
    end

    let!(:rolling_stones) do
      Band.create!(name: "The Rolling Stones")
    end

    let!(:death_cab) do
      Band.create!(name: "Death Cab For Cutie")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context "when there's no sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third_to_last document" do
        expect(context.third_to_last!).to eq(new_order)
      end
    end

    context "when there's a custom sort" do
      let(:criteria) do
        Band.all
      end

      it "gets the third_to_last document" do
        expect(context.sort(name: 1).third_to_last!).to eq(depeche_mode)
      end
    end

    context "when there are no documents" do
      let(:criteria) do
        Band.where(name: "bogus")
      end

      it "raises an error" do
        expect do
          context.third_to_last!
        end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Band./)
      end
    end
  end

  describe '#load_async' do
    let!(:band) do
      Band.create!(name: "Depeche Mode")
    end

    let(:criteria) do
      Band.where(name: "Depeche Mode")
    end

    let(:context) do
      described_class.new(criteria)
    end

    context 'with global thread pool async query executor' do
      config_override :async_query_executor, :global_thread_pool

      it 'preloads the documents' do
        context.load_async
        context.documents_loader.wait

        expect(context.view).not_to receive(:map)
        expect(context.to_a).to eq([band])
      end

      it 're-raises exception during preload' do
        expect_any_instance_of(Mongoid::Contextual::Mongo::DocumentsLoader)
          .to receive(:execute)
          .at_least(:once)
          .and_raise(Mongo::Error::OperationFailure)

        context.load_async
        context.documents_loader.wait

        expect do
          context.to_a
        end.to raise_error(Mongo::Error::OperationFailure)
      end
    end

    context 'with immediate thread pool async query executor' do
      config_override :async_query_executor, :immediate

      it 'preloads the documents' do
        context.load_async
        context.documents_loader.wait

        expect(context.view).not_to receive(:map)
        expect(context.to_a).to eq([band])
      end

      it 're-raises exception during preload' do
        expect_any_instance_of(Mongoid::Contextual::Mongo::DocumentsLoader)
          .to receive(:execute)
          .at_least(:once)
          .and_raise(Mongo::Error::OperationFailure)

        context.load_async
        context.documents_loader.wait

        expect do
          context.to_a
        end.to raise_error(Mongo::Error::OperationFailure)
      end
    end
  end
end
