# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"
require 'support/models/canvas'
require_relative '../belongs_to_models.rb'

describe Mongoid::Association::Referenced::BelongsTo::Proxy do

  before(:all) do
    Person.reset_callbacks(:validate)
  end

  let(:person) do
    Person.create!
  end

  describe "#=" do

    context "when the relation is named target" do

      let(:target) do
        User.new
      end

      context "when the relation is referenced from an embeds many" do

        context "when setting via create" do

          let(:service) do
            person.services.create(target: target)
          end

          it "sets the target relation" do
            expect(service.target).to eq(target)
          end
        end
      end
    end

    context "when the inverse relation has no reference defined" do

      let(:agent) do
        Agent.new(title: "007")
      end

      let(:game) do
        Game.new(name: "Donkey Kong")
      end

      before do
        agent.game = game
      end

      it "sets the relation" do
        expect(agent.game).to eq(game)
      end

      it "sets the foreign_key" do
        expect(agent.game_id).to eq(game.id)
      end
    end

    context "when referencing a document from an embedded document" do

      let(:person) do
        Person.create!
      end

      let(:address) do
        person.addresses.create!(street: "Wienerstr")
      end

      let(:account) do
        Account.create!(name: "1", number: 1000000)
      end

      before do
        address.account = account
      end

      it "sets the relation" do
        expect(address.account).to eq(account)
      end

      it "does not erase the association metadata" do
        expect(address._association).to_not be_nil
      end

      it "allows saving of the embedded document" do
        expect(address.save!).to be true
      end
    end

    context "when the parent is a references one" do

      context "when the relation is not polymorphic" do

        context "when the child is a new record" do

          let(:person) do
            Person.new
          end

          let(:game) do
            Game.new
          end

          before do
            game.person = person
          end

          it "sets the target of the relation" do
            expect(game.person._target).to eq(person)
          end

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

          it "sets the base on the inverse relation" do
            expect(person.game).to eq(game)
          end

          it "sets the same instance on the inverse relation" do
            expect(person.game).to eql(game)
          end

          it "does not save the target" do
            expect(person).to_not be_persisted
          end

          it "drops private visibility from private methods" do
            # This is unfortunate but it appears that Ruby does not
            # have a way to distinguish how method_missing was invoked
            # (i.e. via an explicit send or method call).
            # See https://jira.mongodb.org/browse/MONGOID-5009
            game.person.secret_name.should == 'secret'
          end

          it "allows private methods to be invoked" do
            game.person.send(:secret_name).should == 'secret'
          end

          it "properly exposes delegated methods visibility" do
            expect(defined?(game.person.id)).to eq("method")
            expect(defined?(game.person.secret_name)).to be_nil
          end
        end

        context "when the child is not a new record" do

          let(:person) do
            Person.new
          end

          let(:game) do
            Game.create!
          end

          before do
            game.person = person
          end

          it "sets the target of the relation" do
            expect(game.person._target).to eq(person)
          end

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

          it "sets the base on the inverse relation" do
            expect(person.game).to eq(game)
          end

          it "sets the same instance on the inverse relation" do
            expect(person.game).to eql(game)
          end

          it "does not saves the target" do
            expect(person).to_not be_persisted
          end
        end
      end

      context "when the relation is polymorphic" do

        context "when the parent is a subclass" do

          let(:canvas) do
            Canvas::Test.create!
          end

          let(:comment) do
            Comment.create!(title: "test")
          end

          before do
            comment.commentable = canvas
            comment.save!
          end

          it "sets the correct value in the type field" do
            expect(comment.commentable_type).to eq("Canvas::Test")
          end

          it "can retrieve the document from the database" do
            expect(comment.reload.commentable).to eq(canvas)
          end
        end

        context "when the child is a new record" do

          let(:bar) do
            Bar.new
          end

          let(:rating) do
            Rating.new
          end

          before do
            rating.ratable = bar
          end

          it "sets the target of the relation" do
            expect(rating.ratable._target).to eq(bar)
          end

          it "sets the foreign key on the relation" do
            expect(rating.ratable_id).to eq(bar.id)
          end

          it "sets the base on the inverse relation" do
            expect(bar.rating).to eq(rating)
          end

          it "sets the same instance on the inverse relation" do
            expect(bar.rating).to eql(rating)
          end

          it "does not save the target" do
            expect(bar).to_not be_persisted
          end
        end

        context "when the child is not a new record" do

          let(:bar) do
            Bar.new
          end

          let(:rating) do
            Rating.create!
          end

          before do
            rating.ratable = bar
          end

          it "sets the target of the relation" do
            expect(rating.ratable._target).to eq(bar)
          end

          it "sets the foreign key of the relation" do
            expect(rating.ratable_id).to eq(bar.id)
          end

          it "sets the base on the inverse relation" do
            expect(bar.rating).to eq(rating)
          end

          it "sets the same instance on the inverse relation" do
            expect(bar.rating).to eql(rating)
          end

          it "does not saves the target" do
            expect(bar).to_not be_persisted
          end
        end
      end
    end

    context "when the parent is a references many" do

      context "when the relation is not polymorphic" do

        context 'when the child has persistence options set' do

          before do
            Person.collection.client['other-posts'].delete_many
            Person.collection.client['other-people'].delete_many
          end

          let(:person) do
            Person.new
          end

          let(:post) do
            Post.new
          end

          before do
            post.with(collection: 'other-posts') do |post|
              person.with(collection: 'other-people') do |person|
                post.person = person
                person.save!
                post.save!
              end
            end
          end

          let(:other_people_count) do
            Person.with(collection: 'other-people') do |person_class|
              person_class.count
            end
          end

          let(:other_posts_count) do
            Post.with(collection: 'other-posts') do |post_class|
              post_class.count
            end
          end

          it 'applies the persistence options when saving the child' do
            expect(other_people_count).to eq(1)
            expect(other_posts_count).to eq(1)
          end
        end

        context "when the child is a new record" do

          let(:person) do
            Person.new
          end

          let(:post) do
            Post.new
          end

          before do
            post.person = person
          end

          it "sets the target of the relation" do
            expect(post.person._target).to eq(person)
          end

          it "sets the foreign key on the relation" do
            expect(post.person_id).to eq(person.id)
          end

          it "does not save the target" do
            expect(person).to_not be_persisted
          end
        end

        context "when the child is not a new record" do

          let(:person) do
            Person.new
          end

          let(:post) do
            Post.create!
          end

          before do
            post.person = person
          end

          it "sets the target of the relation" do
            expect(post.person._target).to eq(person)
          end

          it "sets the foreign key of the relation" do
            expect(post.person_id).to eq(person.id)
          end

          it "does not saves the target" do
            expect(person).to_not be_persisted
          end
        end
      end

      context "when the relation is polymorphic" do

        context "when multiple relations against the same class exist" do

          let(:face) do
            Face.new
          end

          let(:eye) do
            Eye.new
          end

          it "raises an error" do
            expect {
              eye.eyeable = face
            }.to raise_error(Mongoid::Errors::InvalidSetPolymorphicRelation)
          end
        end

        context "when multiple relations of the same name but different class exist" do

          let(:eye) do
            Eye.new
          end

          let(:eye_bowl) do
            EyeBowl.new
          end

          it "should assign as expected" do
            eye.suspended_in = eye_bowl
            expect(eye.suspended_in._target).to eq(eye_bowl)
          end
        end

        context "when one relation against the same class exists" do

          context "when the child is a new record" do

            let(:movie) do
              Movie.new
            end

            let(:rating) do
              Rating.new
            end

            before do
              rating.ratable = movie
            end

            it "sets the target of the relation" do
              expect(rating.ratable._target).to eq(movie)
            end

            it "sets the foreign key on the relation" do
              expect(rating.ratable_id).to eq(movie.id)
            end

            it "does not save the target" do
              expect(movie).to_not be_persisted
            end
          end

          context "when the child is not a new record" do

            let(:movie) do
              Movie.new
            end

            let(:rating) do
              Rating.create!
            end

            before do
              rating.ratable = movie
            end

            it "sets the target of the relation" do
              expect(rating.ratable._target).to eq(movie)
            end

            it "sets the foreign key of the relation" do
              expect(rating.ratable_id).to eq(movie.id)
            end

            it "does not saves the target" do
              expect(movie).to_not be_persisted
            end
          end
        end
      end
    end
  end

  describe "#= nil" do

    context "when dependent is destroy" do

      let(:account) do
        Account.create!(name: 'checkings')
      end

      let(:drug) do
        Drug.create!
      end

      let(:person) do
        Person.create!
      end

      context "when relation is has_one" do

        around(:each) do |example|
          original_account_dependents = Account.dependents
          Account.dependents = []
          example.run
          Account.dependents = original_account_dependents
        end

        before do
          Account.belongs_to :person, dependent: :destroy
          Person.has_one :account
          person.account = account
          person.save!
        end

        after :all do
          Account.belongs_to :person, dependent: :nullify
          Person.has_one :account, validate: false
        end

        context "when parent exists" do

          context "when child touch the parent" do

            let!(:account_from_db) { account.reload }

            it "queries only the parent" do
              expect_query(1) do
                expect(account_from_db.person.id).to eq(person.id)
              end
            end
          end

          context "when child is destroyed" do

            before do
              account.destroy
            end

            it "deletes child" do
              expect(account).to be_destroyed
            end

            it "deletes parent" do
              expect(person).to be_destroyed
            end
          end
        end
      end

      context "when relation is has_many" do

        around(:each) do |example|
          original_drug_dependents = Drug.dependents
          Drug.dependents = []
          example.run
          Drug.dependents = original_drug_dependents
        end

        before do
          Drug.belongs_to :person, dependent: :destroy
          Person.has_many :drugs
          person.drugs = [drug]
          person.save!
        end

        after :all do
          Drug.belongs_to :person, dependent: :nullify
          Person.has_many :drugs, validate: false
        end

        context "when parent exists" do

          context "when child is destroyed" do

            before do
              drug.destroy
            end

            it "deletes child" do
              expect(drug).to be_destroyed
            end

            it "deletes parent" do
              expect(person).to be_destroyed
            end
          end
        end
      end
    end

    context "when dependent is delete" do

      let(:account) do
        Account.create!(name: 'Foobar')
      end

      let(:drug) do
        Drug.create!
      end

      let(:person) do
        Person.create!
      end

      context "when relation is has_one" do

        around(:each) do |example|
          original_account_dependents = Account.dependents
          Account.dependents = []
          example.run
          Account.dependents = original_account_dependents
        end

        before do
          Account.belongs_to :person, dependent: :delete_all
          Person.has_one :account
          person.account = account
          person.save!
        end

        after :all do
          Account.belongs_to :person, dependent: :nullify
          Person.has_one :account, validate: false
        end

        context "when parent is persisted" do

          context "when child is destroyed" do

            before do
              account.destroy
            end

            it "deletes child" do
              expect(account).to be_destroyed
            end

            it "deletes parent" do
              expect(person).to be_destroyed
            end
          end
        end
      end

      context "when relation is has_many" do

        around(:each) do |example|
          original_drug_dependents = Drug.dependents
          Drug.dependents = []
          example.run
          Drug.dependents = original_drug_dependents
        end

        before do
          Drug.belongs_to :person, dependent: :delete_all
          Person.has_many :drugs
          person.drugs = [drug]
          person.save!
        end

        after :all do
          Drug.belongs_to :person, dependent: :nullify
          Person.has_many :drugs, validate: false
        end

        context "when parent exists" do

          context "when child is destroyed" do

            before do
              drug.destroy
            end

            it "deletes child" do
              expect(drug).to be_destroyed
            end

            it "deletes parent" do
              expect(person).to be_destroyed
            end
          end
        end
      end
    end

    context "when dependent is nullify" do

      let(:account) do
        Account.create!(name: 'Foobar')
      end

      let(:drug) do
        Drug.create!
      end

      let(:person) do
        Person.create!
      end

      context "when relation is has_one" do

        around(:each) do |example|
          original_account_dependents = Account.dependents
          Account.dependents = []
          example.run
          Account.dependents = original_account_dependents
        end

        before do
          Account.belongs_to :person, dependent: :nullify
          Person.has_one :account
          person.account = account
          person.save!
        end

        context "when parent is persisted" do

          context "when child is destroyed" do

            before do
              account.destroy
            end

            it "deletes child" do
              expect(account).to be_destroyed
            end

            it "doesn't delete parent" do
              expect(person).to_not be_destroyed
            end

            it "removes the link" do
              expect(person.account).to be_nil
            end
          end
        end
      end

      context "when relation is has_many" do

        around(:each) do |example|
          original_drug_dependents = Drug.dependents
          Drug.dependents = []
          example.run
          Drug.dependents = original_drug_dependents
        end

        before do
          Drug.belongs_to :person, dependent: :nullify
          Person.has_many :drugs
          person.drugs = [drug]
          person.save!
        end

        # NOTE: there as a bad interdependency here, with the auto_save_spec.rb
        # file. If auto_save_spec.rb runs before this, the following specs fail
        # with "undefined method `nullify' for an instance of Person".

        context "when parent exists" do

          context "when child is destroyed" do

            before do
              drug.destroy
            end

            it "deletes child" do
              expect(drug).to be_destroyed
            end

            it "doesn't deletes parent" do
              expect(person).to_not be_destroyed
            end

            it "removes the link" do
              expect(person.drugs).to eq([])
            end
          end
        end
      end
    end

    context "when the inverse relation has no reference defined" do

      let(:agent) do
        Agent.new(title: "007")
      end

      let(:game) do
        Game.new(name: "Donkey Kong")
      end

      before do
        agent.game = game
        agent.game = nil
      end

      it "removes the relation" do
        expect(agent.game).to be_nil
      end

      it "removes the foreign_key" do
        expect(agent.game_id).to be_nil
      end
    end

    context "when the parent is a references one" do

      context "when the relation is not polymorphic" do

        context "when the parent is a new record" do

          let(:person) do
            Person.new
          end

          let(:game) do
            Game.new
          end

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

          it "sets the relation to nil" do
            expect(game.person).to be_nil
          end

          it "removed the inverse relation" do
            expect(person.game).to be_nil
          end

          it "removes the foreign key value" do
            expect(game.person_id).to be_nil
          end
        end

        context "when the parent is not a new record" do

          let(:person) do
            Person.create!
          end

          let(:game) do
            Game.create!
          end

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

          it "sets the relation to nil" do
            expect(game.person).to be_nil
          end

          it "removed the inverse relation" do
            expect(person.game).to be_nil
          end

          it "removes the foreign key value" do
            expect(game.person_id).to be_nil
          end

          it "does not delete the child" do
            expect(game).to_not be_destroyed
          end
        end
      end

      context "when the relation is polymorphic" do

        context "when one relation against the same class exists" do

          context "when the parent is a new record" do

            let(:bar) do
              Bar.new
            end

            let(:rating) do
              Rating.new
            end

            before do
              rating.ratable = bar
              rating.ratable = nil
            end

            it "sets the relation to nil" do
              expect(rating.ratable).to be_nil
            end

            it "removed the inverse relation" do
              expect(bar.rating).to be_nil
            end

            it "removes the foreign key value" do
              expect(rating.ratable_id).to be_nil
            end
          end

          context "when the parent is not a new record" do

            let(:bar) do
              Bar.new
            end

            let(:rating) do
              Rating.create!
            end

            before do
              rating.ratable = bar
              rating.ratable = nil
            end

            it "sets the relation to nil" do
              expect(rating.ratable).to be_nil
            end

            it "removed the inverse relation" do
              expect(bar.rating).to be_nil
            end

            it "removes the foreign key value" do
              expect(rating.ratable_id).to be_nil
            end
          end
        end
      end
    end

    context "when the parent is a references many" do

      context "when the relation is not polymorphic" do

        context "when the parent is a new record" do

          let(:person) do
            Person.new
          end

          let(:post) do
            Post.new
          end

          before do
            post.person = person
            post.person = nil
          end

          it "sets the relation to nil" do
            expect(post.person).to be_nil
          end

          it "removed the inverse relation" do
            expect(person.posts).to be_empty
          end

          it "removes the foreign key value" do
            expect(post.person_id).to be_nil
          end
        end

        context "when the parent is not a new record" do

          let(:person) do
            Person.new
          end

          let(:post) do
            Post.create!
          end

          before do
            post.person = person
            post.person = nil
          end

          it "sets the relation to nil" do
            expect(post.person).to be_nil
          end

          it "removed the inverse relation" do
            expect(person.posts).to be_empty
          end

          it "removes the foreign key value" do
            expect(post.person_id).to be_nil
          end
        end
      end

      context "when the relation is polymorphic" do

        context "when the parent is a new record" do

          let(:movie) do
            Movie.new
          end

          let(:rating) do
            Rating.new
          end

          before do
            rating.ratable = movie
            rating.ratable = nil
          end

          it "sets the relation to nil" do
            expect(rating.ratable).to be_nil
          end

          it "removed the inverse relation" do
            expect(movie.ratings).to be_empty
          end

          it "removes the foreign key value" do
            expect(rating.ratable_id).to be_nil
          end
        end

        context "when the parent is not a new record" do

          let(:movie) do
            Movie.new
          end

          let(:rating) do
            Rating.create!
          end

          before do
            rating.ratable = movie
            rating.ratable = nil
          end

          it "sets the relation to nil" do
            expect(rating.ratable).to be_nil
          end

          it "removed the inverse relation" do
            expect(movie.ratings).to be_empty
          end

          it "removes the foreign key value" do
            expect(rating.ratable_id).to be_nil
          end
        end
      end
    end
  end

  describe "#respond_to?" do

    let(:person) do
      Person.new
    end

    let(:game) do
      person.build_game(name: "Tron")
    end

    let(:document) do
      game.person
    end

    Mongoid::Document.public_instance_methods(true).each do |method|

      context "when checking #{method}" do

        it "returns true" do
          expect(document.respond_to?(method)).to be true
        end
      end
    end
  end

  context "when the relation is self referencing" do

    let(:game_one) do
      Game.new(name: "Diablo")
    end

    let(:game_two) do
      Game.new(name: "Warcraft")
    end

    context "when setting the parent" do

      before do
        game_one.parent = game_two
      end

      it "sets the parent" do
        expect(game_one.parent).to eq(game_two)
      end

      it "does not set the parent recursively" do
        expect(game_two.parent).to be_nil
      end
    end
  end

  context "when the relation belongs to a has many and has one" do

    before(:all) do
      class A
        include Mongoid::Document
        has_many :bs, inverse_of: :a
      end

      class B
        include Mongoid::Document
        belongs_to :a, inverse_of: :bs
        belongs_to :c, inverse_of: :b
      end

      class C
        include Mongoid::Document
        has_one :b, inverse_of: :c
      end
    end

    after(:all) do
      Object.send(:remove_const, :A)
      Object.send(:remove_const, :B)
      Object.send(:remove_const, :C)
    end

    context "when setting the has one" do

      let(:a) do
        A.new
      end

      let(:b) do
        B.new
      end

      let(:c) do
        C.new
      end

      before do
        b.c = c
      end

      context "when subsequently setting the has many" do

        before do
          b.a = a
        end

        context "when setting the has one again" do

          before do
            b.c = c
          end

          it "allows the reset of the has one" do
            expect(b.c).to eq(c)
          end
        end
      end
    end
  end

  context "when replacing the relation with another" do

    let!(:person) do
      Person.create!
    end

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

    let!(:game) do
      person.create_game(name: "Tron")
    end

    before do
      post.person = game.person
      post.save!
    end

    it "clones the relation" do
      expect(post.person).to eq(person)
    end

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

    it "does not remove the previous relation" do
      expect(game.person).to eq(person)
    end

    it "does not remove the previous foreign key" do
      expect(game.person_id).to eq(person.id)
    end

    context "when reloading" do

      before do
        post.reload
        game.reload
      end

      it "persists the relation" do
        expect(post.reload.person).to eq(person)
      end

      it "persists the foreign key" do
        expect(post.reload.person_id).to eq(game.person_id)
      end

      it "does not remove the previous relation" do
        expect(game.person).to eq(person)
      end

      it "does not remove the previous foreign key" do
        expect(game.person_id).to eq(person.id)
      end
    end
  end

  context "when the document belongs to a has one and has many" do

    let(:movie) do
      Movie.create!(name: "Infernal Affairs")
    end

    let(:account) do
      Account.create!(name: "Leung")
    end

    context "when creating the document" do

      let(:comment) do
        Comment.create!(title: 'My Title', movie: movie, account: account)
      end

      it "sets the correct has one" do
        expect(comment.account).to eq(account)
      end

      it "sets the correct has many" do
        expect(comment.movie).to eq(movie)
      end
    end
  end

  context "when reloading the relation" do

    let!(:person_one) do
      Person.create!
    end

    let!(:person_two) do
      Person.create!(title: "Sir")
    end

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

    before do
      game.person = person_one
      game.save!
    end

    context "when the relation references the same document" do

      before do
        Person.collection.find({ _id: person_one.id }).
            update_one({ "$set" => { title: "Madam" }})
      end

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

      it "reloads the document from the database" do
        expect(reloaded.title).to eq("Madam")
      end

      it "sets a new document instance" do
        expect(reloaded).to_not equal(person_one)
      end
    end

    context "when the relation references a different document" do

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

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

      it "reloads the new document from the database" do
        expect(reloaded.title).to eq("Sir")
      end

      it "sets a new document instance" do
        expect(reloaded).to_not equal(person_one)
      end
    end
  end

  context "when creating with a reference to an integer id parent" do

    let!(:jar) do
      Jar.create! do |doc|
        doc._id = 1
      end
    end

    let(:cookie) do
      Cookie.create!(jar_id: "1")
    end

    it "allows strings to be passed as the id" do
      expect(cookie.jar).to eq(jar)
    end

    it "persists the relation" do
      expect(cookie.reload.jar).to eq(jar)
    end
  end

  context "when setting the relation via the foreign key" do

    context "when the relation exists" do

      let!(:person_one) do
        Person.create!
      end

      let!(:person_two) do
        Person.create!
      end

      let!(:game) do
        Game.create!(person: person_one)
      end

      before do
        game.person_id = person_two.id
      end

      it "sets the new document on the relation" do
        expect(game.person).to eq(person_two)
      end
    end
  end

  describe "#method_missing" do
      let!(:person) do
        Person.create!
      end

      let!(:game) do
        Game.create!(person: person)
      end

    it 'handles keyword args' do
      expect do
        game.person.set_personal_data(ssn: '123', age: 25)
      end.not_to raise_error
    end
  end

  # This is a very specific case, see MONGOID-5089 for more details.
  context "when required is false, child is an orphan, and parent has explicit _id" do
    let(:comment) { BTMComment.create! }
    let(:article) do
      BTMArticle.new(
        comment_ids: [comment.id],
        id: 1
      )
    end

    it "uses the correct explicit id" do
      expect(article.comments.first.article_id).to eq(1)
    end
  end
end
