# frozen_string_literal: true
# encoding: utf-8

require "spec_helper"

describe Mongoid::Association::Referenced::HasMany::Targets::Enumerable do

  describe "#==" do

    context "when comparing with an enumerable" do

      let(:person) do
        Person.create
      end

      let!(:post) do
        Post.create(person_id: person.id)
      end

      context "when only a criteria target exists" do

        let(:criteria) do
          Post.where(person_id: person.id)
        end

        let!(:enumerable) do
          described_class.new(criteria)
        end

        it "returns the equality check" do
          expect(enumerable).to eq([ post ])
        end
      end

      context "when only an array target exists" do

        let!(:enumerable) do
          described_class.new([ post ])
        end

        it "returns the equality check" do
          expect(enumerable._loaded.values).to eq([ post ])
        end
      end

      context "when a criteria and added exist" do

        let(:criteria) do
          Post.where(person_id: person.id)
        end

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

        let(:post_two) do
          Post.new
        end

        context "when the added does not contain unloaded docs" do

          before do
            enumerable << post_two
          end

          it "returns the equality check" do
            expect(enumerable).to eq([ post, post_two ])
          end
        end

        context "when the added contains unloaded docs" do

          before do
            enumerable << post
          end

          it "returns the equality check" do
            expect(enumerable).to eq([ post ])
          end
        end

        context "when the enumerable is loaded" do

          before do
            enumerable.instance_variable_set(:@executed, true)
          end

          context "when the loaded has no docs and added is persisted" do

            before do
              post.save
              enumerable._added[post.id] = post
            end

            it "returns the equality check" do
              expect(enumerable).to eq([ post ])
            end
          end
        end
      end
    end

    context "when comparing with a non enumerable" do

      let(:enumerable) do
        described_class.new([])
      end

      it "returns false" do
        expect(enumerable).to_not eq("person")
      end
    end
  end

  describe "#===" do

    let(:enumerable) do
      described_class.new([])
    end

    context "when compared to an array class" do

      it "returns true" do
        expect(enumerable === Array).to be true
      end
    end

    context "when compared to an enumerable class" do

      it "returns true" do
        expect(enumerable === described_class).to be true
      end
    end

    context "when compared to a different class" do

      it "returns false" do
        expect(enumerable === Mongoid::Document).to be false
      end
    end

    context "when compared to an array instance" do

      context "when the entries are equal" do

        let(:other) do
          described_class.new([])
        end

        it "returns true" do
          expect(enumerable === other).to be true
        end
      end

      context "when the entries are not equal" do

        let(:other) do
          described_class.new([ Band.new ])
        end

        it "returns false" do
          expect(enumerable === other).to be false
        end
      end
    end
  end

  describe "#<<" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(person_id: person.id)
    end

    let!(:enumerable) do
      described_class.new([])
    end

    context "when the relation is empty" do

      let!(:added) do
        enumerable << post
      end

      it "adds the document to the added target" do
        expect(enumerable._added).to eq({ post.id => post })
      end

      it "returns the added documents" do
        expect(added).to eq([ post ])
      end

      it "sets the base on the new document" do
        expect_query(0) do
          added.collect(&:person)
        end
      end
    end
  end

  describe "#any?" do

    let(:person) do
      Person.create
    end

    let!(:post_one) do
      Post.create(person_id: person.id)
    end

    let!(:post_two) do
      Post.create(person_id: person.id)
    end

    context "when only a criteria target exists" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      let!(:any) do
        enumerable.any?
      end

      it "returns true" do
        expect(any).to be true
      end

      it "retains the correct length" do
        expect(enumerable.length).to eq(2)
      end

      it "retains the correct length when calling to_a" do
        expect(enumerable.to_a.length).to eq(2)
      end

      context "when iterating over the relation a second time" do

        before do
          enumerable.each { |post| post }
        end

        it "retains the correct length" do
          expect(enumerable.length).to eq(2)
        end

        it "retains the correct length when calling to_a" do
          expect(enumerable.to_a.length).to eq(2)
        end
      end
    end

    context "when the documents have been loaded" do
      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      before do
        enumerable.load_all!
      end

      it "is _loaded" do
        expect(enumerable._loaded?).to be true
      end

      context "when a block is given" do
        it "returns true when the predicate is true" do
          expect(
            enumerable.any? { |doc| true }
          ).to be true
        end

        it "returns false when the predicate is false" do
          expect(
            enumerable.any? { |doc| false }
          ).to be false
        end
      end

      context "when an argument is given" do
        ruby_version_gte '2.5'

        it "returns true when the argument is true" do
          expect(enumerable.any?(Post)).to be true
        end

        it "returns false when the argument is false" do
          expect(enumerable.any?(Sandwich)).to be false
        end
      end

      context "when both an argument and a block are given" do
        ruby_version_gte '2.5'

        it "gives precedence to the pattern" do
          expect(
            enumerable.any?(Post) { |doc| false }
          ).to be true
        end
      end
    end

    context "when the documents are not loaded" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      it "is not _loaded" do
        expect(enumerable._loaded?).to be false
      end

      context "when a block is given" do
        it "returns true when the predicate is true" do
          expect(
            enumerable.any? { |doc| true }
          ).to be true
        end

        it "returns false when the predicate is false" do
          expect(
            enumerable.any? { |doc| false }
          ).to be false
        end
      end

      context "when an argument is given" do
        ruby_version_gte '2.5'

        it "returns true when the argument is true" do
          expect(enumerable.any?(Post)).to be true
        end

        it "returns false when the argument is false" do
          expect(enumerable.any?(Sandwich)).to be false
        end
      end

      context "when both an argument and a block are given" do
        ruby_version_gte '2.5'

        it "gives precedence to the pattern" do
          expect(
            enumerable.any?(Post) { |doc| false }
          ).to be true
        end
      end
    end
  end

  describe "#clear" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(person_id: person.id)
    end

    let!(:post_two) do
      Post.create(person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

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

    before do
      enumerable._loaded[post.id] = post
      enumerable << post
    end

    let!(:clear) do
      enumerable.clear do |doc|
        expect(doc).to be_a(Post)
      end
    end

    it "clears out the loaded docs" do
      expect(enumerable._loaded).to be_empty
    end

    it "clears out the added docs" do
      expect(enumerable._added).to be_empty
    end

    it "retains its loaded state" do
      expect(enumerable).to_not be__loaded
    end
  end

  describe "#clone" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(title: "one", person_id: person.id)
    end

    let!(:post_two) do
      Post.create(title: "two", person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

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

    before do
      enumerable << post
      enumerable << post_two
    end

    let(:cloned) do
      enumerable.clone
    end

    it "does not retain the first id" do
      expect(cloned.first).to_not eq(post)
    end

    it "does not retain the last id" do
      expect(cloned.last).to_not eq(post_two)
    end
  end

  describe "#delete" do

    let(:person) do
      Person.create
    end

    context "when the document is loaded" do

      let!(:post) do
        Post.create(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new([ post ])
      end

      let!(:deleted) do
        enumerable.delete(post)
      end

      it "deletes the document from the enumerable" do
        expect(enumerable._loaded).to be_empty
      end

      it "returns the document" do
        expect(deleted).to eq(post)
      end
    end

    context "when the document is added" do

      let!(:post) do
        Post.new
      end

      let(:criteria) do
        Person.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      before do
        enumerable << post
      end

      let!(:deleted) do
        enumerable.delete(post)
      end

      it "removes the document from the added docs" do
        expect(enumerable._added).to be_empty
      end

      it "returns the document" do
        expect(deleted).to eq(post)
      end
    end

    context "when the document is unloaded" do

      let!(:post) do
        Post.create(person_id: person.id)
      end

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      let!(:deleted) do
        enumerable.delete(post)
      end

      it "does not load the document" do
        expect(enumerable._loaded).to be_empty
      end

      it "returns the document" do
        expect(deleted).to eq(post)
      end
    end

    context "when the document is not found" do

      let!(:post) do
        Post.create(person_id: person.id)
      end

      let(:criteria) do
        Person.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new([ post ])
      end

      let!(:deleted) do
        enumerable.delete(Post.new) do |doc|
          expect(doc).to be_nil
        end
      end

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

  describe "#delete_if" do

    let(:person) do
      Person.create
    end

    context "when the document is loaded" do

      let!(:post) do
        Post.create(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new([ post ])
      end

      let!(:deleted) do
        enumerable.delete_if { |doc| doc == post }
      end

      it "deletes the document from the enumerable" do
        expect(enumerable._loaded).to be_empty
      end

      it "returns the remaining docs" do
        expect(deleted).to be_empty
      end
    end

    context "when the document is added" do

      let!(:post) do
        Post.new
      end

      let(:criteria) do
        Person.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      before do
        enumerable << post
      end

      let!(:deleted) do
        enumerable.delete_if { |doc| doc == post }
      end

      it "removes the document from the added docs" do
        expect(enumerable._added).to be_empty
      end

      it "returns the remaining docs" do
        expect(deleted).to be_empty
      end
    end

    context "when the document is unloaded" do

      let!(:post) do
        Post.create(person_id: person.id)
      end

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      let!(:deleted) do
        enumerable.delete_if { |doc| doc == post }
      end

      it "does not load the document" do
        expect(enumerable._loaded).to be_empty
      end

      it "returns the remaining docs" do
        expect(deleted).to be_empty
      end
    end

    context "when the block doesn't match" do

      let!(:post) do
        Post.create(person_id: person.id)
      end

      let(:criteria) do
        Person.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new([ post ])
      end

      let!(:deleted) do
        enumerable.delete_if { |doc| doc == Post.new }
      end

      it "returns the remaining docs" do
        expect(deleted).to eq([ post ])
      end
    end
  end

  describe "#detect" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(person: person, title: "test")
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:enumerable) do
      described_class.new(criteria)
    end

    context "when setting a value on the matching document" do

      before do
        enumerable.detect{ |post| post.title = "test" }.rating = 10
      end

      it "sets the value on the instance" do
        expect(enumerable.detect{ |post| post.title = "test" }.rating).to eq(10)
      end
    end
  end

  describe "#each" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(person_id: person.id)
    end

    context "when only a criteria target exists" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      let!(:iterated) do
        enumerable.each do |doc|
          expect(doc).to be_a(Post)
        end
      end

      it "loads each document" do
        expect(enumerable._loaded).to eq({ post.id => post })
      end

      it "becomes loaded" do
        expect(enumerable).to be__loaded
      end

      context 'when the base relation is accessed from each document' do

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

        before do
          Post.create(person_id: person.id)
          Post.create(person_id: person.id)
        end

        it 'sets the base relation from the criteria' do
          expect(persons.uniq.size).to eq(1)
        end
      end
    end

    context "when only an array target exists" do

      let!(:enumerable) do
        described_class.new([ post ])
      end

      let!(:iterated) do
        enumerable.each do |doc|
          expect(doc).to be_a(Post)
        end
      end

      it "does not alter the loaded docs" do
        expect(enumerable._loaded).to eq({ post.id => post })
      end

      it "stays loaded" do
        expect(enumerable).to be__loaded
      end
    end

    context "when a criteria and added exist" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      let(:post_two) do
        Post.new
      end

      context "when the added does not contain unloaded docs" do

        before do
          enumerable << post_two
        end

        let!(:iterated) do
          enumerable.each do |doc|
            expect(doc).to be_a(Post)
          end
        end

        it "adds the unloaded to the loaded docs" do
          expect(enumerable._loaded).to eq({ post.id => post })
        end

        it "keeps the appended in the added docs" do
          expect(enumerable._added).to eq({ post_two.id => post_two })
        end

        it "stays loaded" do
          expect(enumerable).to be__loaded
        end
      end

      context "when the added contains unloaded docs" do

        before do
          enumerable << post
        end

        let!(:iterated) do
          enumerable.each do |doc|
            expect(doc).to be_a(Post)
          end
        end

        it "adds the persisted added doc to the loaded" do
          expect(enumerable._loaded).to eq({ post.id => post })
        end

        it "stays loaded" do
          expect(enumerable).to be__loaded
        end
      end
    end

    context "when no block is passed" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      it "returns an enumerator" do
        expect(enumerable.each.class.include?(Enumerable)).to be true
      end

    end
  end

  describe "#entries" do

    let(:person) do
      Person.create
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:enumerable) do
      described_class.new(criteria)
    end

    context "when the added contains a persisted document" do

      let!(:post) do
        Post.create(person_id: person.id)
      end

      before do
        enumerable << post
      end

      let(:entries) do
        enumerable.entries
      end

      it "yields to the in memory documents first" do
        expect(entries.first).to equal(post)
      end
    end
  end

  describe "#first" do

    let(:person) do
      Person.create
    end

    context "when the enumerable is not loaded" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

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

      context "when unloaded is not empty" do

        context "when added is empty" do

          let!(:post) do
            Post.create(person_id: person.id)
          end

          let(:first) do
            enumerable.first
          end

          it "returns the first unloaded doc" do
            expect(first).to eq(post)
          end

          it "does not load the enumerable" do
            expect(enumerable).to_not be__loaded
          end

          it "receives query only once" do
            expect(criteria).to receive(:first).once
            first
          end
        end

        context "when added is not empty" do

          let!(:post) do
            Post.create(person_id: person.id)
          end

          let(:post_two) do
            Post.new(person_id: person.id)
          end

          before do
            enumerable << post_two
          end

          let(:first) do
            enumerable.first
          end

          context "when a perviously persisted unloaded doc exists" do

            it "returns the first added doc" do
              expect(first).to eq(post)
            end

            it "does not load the enumerable" do
              expect(enumerable).to_not be__loaded
            end
          end
        end
      end

      context "when unloaded is empty" do

        let!(:post) do
          Post.new(person_id: person.id)
        end

        before do
          enumerable << post
        end

        let(:first) do
          enumerable.first
        end

        it "returns the first loaded doc" do
          expect(first).to eq(post)
        end

        it "does not load the enumerable" do
          expect(enumerable).to_not be__loaded
        end
      end

      context "when unloaded and added are empty" do

        let(:first) do
          enumerable.first
        end

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

        it "does not load the enumerable" do
          expect(enumerable).to_not be__loaded
        end
      end
    end

    context "when the enumerable is loaded" do

      context "when loaded is not empty" do

        let!(:post) do
          Post.create(person_id: person.id)
        end

        let(:enumerable) do
          described_class.new([ post ])
        end

        let(:first) do
          enumerable.first
        end

        it "returns the first loaded doc" do
          expect(first).to eq(post)
        end
      end

      context "when loaded is empty" do

        let!(:post) do
          Post.create(person_id: person.id)
        end

        let(:enumerable) do
          described_class.new([])
        end

        before do
          enumerable << post
        end

        let(:first) do
          enumerable.first
        end

        it "returns the first added doc" do
          expect(first).to eq(post)
        end
      end

      context "when loaded and added are empty" do

        let(:enumerable) do
          described_class.new([])
        end

        let(:first) do
          enumerable.first
        end

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

    context 'when the id_sort option is none' do

      let(:person) do
        Person.create
      end

      let(:criteria) do
        Post.where(person_id: person.id)
      end

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

      let!(:first_post) do
        person.posts.create(title: "One")
      end

      let!(:second_post) do
        person.posts.create(title: "Two")
      end

      it 'does not use the sort on id' do
        expect(enumerable.first(id_sort: :none)).to eq(first_post)
      end
    end

    context 'when the id_sort option is not provided' do

      let(:person) do
        Person.create
      end

      let(:criteria) do
        Post.where(person_id: person.id)
      end

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

      let!(:first_post) do
        person.posts.create(title: "One")
      end

      let!(:second_post) do
        person.posts.create(title: "Two")
      end

      it 'uses the sort on id' do
        expect(enumerable.first).to eq(first_post)
      end
    end
  end

  describe "#include?" do

    let(:person) do
      Person.create
    end

    let!(:post_one) do
      Post.create(person_id: person.id)
    end

    let!(:post_two) do
      Post.create(person_id: person.id)
    end

    context "when no criteria exists" do

      context "when the enumerable is loaded" do

        let!(:enumerable) do
          described_class.new([ post_one, post_two ])
        end

        let!(:included) do
          enumerable.include?(post_two)
        end

        it "returns true" do
          expect(included).to be true
        end

        it "retains the correct length" do
          expect(enumerable.length).to eq(2)
        end

        it "retains the correct length when calling to_a" do
          expect(enumerable.to_a.length).to eq(2)
        end
      end

      context "when the enumerable contains an added document" do

        let!(:enumerable) do
          described_class.new([])
        end

        let(:post_three) do
          Post.new(person_id: person)
        end

        before do
          enumerable.push(post_three)
        end

        let!(:included) do
          enumerable.include?(post_three)
        end

        it "returns true" do
          expect(included).to be true
        end
      end
    end

    context "when the document is present and not the first" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

      let!(:enumerable) do
        described_class.new(criteria)
      end

      let!(:included) do
        enumerable.include?(post_two)
      end

      it "returns true" do
        expect(included).to be true
      end

      it "retains the correct length" do
        expect(enumerable.length).to eq(2)
      end

      it "retains the correct length when calling to_a" do
        expect(enumerable.to_a.length).to eq(2)
      end

      context "when iterating over the relation a second time" do

        before do
          enumerable.each { |post| post }
        end

        it "retains the correct length" do
          expect(enumerable.length).to eq(2)
        end

        it "retains the correct length when calling to_a" do
          expect(enumerable.to_a.length).to eq(2)
        end
      end
    end
  end

  describe "#initialize" do

    let(:person) do
      Person.new
    end

    context "when provided with a criteria" do

      let(:criteria) do
        Post.where(person_id: person.id)
      end

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

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

      it "is not loaded" do
        expect(enumerable).to_not be__loaded
      end
    end

    context "when provided an array" do

      let(:post) do
        Post.new
      end

      let(:enumerable) do
        described_class.new([ post ])
      end

      it "does not set a criteria" do
        expect(enumerable._unloaded).to be_nil
      end

      it "is loaded" do
        expect(enumerable).to be__loaded
      end
    end
  end

  describe "#in_memory" do

    let(:person) do
      Person.new
    end

    context "when the enumerable is loaded" do

      let(:post) do
        Post.new
      end

      let(:enumerable) do
        described_class.new([ post ])
      end

      let(:post_two) do
        Post.new
      end

      before do
        enumerable << post_two
      end

      let(:in_memory) do
        enumerable.in_memory
      end

      it "returns the loaded and added docs" do
        expect(in_memory).to eq([ post, post_two ])
      end
    end

    context "when the enumerable is not loaded" do

      let(:post) do
        Post.new(person_id: person.id)
      end

      let(:enumerable) do
        described_class.new(Post.where(person_id: person.id))
      end

      let(:post_two) do
        Post.new(person_id: person.id)
      end

      before do
        enumerable << post_two
      end

      let(:in_memory) do
        enumerable.in_memory
      end

      it "returns the added docs" do
        expect(in_memory).to eq([ post_two ])
      end
    end

    context "when passed a block" do

      let(:enumerable) do
        described_class.new(Post.where(person_id: person.id))
      end

      let(:post_two) do
        Post.new(person_id: person.id)
      end

      before do
        enumerable << post_two
      end

      it "yields to each in memory document" do
        enumerable.in_memory do |doc|
          expect(doc).to eq(post_two)
        end
      end
    end
  end

  describe "#is_a?" do

    let(:enumerable) do
      described_class.new(Post.all)
    end

    context "when checking against enumerable" do

      it "returns true" do
        expect(enumerable.is_a?(::Enumerable)).to be true
      end
    end

    context "when checking against array" do

      it "returns true" do
        expect(enumerable.is_a?(Array)).to be true
      end
    end
  end

  describe "#last" do

    let(:person) do
      Person.create
    end

    context "when the enumerable is not loaded" do

      let(:criteria) do
        Post.asc(:_id).where(person_id: person.id)
      end

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

      context "when unloaded is not empty" do

        let!(:post) do
          Post.create(person_id: person.id)
        end

        let(:last) do
          enumerable.last
        end

        it "returns the last unloaded doc" do
          expect(last).to eq(post)
        end

        it "does not load the enumerable" do
          expect(enumerable).to_not be__loaded
        end

        it "receives query only once" do
          expect(criteria).to receive(:last).once
          last
        end
      end

      context "when unloaded is empty" do

        let!(:post) do
          Post.new(person_id: person.id)
        end

        before do
          enumerable << post
        end

        let(:last) do
          enumerable.last
        end

        it "returns the last unloaded doc" do
          expect(last).to eq(post)
        end

        it "does not load the enumerable" do
          expect(enumerable).to_not be__loaded
        end
      end

      context "when unloaded and added are empty" do

        let(:last) do
          enumerable.last
        end

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

        it "does not load the enumerable" do
          expect(enumerable).to_not be__loaded
        end
      end

      context "when added is not empty" do

        let!(:post_one) do
          person.posts.create
        end

        let!(:post_two) do
          person.posts.create
        end

        let(:last) do
          enumerable.last
        end

        context "when accessing from a reloaded child" do

          it "returns the last document" do
            expect(post_one.reload.person.posts.asc(:_id).last).to eq(post_two)
          end
        end
      end
    end

    context "when the enumerable is loaded" do

      context "when loaded is not empty" do

        let!(:post) do
          Post.create(person_id: person.id)
        end

        let(:enumerable) do
          described_class.new([ post ])
        end

        let(:last) do
          enumerable.last
        end

        it "returns the last loaded doc" do
          expect(last).to eq(post)
        end
      end

      context "when loaded is empty" do

        let!(:post) do
          Post.create(person_id: person.id)
        end

        let(:enumerable) do
          described_class.new([])
        end

        before do
          enumerable << post
        end

        let(:last) do
          enumerable.last
        end

        it "returns the last added doc" do
          expect(last).to eq(post)
        end
      end

      context "when loaded and added are empty" do

        let(:enumerable) do
          described_class.new([])
        end

        let(:last) do
          enumerable.last
        end

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

    context 'when the id_sort option is none' do

      let(:person) do
        Person.create
      end

      let(:criteria) do
        Post.where(person_id: person.id)
      end

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

      let!(:first_post) do
        person.posts.create(title: "One")
      end

      let!(:second_post) do
        person.posts.create(title: "Two")
      end

      it 'does not use the sort on id' do
        expect(enumerable.last(id_sort: :none)).to eq(first_post)
      end
    end

    context 'when the id_sort option is not provided' do

      let(:person) do
        Person.create
      end

      let(:criteria) do
        Post.where(person_id: person.id)
      end

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

      let!(:first_post) do
        person.posts.create(title: "One")
      end

      let!(:second_post) do
        person.posts.create(title: "Two")
      end

      it 'uses the sort on id' do
        expect(enumerable.last).to eq(second_post)
      end
    end
  end

  describe "#kind_of?" do

    let(:enumerable) do
      described_class.new(Post.all)
    end

    context "when checking against enumerable" do

      it "returns true" do
        expect(enumerable.kind_of?(::Enumerable)).to be true
      end
    end

    context "when checking against array" do

      it "returns true" do
        expect(enumerable.kind_of?(Array)).to be true
      end
    end
  end

  describe "#load_all!" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:enumerable) do
      described_class.new(criteria)
    end

    let!(:loaded) do
      enumerable.load_all!
    end

    it "loads all the unloaded documents" do
      expect(enumerable._loaded).to eq({ post.id => post })
    end

    it "returns the object" do
      expect(loaded).to eq([post])
    end

    it "sets loaded to true" do
      expect(enumerable).to be__loaded
    end
  end

  describe "#reset" do

    let(:person) do
      Person.create
    end

    let(:post) do
      Post.create(person_id: person.id)
    end

    let(:post_two) do
      Post.create(person_id: person.id)
    end

    let(:enumerable) do
      described_class.new([ post ])
    end

    before do
      enumerable << post_two
    end

    let!(:reset) do
      enumerable.reset
    end

    it "is not loaded" do
      expect(enumerable).to_not be__loaded
    end

    it "clears out the loaded docs" do
      expect(enumerable._loaded).to be_empty
    end

    it "clears out the added docs" do
      expect(enumerable._added).to be_empty
    end
  end

  describe "#respond_to?" do

    let(:enumerable) do
      described_class.new([])
    end

    context "when checking against array methods" do

      [].methods.each do |method|

        it "returns true for #{method}" do
          expect(enumerable).to respond_to(method)
        end
      end
    end
  end

  describe "#size" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(person_id: person.id)
    end

    context "when the base is new" do

      let!(:person) do
        Person.new
      end

      context "when the added contains a persisted document" do

        let!(:post) do
          Post.create(person_id: person.id)
        end

        context "when the enumerable is not loaded" do

          let(:enumerable) do
            described_class.new(Post.where(person_id: person.id))
          end

          it "includes the number of all added documents" do
            expect(enumerable.size).to eq(1)
          end
        end
      end
    end

    context "when the enumerable is loaded" do

      let(:enumerable) do
        described_class.new([ post ])
      end

      let(:post_two) do
        Post.new(person_id: person.id)
      end

      before do
        enumerable << post_two
      end

      let(:size) do
        enumerable.size
      end

      it "returns the loaded size plus added size" do
        expect(size).to eq(2)
      end

      it "matches the size of the loaded enumerable" do
        expect(size).to eq(enumerable.to_a.size)
      end
    end

    context "when the enumerable is not loaded" do

      let(:enumerable) do
        described_class.new(Post.where(person_id: person.id))
      end

      context "when the added contains new documents" do

        let(:post_two) do
          Post.new(person_id: person.id)
        end

        before do
          enumerable << post_two
        end

        let(:size) do
          enumerable.size
        end

        it "returns the unloaded count plus added new size" do
          expect(size).to eq(2)
        end
      end

      context "when the added contains persisted documents" do

        let(:post_two) do
          Post.create(person_id: person.id)
        end

        before do
          enumerable << post_two
        end

        let(:size) do
          enumerable.size
        end

        it "returns the unloaded count plus added new size" do
          expect(size).to eq(2)
        end
      end
    end
  end

  describe "#to_json" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(title: "test", person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:enumerable) do
      described_class.new(criteria)
    end

    before do
      enumerable << post
    end

    let!(:json) do
      enumerable.to_json
    end

    it "serializes the enumerable" do
      expect(json).to include(post.title)
    end
  end

  describe "#to_json(parameters)" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(title: "test", person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:json) do
      person.posts.to_json({except: 'title'})
    end

    it "serializes the enumerable" do
      expect(json).to_not include(post.title)
    end
  end

  describe "#as_json" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(title: "test", person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:enumerable) do
      described_class.new(criteria)
    end

    before do
      enumerable << post
    end

    let!(:json) do
      enumerable.as_json
    end

    it "serializes the enumerable" do
      expect(json.size).to eq(1)
      expect(json[0]['title']).to eq(post.title)
    end
  end

  describe "#as_json(parameters)" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(title: "test", person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:json) do
      person.posts.as_json({except: "title"})
    end

    it "serializes the enumerable" do
      expect(json.size).to eq(1)
    end

    it "includes the proper fields" do
      expect(json[0].keys).to_not include("title")
    end
  end

  describe "#uniq" do

    let(:person) do
      Person.create
    end

    let!(:post) do
      Post.create(person_id: person.id)
    end

    let(:criteria) do
      Post.where(person_id: person.id)
    end

    let!(:enumerable) do
      described_class.new(criteria)
    end

    before do
      enumerable << post
      enumerable._loaded[post.id] = post
    end

    let!(:uniq) do
      enumerable.uniq
    end

    it "returns the unique documents" do
      expect(uniq).to eq([ post ])
    end

    it "sets loaded to true" do
      expect(enumerable).to be__loaded
    end
  end

  describe 'setting the same parent object on enumerated children objects' do

    let(:person) do
      Person.create
    end

    context 'when a single child is fetched' do

      let!(:post) do
        person.posts << Post.new
        person.posts.first
      end

      it 'does not query the database to access the parent' do
        expect_query(0) do
          expect(post.person).to eq(person)
        end
      end
    end

    context 'when a single child is fetched with a scope' do

      let!(:post) do
        person.posts << Post.new(title: 'open')
        person.posts.open.first
      end

      it 'does not query the database to access the parent' do
        expect_query(0) do
          expect(post.person).to eq(person)
        end
      end
    end

    context 'when multiple children are fetched' do

      let!(:posts) do
        person.posts << Post.new
        person.posts << Post.new
        person.posts << Post.new
        person.posts.to_a
      end

      it 'does not query the database to access the parent' do
        expect_query(0) do
          expect(posts.all? { |post| post.person == person }).to be(true)
        end
      end
    end

    context 'when multiple children are fetched with query criteria' do

      let!(:posts) do
        person.posts << Post.new(title: 'open')
        person.posts << Post.new(title: 'open')
        person.posts << Post.new(title: 'not-a-test')
        person.posts.where(title: 'open').to_a
      end

      it 'does not query the database to access the parent' do
        expect_query(0) do
          expect(posts.all? { |post| post.person == person }).to be(true)
        end
      end
    end

    context 'when multiple children are fetched with a scope' do

      let!(:posts) do
        person.posts << Post.new(title: 'open')
        person.posts << Post.new(title: 'open')
        person.posts << Post.new(title: 'not-a-test')
        person.posts.open.to_a
      end

      it 'does not query the database to access the parent' do
        expect_query(0) do
          expect(posts.all? { |post| post.person == person }).to be(true)
        end
      end
    end

    context 'when the parent is updated in memory' do

      let!(:posts) do
        person.posts << Post.new
        person.posts << Post.new
        person.username = 'emily'
        person.posts.to_a
      end

      it 'does not query the database to access the parent' do
        expect_query(0) do
          expect(posts.all? { |post| post.person.username == 'emily' }).to be(true)
        end
      end
    end
  end
end