# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"

describe Mongoid::Association::Macros do

  class TestClass
    include Mongoid::Document
  end

  let(:klass) do
    TestClass
  end

  before do
    klass.relations.clear
    klass._validators.clear
  end

  after do
    klass.relations.clear
    klass.validators.clear
  end

  describe 'Model loading' do

    let(:model_associations) do
      class TestModel
        include Mongoid::Document
        field :associations
      end
    end

    after do
      Object.send(:remove_const, :TestModel)
    end

    it 'prohibits the use of :associations as an attribute' do
      expect {
        model_associations
      }.to raise_exception(Mongoid::Errors::InvalidField)
    end
  end

  describe ".embedded_in" do

    it "defines the macro" do
      expect(klass).to respond_to(:embedded_in)
    end

    context 'when the relation name is invalid' do

      let(:relation) do
        klass.embedded_in(:fields)
      end

      it 'raises an InvalidRelation exception' do
        expect {
          relation
        }.to raise_exception(Mongoid::Errors::InvalidRelation)
      end
    end

    context "when defining the relation" do

      before do
        klass.embedded_in(:person)
      end

      it "adds the association to the klass" do
        expect(klass.relations["person"]).to_not be_nil
      end

      it "defines the getter" do
        expect(klass.allocate).to respond_to(:person)
      end

      it "defines the setter" do
        expect(klass.allocate).to respond_to(:person=)
      end

      it "creates the correct relation" do
        expect(klass.relations["person"]).to be_a(
          Mongoid::Association::Embedded::EmbeddedIn
        )
      end

      it "does not add associated validations" do
        expect(klass._validators).to be_empty
      end

      context 'when an invalid option is provided' do

        it 'raises an InvalidRelationOption exception' do
          expect {
            klass.embedded_in(:person, sandwich: true)
          }.to raise_exception(Mongoid::Errors::InvalidRelationOption)
        end
      end

      context "association properties" do

        let(:association) do
          klass.relations["person"]
        end

        it "automatically adds the name" do
          expect(association.name).to eq(:person)
        end

        it "automatically adds the inverse class name" do
          expect(association.inverse_class_name).to eq("TestClass")
        end
      end
    end
  end

  describe ".embeds_many" do

    it "defines the macro" do
      expect(klass).to respond_to(:embeds_many)
    end

    context "when defining the relation" do

      context 'when the relation name is invalid' do

        let(:relation) do
          klass.embeds_many(:fields)
        end

        it 'raises an InvalidRelation exception' do
          expect {
            relation
          }.to raise_exception(Mongoid::Errors::InvalidRelation)
        end
      end

      before do
        klass.embeds_many(:addresses)
      end

      it "adds the association to the klass" do
        expect(klass.relations["addresses"]).to_not be_nil
      end

      it "defines the getter" do
        expect(klass.allocate).to respond_to(:addresses)
      end

      it "defines the setter" do
        expect(klass.allocate).to respond_to(:addresses=)
      end

      it "creates the correct relation" do
        expect(klass.relations["addresses"]).to be_a(
          Mongoid::Association::Embedded::EmbedsMany
        )
      end

      it "adds an associated validation" do
        expect(klass._validators[:addresses].first).to be_a(
          Mongoid::Validatable::AssociatedValidator
        )
      end

      context 'when an invalid option is provided' do

        it 'raises an InvalidRelationOption exception' do
          expect {
            klass.embeds_many(:addresses, sandwich: true)
          }.to raise_exception(Mongoid::Errors::InvalidRelationOption)
        end
      end

      context "association properties" do

        let(:association) do
          klass.relations["addresses"]
        end

        it "automatically adds the name" do
          expect(association.name).to eq(:addresses)
        end

        it "automatically adds the inverse class name" do
          expect(association.inverse_class_name).to eq("TestClass")
        end
      end
    end

    context 'when defining order on relation' do

      before do
        klass.embeds_many(:addresses, order: :number.asc)
      end

      let(:association) do
        klass.relations["addresses"]
      end

      it "adds association to klass" do
        expect(association.order).to_not be_nil
      end

      it "returns Mongoid::Criteria::Queryable::Key" do
        expect(association.order).to be_kind_of(Mongoid::Criteria::Queryable::Key)
      end
    end

    context "when setting validate to false" do

      before do
        klass.embeds_many(:addresses, validate: false)
      end

      it "does not add associated validations" do
        expect(klass._validators).to be_empty
      end
    end
  end

  describe ".embeds_one" do

    it "defines the macro" do
      expect(klass).to respond_to(:embeds_one)
    end

    context "when defining the relation" do

      context 'when the relation name is invalid' do

        let(:relation) do
          klass.embeds_one(:fields)
        end

        it 'raises an InvalidRelation exception' do
          expect {
            relation
          }.to raise_exception(Mongoid::Errors::InvalidRelation)
        end
      end

      before do
        klass.embeds_one(:name)
      end

      it "adds the association to the klass" do
        expect(klass.relations["name"]).to_not be_nil
      end

      it "defines the getter" do
        expect(klass.allocate).to respond_to(:name)
      end

      it "defines the setter" do
        expect(klass.allocate).to respond_to(:name=)
      end

      it "defines the builder" do
        expect(klass.allocate).to respond_to(:build_name)
      end

      it "defines the creator" do
        expect(klass.allocate).to respond_to(:create_name)
      end

      it "creates the correct relation" do
        expect(klass.relations["name"]).to be_a(
          Mongoid::Association::Embedded::EmbedsOne
        )
      end

      it "adds an associated validation" do
        expect(klass._validators[:name].first).to be_a(
          Mongoid::Validatable::AssociatedValidator
        )
      end

      context 'when an invalid option is provided' do

        it 'raises an InvalidRelationOption exception' do
          expect {
            klass.embeds_one(:name, sandwich: true)
          }.to raise_exception(Mongoid::Errors::InvalidRelationOption)
        end
      end

      context "association properties" do

        let(:association) do
          klass.relations["name"]
        end

        it "automatically adds the name" do
          expect(association.name).to eq(:name)
        end

        it "automatically adds the inverse class name" do
          expect(association.inverse_class_name).to eq("TestClass")
        end
      end
    end

    context "when setting validate to false" do

      before do
        klass.embeds_one(:name, validate: false)
      end

      it "does not add associated validations" do
        expect(klass._validators).to be_empty
      end
    end
  end

  describe ".belongs_to" do

    let(:_class) do
      class RelationsTestClass
        include Mongoid::Document
      end
    end

    let(:conf) do
      CONFIG.merge(options: { belongs_to_required_by_default: default_require })
    end

    let(:relation) do
      _class.new
    end

    let(:relation_options) { {} }

    let(:default_require) { Mongoid.belongs_to_required_by_default }

    before do
      Mongoid.configure do |config|
        config.load_configuration(conf)
      end
      _class.belongs_to(:person, relation_options)
    end

    after do
      Object.send(:remove_const, _class.name)
    end

    after(:all) do
      Mongoid.configure do |config|
        config.load_configuration(CONFIG)
      end
    end

    it "defines the macro" do
      expect(_class).to respond_to(:belongs_to)
    end

    context 'when the relation has options' do

      context 'when an invalid option is provided' do

        it 'raises an InvalidRelationOption exception' do
          expect {
            klass.belongs_to(:person, sandwich: true)
          }.to raise_exception(Mongoid::Errors::InvalidRelationOption)
        end
      end

      context 'when the relation has the option :required' do

        context 'when the relation does not have the :optional option' do

          context 'when :required is true' do

            let(:relation_options) do
              { required: true }
            end

            context 'when the default config is to require the association' do

              let(:default_require) { true }

              it 'requires the association' do
                expect(relation.save).to be(false)
              end
            end

            context 'when the default config is to not require the association' do

              let(:default_require) { false }

              it 'requires the association' do
                expect(relation.save).to be(false)
              end
            end
          end

          context 'when :required is false' do

            let(:relation_options) do
              { required: false }
            end

            context 'when the default config is to require the association' do

              let(:default_require) { true }

              it 'does not require the association' do
                expect(relation.save!).to be(true)
              end
            end

            context 'when the default config is to not require the association' do

              let(:default_require) { false }

              it 'does not require the association' do
                expect(relation.save!).to be(true)
              end
            end
          end
        end

        context 'when the relation has the option :optional' do

          context 'when :required is true' do

            context 'when :optional is true' do

              let(:relation_options) do
                {
                  required: true,
                  optional: true
                }
              end

              context 'when the default config is to require the association' do

                let(:default_require) { true }

                it 'requires the association' do
                  expect(relation.save).to be(false)
                end
              end

              context 'when the default config is to not require the association' do

                let(:default_require) { false }

                it 'requires the association' do
                  expect(relation.save).to be(false)
                end
              end
            end

            context 'when :optional is false' do

              let(:relation_options) do
                {
                  required: true,
                  optional: false
                }
              end

              context 'when the default config is to require the association' do

                let(:default_require) { true }

                it 'requires the association' do
                  expect(relation.save).to be(false)
                end
              end

              context 'when the default config is to not require the association' do

                let(:default_require) { false }

                it 'requires the association' do
                  expect(relation.save).to be(false)
                end
              end
            end
          end

          context 'when :required is false' do

            context 'when :optional is true' do

              let(:relation_options) do
                {
                  required: false,
                  optional: true
                }
              end

              context 'when the default config is to require the association' do

                let(:default_require) { true }

                it 'does not require the association' do
                  expect(relation.save!).to be(true)
                end
              end

              context 'when the default config is to not require the association' do

                let(:default_require) { true }

                it 'does not require the association' do
                  expect(relation.save!).to be(true)
                end
              end
            end

            context 'when :optional is false' do

              let(:relation_options) do
                {
                  required: false,
                  optional: false
                }
              end

              context 'when the default config is to require the association' do

                let(:default_require) { true }

                it 'does not require the association' do
                  expect(relation.save!).to be(true)
                end
              end

              context 'when the default config is to not require the association' do

                let(:default_require) { false }

                it 'does not require the association' do
                  expect(relation.save!).to be(true)
                end
              end
            end
          end
        end
      end

      context 'when the relation does not have the option :required' do

        context 'when the relation has the option :optional' do

          context 'when :optional is true' do

            let(:relation_options) do
              { optional: true }
            end

            context 'when the default config is to require the association' do

              let(:default_require) { true }

              it 'does not require the association' do
                expect(relation.save!).to be(true)
              end
            end

            context 'when the default config is to not require the association' do

              let(:default_require) { false }

              it 'does not require the association' do
                expect(relation.save!).to be(true)
              end
            end
          end

          context 'when :optional is false' do

            let(:relation_options) do
              { optional: false }
            end

            context 'when the default config is to require the association' do

              let(:default_require) { true }

              it 'requires the association' do
                expect(relation.save).to be(false)
              end
            end

            context 'when the default config is to not require the association' do

              let(:default_require) { false }

              it 'requires the association' do
                expect(relation.save).to be(false)
              end
            end
          end
        end
      end
    end

    context 'when the relation does not have options' do

      context 'when the default config is to require the association' do

        let(:default_require) { true }

        it 'requires the association' do
          expect(relation.save).to be(false)
        end
      end

      context 'when the default config is to not require the association' do

        let(:default_require) { false }

        it 'does not require the association' do
          expect(relation.save!).to be(true)
        end
      end
    end

    context "when the relation is polymorphic" do

      context "when indexed is true" do

        before do
          klass.belongs_to(:relatable, polymorphic: true, index: true)
        end

        let(:index) do
          klass.index_specifications.first
        end

        it "adds the background index to the definitions" do
          expect(index.key).to eq(relatable_id: 1, relatable_type: 1)
        end
      end
    end

    context "when defining the relation" do

      context 'when the relation name is invalid' do

        let(:relation) do
          klass.belongs_to(:fields)
        end

        it 'raises an InvalidRelation exception' do
          expect {
            relation
          }.to raise_exception(Mongoid::Errors::InvalidRelation)
        end
      end

      before do
        klass.belongs_to(:person)
      end

      it "adds the association to the klass" do
        expect(klass.relations["person"]).to_not be_nil
      end

      it "defines the getter" do
        expect(klass.allocate).to respond_to(:person)
      end

      it "defines the setter" do
        expect(klass.allocate).to respond_to(:person=)
      end

      it "defines the builder" do
        expect(klass.allocate).to respond_to(:build_person)
      end

      it "defines the creator" do
        expect(klass.allocate).to respond_to(:create_person)
      end

      it "creates the correct relation" do
        expect(klass.relations["person"]).to be_a(
          Mongoid::Association::Referenced::BelongsTo
        )
      end

      it "creates the field for the foreign key" do
        expect(klass.allocate).to respond_to(:person_id)
      end

      context "association properties" do

        let(:association) do
          klass.relations["person"]
        end

        it "automatically adds the name" do
          expect(association.name).to eq(:person)
        end

        it "automatically adds the inverse class name" do
          expect(association.inverse_class_name).to eq("TestClass")
        end
      end
    end
  end

  describe ".has_many" do

    it "defines the macro" do
      expect(klass).to respond_to(:has_many)
    end

    context "when defining the relation" do

      context 'when the relation name is invalid' do

        let(:relation) do
          klass.has_many(:fields)
        end

        it 'raises an InvalidRelation exception' do
          expect {
            relation
          }.to raise_exception(Mongoid::Errors::InvalidRelation)
        end
      end

      before do
        klass.has_many(:posts)
      end

      it "adds the association to the klass" do
        expect(klass.relations["posts"]).to_not be_nil
      end

      it "defines the getter" do
        expect(klass.allocate).to respond_to(:posts)
      end

      it "defines the setter" do
        expect(klass.allocate).to respond_to(:posts=)
      end

      it "creates the correct relation" do
        expect(klass.relations["posts"]).to be_a(
          Mongoid::Association::Referenced::HasMany
        )
      end

      it "adds an associated validation" do
        expect(klass._validators[:posts].first).to be_a(
          Mongoid::Validatable::AssociatedValidator
        )
      end

      context "association properties" do

        let(:association) do
          klass.relations["posts"]
        end

        it "automatically adds the name" do
          expect(association.name).to eq(:posts)
        end

        it "automatically adds the inverse class name" do
          expect(association.inverse_class_name).to eq("TestClass")
        end
      end

      context 'when an invalid option is provided' do

        it 'raises an InvalidRelationOption exception' do
          expect {
            klass.has_many(:names, sandwich: true)
          }.to raise_exception(Mongoid::Errors::InvalidRelationOption)
        end
      end
    end

    context 'when defining order on relation' do

      before do
        klass.has_many(:posts, order: :rating.asc)
      end

      let(:association) do
        klass.relations["posts"]
      end

      it "adds association to klass" do
        expect(association.order).to_not be_nil
      end

      it "returns Mongoid::Criteria::Queryable::Key" do
        expect(association.order).to be_kind_of(Mongoid::Criteria::Queryable::Key)
      end
    end

    context "when setting validate to false" do

      before do
        klass.has_many(:posts, validate: false)
      end

      it "does not add associated validations" do
        expect(klass._validators).to be_empty
      end
    end
  end

  describe ".has_and_belongs_to_many" do

    it "defines the macro" do
      expect(klass).to respond_to(:has_and_belongs_to_many)
    end

    context "when defining the relation" do

      context 'when the relation name is invalid' do

        let(:relation) do
          klass.has_and_belongs_to_many(:fields)
        end

        it 'raises an InvalidRelation exception' do
          expect {
            relation
          }.to raise_exception(Mongoid::Errors::InvalidRelation)
        end
      end

      context 'when an invalid option is provided' do

        it 'raises an InvalidRelationOption exception' do
          expect {
            klass.has_and_belongs_to_many(:fields, sandwich: true)
          }.to raise_exception(Mongoid::Errors::InvalidRelationOption)
        end
      end

      before do
        klass.has_and_belongs_to_many(:preferences)
      end

      it "adds the association to the klass" do
        expect(klass.relations["preferences"]).to_not be_nil
      end

      it "defines the getter" do
        expect(klass.allocate).to respond_to(:preferences)
      end

      it "defines the setter" do
        expect(klass.allocate).to respond_to(:preferences=)
      end

      it "creates the correct relation" do
        expect(klass.relations["preferences"]).to be_a(
          Mongoid::Association::Referenced::HasAndBelongsToMany
        )
      end

      it "creates the field for the foreign key" do
        expect(klass.allocate).to respond_to(:preference_ids)
      end

      context 'when defining order on relation' do

        before do
          klass.has_and_belongs_to_many(:preferences, order: :ranking.asc)
        end

        let(:association) do
          klass.relations["preferences"]
        end

        it "adds association to klass" do
          expect(association.order).to_not be_nil
        end

        it "returns Mongoid::Criteria::Queryable::Key" do
          expect(association.order).to be_kind_of(Mongoid::Criteria::Queryable::Key)
        end
      end

      context "association properties" do

        let(:association) do
          klass.relations["preferences"]
        end

        it "automatically adds the name" do
          expect(association.name).to eq(:preferences)
        end

        it "automatically adds the inverse class name" do
          expect(association.inverse_class_name).to eq("TestClass")
        end
      end
    end
  end

  describe ".has_one" do

    it "defines the macro" do
      expect(klass).to respond_to(:has_one)
    end

    context "when defining the relation" do

      context 'when the relation name is invalid' do

        let(:relation) do
          klass.has_one(:fields)
        end

        it 'raises an InvalidRelation exception' do
          expect {
            relation
          }.to raise_exception(Mongoid::Errors::InvalidRelation)
        end
      end

      context 'when an invalid option is provided' do

        it 'raises an InvalidRelationOption exception' do
          expect {
            klass.has_one(:game, sandwich: true)
          }.to raise_exception(Mongoid::Errors::InvalidRelationOption)
        end
      end

      before do
        klass.has_one(:game)
      end

      it "adds the association to the klass" do
        expect(klass.relations["game"]).to_not be_nil
      end

      it "defines the getter" do
        expect(klass.allocate).to respond_to(:game)
      end

      it "defines the setter" do
        expect(klass.allocate).to respond_to(:game=)
      end

      it "defines the builder" do
        expect(klass.allocate).to respond_to(:build_game)
      end

      it "defines the creator" do
        expect(klass.allocate).to respond_to(:create_game)
      end

      it "creates the correct relation" do
        expect(klass.relations["game"]).to be_a(
          Mongoid::Association::Referenced::HasOne
        )
      end

      it "adds an associated validation" do
        expect(klass._validators[:game].first).to be_a(
          Mongoid::Validatable::AssociatedValidator
        )
      end

      context "association properties" do

        let(:association) do
          klass.relations["game"]
        end

        it "automatically adds the name" do
          expect(association.name).to eq(:game)
        end

        it "automatically adds the inverse class name" do
          expect(association.inverse_class_name).to eq("TestClass")
        end
      end
    end

    context "when setting validate to false" do

      before do
        klass.has_one(:game, validate: false)
      end

      it "does not add associated validations" do
        expect(klass._validators).to be_empty
      end
    end
  end

  describe "#relations" do

    before do
      klass.embeds_one(:name)
    end

    it "returns a bson document of relations" do
      expect(klass.allocate.relations).to be_a_kind_of(BSON::Document)
    end

    it "has keys that are the relation name" do
      expect(klass.allocate.relations['name']).to be_a(Mongoid::Association::Embedded::EmbedsOne)
    end

    it "has keys that can be accessed by a String" do
      expect(klass.allocate.relations['name']).to be_a(Mongoid::Association::Embedded::EmbedsOne)
    end

    it "has keys that can be accessed by a Symbol" do
      expect(klass.allocate.relations[:name]).to be_a(Mongoid::Association::Embedded::EmbedsOne)
    end

    it "has values that are association" do
      expect(
        klass.allocate.relations.values.first
      ).to be_a_kind_of(Mongoid::Association::Embedded::EmbedsOne)
    end
  end

  describe ".relations" do

    before do
      klass.embeds_one(:name)
    end

    it "has keys that are the relation name" do
      expect(klass.allocate.relations['name']).to be_a(Mongoid::Association::Embedded::EmbedsOne)
    end

    it "has keys that can be accessed by a String" do
      expect(klass.allocate.relations['name']).to be_a(Mongoid::Association::Embedded::EmbedsOne)
    end

    it "has keys that can be accessed by a Symbol" do
      expect(klass.allocate.relations[:name]).to be_a(Mongoid::Association::Embedded::EmbedsOne)
    end

    it "has values that are association" do
      expect(
        klass.relations.values.first
      ).to be_a(Mongoid::Association::Embedded::EmbedsOne)
    end
  end

  context "when creating an association with an extension" do

    class Peep
      include Mongoid::Document
    end

    class Handle
      include Mongoid::Document

      module Extension
        def short_name
          "spec"
        end
      end

      module AnotherExtension
        def very_short_name
          "spec"
        end
      end
    end

    let(:peep) do
      Peep.new(handle: Handle.new)
    end

    context "when the extension is a block" do

      before do
        Peep.embeds_one(:handle) do
          def full_name
            "spec"
          end
        end
      end

      it "extends the relation" do
        expect(peep.handle.full_name).to eq("spec")
      end
    end

    context "when the extension is a module" do

      before do
        Peep.embeds_one(:handle, extend: Handle::Extension)
      end

      it "extends the relation" do
        expect(peep.handle.short_name).to eq("spec")
      end
    end

    context "when the extension is two modules" do

      before do
        Peep.embeds_one(:handle, extend: [ Handle::Extension, Handle::AnotherExtension ])
      end

      it "extends the relation" do
        expect(peep.handle.very_short_name).to eq("spec")
      end
    end
  end
end
