# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"

describe Mongoid::Association::Accessors do

  describe "\#{getter}?" do

    let(:person) do
      Person.create!
    end

    context "when the association is a has one" do

      context "when the association exists" do

        let!(:game) do
          person.build_game
        end

        it "returns true" do
          expect(person).to have_game
        end
      end

      context "when the association does not exist" do

        context "when not autobuilding" do

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

        context "when autobuilding" do

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

    context "when the association is a has many" do

      context "when the association has documents" do

        let!(:post) do
          person.posts.build
        end

        it "returns true" do
          expect(person).to have_posts
        end
      end

      context "when the association does not have documents" do

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

    context "when the association is a has and belongs to many" do

      context "when the association has documents" do

        let!(:preference) do
          person.preferences.build
        end

        it "returns true" do
          expect(person).to have_preferences
        end
      end

      context "when the association does not have documents" do

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

    context "when the association is a belongs to" do

      context "when the association is named next" do

        let(:user) do
          User.create!
        end

        it "allows the existence check" do
          expect(user).to_not have_next
        end
      end

      context "when the association exists" do

        let!(:game) do
          person.build_game
        end

        it "returns true" do
          expect(game).to have_person
        end
      end

      context "when the association does not exist" do

        context "when the association does not autobuild" do

          let(:game) do
            Game.new
          end

          it "returns false" do
            expect(game).to_not have_person
          end
        end

        context "when the association autobuilds" do

          let(:book) do
            Book.new
          end

          it "returns false" do
            expect(book).to_not have_person
          end
        end
      end
    end

    context "when the association is an embeds one" do

      context "when the association exists" do

        let!(:name) do
          person.build_name
        end

        it "returns true" do
          expect(person).to have_name
        end
      end

      context "when the association does not exist" do

        context "when the association does not autobuild" do

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

        context "when the association autobuilds" do

          let(:person) do
            Person.new
          end

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

      context "when the association is set to nil first" do
        let!(:name) do
          person.build_name
        end

        it "returns true" do
          person.name = nil
          person.name = name
          expect(person).to have_name
        end
      end
    end

    context "when the association is an embeds many" do

      context "when the association has documents" do

        let!(:address) do
          person.addresses.build
        end

        it "returns true" do
          expect(person).to have_addresses
        end
      end

      context "when the association does not have documents" do

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

    context "when the association is an embedded in" do

      context "when the association exists" do

        let!(:name) do
          person.build_name
        end

        it "returns true" do
          expect(name).to have_namable
        end
      end

      context "when the association does not exist" do

        context "when the association does not autobuild" do

          let(:name) do
            Name.new
          end

          it "returns false" do
            expect(name).to_not have_namable
          end
        end

        context "when the association autobuilds" do

          let(:passport) do
            Passport.new
          end

          it "returns false" do
            expect(passport).to_not have_person
          end
        end
      end
    end
  end

  describe "\#{getter}" do

    let(:person) do
      Person.new
    end

    context "when autobuilding the association" do

      context "when the association is an embeds one" do

        context "when the association does not exist" do

          let!(:passport) do
            person.passport
          end

          it "builds the new document" do
            expect(passport).to be_a(Passport)
          end

          it "stores in the altered attribute" do
            expect(person.as_document["pass"]).to eq(passport.attributes)
          end
        end

        context "when the association exists" do

          let!(:passport) do
            person.build_passport(number: "123123321")
          end

          it "does not build a new document" do
            expect(person.passport).to eq(passport)
          end

        end
      end

      context "when the association is an embedded in" do

        let(:passport) do
          Passport.new
        end

        context "when the association does not exist" do

          let(:person) do
            passport.person
          end

          it "builds the new document" do
            expect(person).to be_a(Person)
          end
        end

        context "when the association exists" do

          let!(:person) do
            passport.build_person(title: "sir")
          end

          it "does not build a new document" do
            expect(passport.person).to eq(person)
          end
        end
      end

      context "when the association is a has one" do

        context "when the association does not exist" do

          let(:book) do
            person.book
          end

          it "builds the new document" do
            expect(book).to be_a(Book)
          end
        end

        context "when the association exists" do

          let!(:book) do
            person.build_book(title: "art of war")
          end

          it "does not build a new document" do
            expect(person.book).to eq(book)
          end
        end
      end

      context "when the association is a belongs to" do

        let(:book) do
          Book.new
        end

        context "when the association does not exist" do

          let(:person) do
            book.person
          end

          it "builds the new document" do
            expect(person).to be_a(Person)
          end
        end

        context "when the association exists" do

          let!(:person) do
            book.build_person(title: "sir")
          end

          it "does not build a new document" do
            expect(book.person).to eq(person)
          end
        end
      end
    end

    context "when the association is not polymorphic" do

      let(:person) do
        Person.create!
      end

      context "when the association is a many to many" do

        let!(:preference) do
          Preference.create!(name: "Setting")
        end

        before do
          person.preferences << Preference.last
        end

        context "when reloading the association directly" do

          let(:preferences) do
            person.preferences(true)
          end

          it "reloads the correct documents" do
            expect(preferences).to eq([ preference ])
          end

          it "reloads a new instance" do
            expect(preferences.first).to_not equal(preference)
          end
        end

        context "when reloading via the base document" do

          let(:preferences) do
            person.reload.preferences
          end

          it "reloads the correct documents" do
            expect(preferences).to eq([ preference ])
          end

          it "reloads a new instance" do
            expect(preferences.first).to_not equal(preference)
          end
        end

        context "when performing a fresh find on the base" do

          let(:preferences) do
            Person.find(person.id).preferences
          end

          it "reloads the correct documents" do
            expect(preferences).to eq([ preference ])
          end
        end
      end

      context "when the association is a many to one" do

        let!(:post) do
          Post.create!(title: "First!")
        end

        before do
          person.posts << Post.last
        end

        context "when reloading the association directly" do

          let(:posts) do
            person.posts(true)
          end

          it "reloads the correct documents" do
            expect(posts).to eq([ post ])
          end

          it "reloads a new instance" do
            expect(posts.first).to_not equal(post)
          end
        end

        context "when reloading via the base document" do

          let(:posts) do
            person.reload.posts
          end

          it "reloads the correct documents" do
            expect(posts).to eq([ post ])
          end

          it "reloads a new instance" do
            expect(posts.first).to_not equal(post)
          end
        end

        context "when performing a fresh find on the base" do

          let(:posts) do
            Person.find(person.id).posts
          end

          it "reloads the correct documents" do
            expect(posts).to eq([ post ])
          end
        end
      end

      context "when the association is a references one" do

        let!(:game) do
          Game.create!(name: "Centipeded")
        end

        before do
          person.game = Game.last
        end

        context "when reloading the association directly" do

          let(:reloaded_game) do
            person.game(true)
          end

          it "reloads the correct documents" do
            expect(reloaded_game).to eq(game)
          end

          it "reloads a new instance" do
            expect(reloaded_game).to_not equal(game)
          end
        end

        context "when reloading via the base document" do

          let(:reloaded_game) do
            person.reload.game
          end

          it "reloads the correct documents" do
            expect(reloaded_game).to eq(game)
          end

          it "reloads a new instance" do
            expect(reloaded_game).to_not equal(game)
          end
        end

        context "when performing a fresh find on the base" do

          let(:reloaded_game) do
            Person.find(person.id).game
          end

          it "reloads the correct documents" do
            expect(reloaded_game).to eq(game)
          end
        end
      end
    end

    context "when the association is polymorphic" do

      context "when there's a single references many/one" do

        let!(:movie) do
          Movie.create!(title: "Inception")
        end

        let!(:book) do
          Book.create!(title: "Jurassic Park")
        end

        let!(:movie_rating) do
          movie.ratings.create!(value: 10)
        end

        let!(:book_rating) do
          book.create_rating(value: 5)
        end

        context "when accessing a referenced in" do

          let!(:rating) do
            Rating.where(value: 10).first
          end

          it "returns the correct document" do
            expect(rating.ratable).to eq(movie)
          end
        end

        context "when accessing a references many" do

          let(:ratings) do
            Movie.first.ratings
          end

          it "returns the correct documents" do
            expect(ratings).to eq([ movie_rating ])
          end
        end

        context "when accessing a references one" do

          let!(:rating) do
            Book.find(book.id).rating
          end

          it "returns the correct document" do
            expect(rating).to eq(book_rating)
          end
        end
      end
    end

    context 'when projecting' do
      context 'embeds_one' do

        let!(:person) do
          Person.create!(passport: Passport.new(number: "123123321", country: "USA"))
        end

        context "when the record is queried with the embedded association projected" do
          let(:persisted_person) { Person.only(:passport).first }

          it 'creates an accessor for the projected embedded document' do
            expect(persisted_person.passport.number).to eq("123123321")
            expect(persisted_person.passport.country).to eq("USA")
          end
        end

        context 'when the record is queried with a field on the embedded association projected' do
          let(:persisted_person) { Person.only("pass.number").first }

          it 'creates an accessor for the projected field on the embedded document' do
            expect(persisted_person.passport.number).to eq("123123321")
          end

          it 'does not create an accessor for another field on the embedded document' do
            expect do
              persisted_person.passport.country
            end.to raise_error(Mongoid::Errors::AttributeNotLoaded, /Attempted to access attribute 'country' on Passport which was not loaded/)
          end
        end

        context 'when projecting association and a field in association' do
          shared_examples 'is prohibited on 4.4+ server' do
            context '4.4 server and higher' do
              min_server_version '4.4'

              it 'is not allowed by server' do
                lambda do
                  persisted_person
                end.should raise_error(Mongo::Error::OperationFailure, /Path collision at pass/)
              end
            end
          end

          context 'association then field' do
            let(:persisted_person) { Person.only(:pass, "pass.number").first }

            context '4.2 server and lower' do
              max_server_version '4.2'

              it 'creates an accessor for the projected field on the embedded document' do
                expect(persisted_person.passport.number).to eq("123123321")
              end

              it 'does not create an accessor for another field on the embedded document' do
                expect do
                  persisted_person.passport.country
                end.to raise_error(Mongoid::Errors::AttributeNotLoaded, /Attempted to access attribute 'country' on Passport which was not loaded/)
              end
            end

            include_examples 'is prohibited on 4.4+ server'
          end

          context 'field then association' do
            let(:persisted_person) { Person.only('pass.number', :pass).first }

            context '4.2 server and lower' do
              max_server_version '4.2'

              it 'creates an accessor for the projected field on the embedded document' do
                expect(persisted_person.passport.number).to eq("123123321")
              end

              it 'retrieves other fields' do
                persisted_person.passport.country.should == 'USA'
              end
            end

            include_examples 'is prohibited on 4.4+ server'
          end
        end
      end

      context 'embeds_many' do

        let!(:person) do
          Person.create!(phone_numbers: [
            Phone.new(number: '111-111-1111', landline: true),
          ])
        end

        context 'when the record is queried with the embedded association projected' do
          let(:persisted_person) { Person.only(:phone_numbers).first }

          it 'creates an accessor for the embedded document' do
            expect(persisted_person.phone_numbers.first.number).to eq('111-111-1111')
            expect(persisted_person.phone_numbers.first.landline).to be true
          end
        end

        shared_examples 'allows access to field of projected association' do
          it 'creates an accessor for the embedded document' do
            expect(persisted_person.phone_numbers.first).to be_a_kind_of(Phone)
          end

          it 'creates an accessor for the projected field on the embedded document' do
            expect(persisted_person.phone_numbers.first.number).to eq('111-111-1111')
          end
        end

        context 'when the record is queried with a field on the embedded association projected' do
          let(:persisted_person) { Person.only("phone_numbers.number").first }

          include_examples 'allows access to field of projected association'

          it 'does not create an accessor for another field on the embedded document' do
            expect do
              persisted_person.phone_numbers.first.landline
            end.to raise_error(Mongoid::Errors::AttributeNotLoaded, /Attempted to access attribute 'landline' on Phone which was not loaded/)
          end
        end

        context 'when projecting association and a field in association' do

          shared_examples 'is prohibited on 4.4+ server' do
            context '4.4 server and higher' do
              min_server_version '4.4'

              it 'is not allowed by server' do
                lambda do
                  persisted_person
                end.should raise_error(Mongo::Error::OperationFailure, /Path collision at phone_numbers/)
              end
            end
          end

          context 'association then field' do
            let(:persisted_person) { Person.only(:phone_numbers, 'phone_numbers.number').first }

            context '4.2 server and lower' do
              max_server_version '4.2'

              include_examples 'allows access to field of projected association'

              it 'does not create an accessor for another field on the embedded document' do
                expect do
                  persisted_person.phone_numbers.first.landline
                end.to raise_error(Mongoid::Errors::AttributeNotLoaded, /Attempted to access attribute 'landline' on Phone which was not loaded/)
              end

            end

            include_examples 'is prohibited on 4.4+ server'
          end

          context 'field then association' do
            let(:persisted_person) { Person.only('phone_numbers.number', :phone_numbers).first }

            context '4.2 server and lower' do
              max_server_version '4.2'

              include_examples 'allows access to field of projected association'

              it 'retrieves all fields of association' do
                persisted_person.phone_numbers.first.landline.should be true
              end
            end

            include_examples 'is prohibited on 4.4+ server'
          end
        end
      end
    end
  end

  context "when setting associations to empty values" do

    context "when the document is a referenced in" do

      let(:post) do
        Post.new
      end

      context "when setting the association directly" do

        before do
          post.person = ""
        end

        it "converts them to nil" do
          expect(post.person).to be_nil
        end
      end

      context "when setting the foreign key" do

        before do
          post.person_id = ""
        end

        it "converts it to nil" do
          expect(post.person_id).to be_nil
        end
      end
    end

    context "when the document is a references one" do

      let(:person) do
        Person.new
      end

      context "when setting the association directly" do

        before do
          person.game = ""
        end

        it "converts them to nil" do
          expect(person.game).to be_nil
        end
      end

      context "when setting the foreign key" do

        let(:game) do
          Game.new
        end

        before do
          game.person_id = ""
        end

        it "converts it to nil" do
          expect(game.person_id).to be_nil
        end
      end
    end

    context "when the document is a references many" do

      let(:person) do
        Person.new
      end

      context "when setting the foreign key" do

        let(:post) do
          Post.new
        end

        before do
          post.person_id = ""
        end

        it "converts it to nil" do
          expect(post.person).to be_nil
        end
      end

      context "when setting the _ids accessor" do

        let(:post) do
          Post.create!
        end

        before do
          person.post_ids = [ "" ]
        end

        it "ignore blank values" do
          expect(person.post_ids).to be_empty
        end
      end
    end

    context "when the document is a references many to many" do

      let(:person) do
        Person.new
      end

      context "when setting the foreign key" do

        before do
          person.preference_ids = [ "", "" ]
        end

        it "does not add them" do
          expect(person.preference_ids).to be_empty
        end
      end
    end
  end

  context 'when setting associations to nil values' do
    context 'when the document embeds one' do
      let(:definitions) do
        class AccessorEmbedding
          include Mongoid::Document

          embeds_one :accessor_embedded
        end

        class AccessorEmbedded
          include Mongoid::Document

          embedded_in :accessor_embedding
        end

        define_embedded
      end

      let(:embedding) do
        AccessorEmbedding.create!
      end

      let(:define_embedded) do
        AccessorEmbedded.create!(accessor_embedding: embedding)
      end

      it 'successfully removes the embedded document' do
        definitions

        expect(embedding.accessor_embedded).not_to be_nil

        embedding.accessor_embedded = nil
        embedding.save!

        expect(embedding.reload.accessor_embedded).to be_nil
      end
    end
  end

  context "when setting association foreign keys" do

    let(:game) do
      Game.new
    end

    let(:person) do
      Person.create!
    end

    context "when value is an empty string" do

      before do
        game.person_id = ""
        game.save!
      end

      it "sets the foreign key to empty" do
        expect(game.reload.person_id).to be_blank
      end
    end

    context "when value is a populated string" do

      before do
        game.person_id = person.id.to_s
        game.save!
      end

      it "sets the foreign key as ObjectID" do
        expect(game.reload.person_id).to eq(person.id)
      end
    end

    context "when value is a ObjectID" do

      before do
        game.person_id = person.id
        game.save!
      end

      it "keeps the foreign key as ObjectID" do
        expect(game.reload.person_id).to eq(person.id)
      end
    end

    context "when setting ids multiple times on the association itself" do

      before do
        game.person = person.id
        game.person = person.id
      end

      it "sets the association foreign key" do
        expect(game.person_id).to eq(person.id)
      end

      it "sets the appropriate association" do
        expect(game.person).to eq(person)
      end
    end
  end

  context 'when setting the association more than once' do

    let(:person) do
      Person.create!
    end

    context 'when the association is a references one' do

      let(:game) do
        Game.create!
      end

      before do
        person.game = game
        game.person = person
      end

      it 'allows the object to be set twice' do
        expect(person.game = game).to be(game)
      end
    end

    context 'when the association is a references many' do

      let!(:preference) do
        Preference.create!(name: "Setting")
      end

      before do
        person.preferences << Preference.last
      end

      it 'allows the object to be set twice' do
        expect(person.preferences << Preference.last).to be_a(Array)
      end
    end

    context 'when the association is an embeds one' do

      let!(:name) do
        Name.new
      end

      before do
        person.name = name
        name.person = person
      end

      it 'allows the object to be set twice' do
        expect(person.name = name).to be(name)
      end
    end

    context 'when the association is an embeds many' do

      let!(:address) do
        Address.new
      end

      before do
        person.addresses << address
      end

      it 'allows the object to be set twice' do
        expect(person.addresses << address).to be_a(Array)
      end
    end
  end
end
