# frozen_string_literal: true

require "spec_helper"
require_relative './embeds_one_models'

describe Mongoid::Association::Embedded::EmbedsOne do

  before do
    class Container; include Mongoid::Document; end
    class EmbeddedObject; include Mongoid::Document; end
  end

  after do
    Container.relations.clear
    EmbeddedObject.relations.clear
  end

  let(:embeds_one_class) do
    Container
  end

  let(:name) do
    :embedded_object
  end

  let(:association) do
    embeds_one_class.embeds_one name, options
  end

  let(:options) do
    { }
  end

  describe '#relation_complements' do

    let(:expected_complements) do
      [
        Mongoid::Association::Embedded::EmbeddedIn,
      ]
    end

    it 'returns the relation complements' do
      expect(association.send(:relation_complements)).to eq(expected_complements)
    end
  end

  describe '#setup!' do

    it 'sets up a getter for the relation' do
      expect(Mongoid::Association::Accessors).to receive(:define_getter!).with(association)
      association.setup!
    end

    it 'sets up a setter for the relation' do
      expect(Mongoid::Association::Accessors).to receive(:define_setter!).with(association)
      association.setup!
    end

    it 'sets up an existence check for the relation' do
      expect(Mongoid::Association::Accessors).to receive(:define_existence_check!).with(association)
      association.setup!
    end

    it 'sets up the builder for the relation' do
      expect(Mongoid::Association::Builders).to receive(:define_builder!).with(association)
      association.setup!
    end

    it 'sets up the creator for the relation' do
      expect(Mongoid::Association::Builders).to receive(:define_creator!).with(association)
      association.setup!
    end

    context 'when the :validate option is true' do

      let(:options) do
        {
            validate: true
        }
      end

      let(:association) do
        # Note that it is necessary to create the association directly, otherwise the
        # setup! method will be called by the :embeds_one macro
        described_class.new(embeds_one_class, name, options)
      end

      it 'sets up validation' do
        expect(embeds_one_class).to receive(:validates_associated).with(name).and_call_original
        association.setup!
      end
    end

    context 'when the :validate option is false' do

      let(:options) do
        {
            validate: false
        }
      end

      it 'does not set up validation' do
        expect(embeds_one_class).not_to receive(:validates_associated)
        association.setup!
      end
    end

    context 'when the :validate option is not provided' do

      let(:association) do
        # Note that it is necessary to create the association directly, otherwise the
        # setup! method will be called by the :embeds_one macro
        described_class.new(embeds_one_class, name, options)
      end

      it 'sets up the validation because it uses the validation default (true)' do
        expect(embeds_one_class).to receive(:validates_associated).with(name).and_call_original
        association.setup!
      end
    end
  end

  describe '#type' do

    context 'when polymorphic' do

      let(:options) do
        { as: :containable }
      end

      it 'returns the as attribute followed by "_type"' do
        expect(association.type).to eq("#{options[:as]}_type")
      end
    end

    context 'when not polymorphic' do

      it 'returns nil' do
        expect(association.type).to be_nil
      end
    end
  end

  describe '#inverse_type' do

    context 'when polymorphic' do

      let(:options) do
        { as: :containable }
      end

      it 'returns nil' do
        expect(association.inverse_type).to be_nil
      end
    end

    context 'when not polymorphic' do

      it 'returns nil' do
        expect(association.inverse_type).to be_nil
      end
    end
  end

  describe '#inverse_type_setter' do

    context 'when polymorphic' do

      let(:options) do
        { as: :containable }
      end

      it 'returns nil' do
        expect(association.inverse_type).to be_nil
      end
    end

    context 'when not polymorphic' do

      it 'returns nil' do
        expect(association.inverse_type).to be_nil
      end
    end
  end

  describe '#embedded?' do

    it 'returns true' do
      expect(association.embedded?).to be(true)
    end
  end

  describe '#primary_key' do

    it 'returns nil' do
      expect(association.primary_key).to be_nil
    end
  end

  describe '#indexed?' do

    it 'returns false' do
      expect(association.indexed?).to be(false)
    end
  end

  describe '#relation' do

    it 'returns Mongoid::Association::Embedded::EmbedsOne::Proxy' do
      expect(association.relation).to be(Mongoid::Association::Embedded::EmbedsOne::Proxy)
    end
  end

  describe '#validation_default' do

    it 'returns true' do
      expect(association.validation_default).to be(true)
    end
  end

  describe '#name' do

    it 'returns the name of the relation' do
      expect(association.name).to be(name)
    end
  end

  describe '#options' do

    it 'returns the options' do
      expect(association.options).to be(options)
    end
  end

  describe '#cyclic?' do

    context 'when :cyclic is specified in the options' do

      let(:options) do
        { cyclic: true }
      end

      it 'returns true' do
        expect(association.cyclic?).to be(true)
      end
    end

    context 'when :cyclic is not specified in the options' do

      it 'returns false' do
        expect(association.cyclic?).to be(false)
      end
    end
  end

  describe '#merge!' do

  end

  describe '#store_as' do

    context 'when :store_as is specified in the options' do

      let(:options) do
        {
          store_as: :baby_kangaroo
        }
      end

      it 'returns the :store_as option as a String' do
        expect(association.store_as).to eq(options[:store_as].to_s)
      end
    end

    context 'when :store_as is not specified in the options' do

      it 'returns name as a String' do
        expect(association.store_as).to eq(name.to_s)
      end
    end
  end

  describe '#touchable?' do

    it 'return false' do
      expect(association.send(:touchable?)).to be(false)
    end
  end

  describe '#order' do

    it 'returns nil' do
      expect(association.order).to be_nil
    end
  end

  describe '#as' do

    context 'when :as is specified in the options' do

      let(:options) do
        {
          as: :containable
        }
      end

      it 'returns the :as option' do
        expect(association.as).to eq(options[:as])
      end
    end

    context 'when :as is not specified in the options' do

      it 'returns nil' do
        expect(association.as).to be_nil
      end
    end
  end

  describe '#polymorphic?' do

    context 'when :as is specified in the options' do

      let(:options) do
        {
          as: :containable
        }
      end

      it 'returns true' do
        expect(association.polymorphic?).to be(true)
      end

    end

    context 'when :as is not specified in the options' do

      it 'returns false' do
        expect(association.polymorphic?).to be(false)
      end
    end
  end

  describe '#type_setter' do

    context 'when polymorphic' do

      let(:options) do
        { as: :containable }
      end

      it 'returns the type followed by = as a String' do
        expect(association.type_setter).to eq("containable_type=")
      end
    end

    context 'when not polymorphic' do

      it 'returns nil' do
        expect(association.type).to be_nil
      end
    end
  end

  describe '#dependent' do

    it 'returns nil' do
      expect(association.dependent).to be_nil
    end
  end

  describe '#inverse_type' do

    it 'returns nil' do
      expect(association.inverse_type).to be_nil
    end
  end

  describe '#bindable?' do

    it 'returns false' do
      expect(association.bindable?(Person.new)).to be(false)
    end
  end

  describe '#inverses' do

    context 'when polymorphic' do

      before do
        EmbeddedObject.embedded_in :containable, polymorphic: true
      end

      let(:options) do
        {
          as: :containable
        }
      end

      context 'when another object is passed to the method' do

        let(:instance_of_other_class) do
          EmbeddedObject.new
        end

        context 'when the relation class has only one relation whose class matches the owning class' do

          it 'returns the :as attribute of this association' do
            expect(association.inverses(instance_of_other_class)).to match_array([ :containable ])
          end
        end

        context 'when :inverse_of is specified' do

          before do
            options.merge!(inverse_of: :inverse_name)
          end

          it 'returns the :inverse_of value' do
            expect(association.inverses(instance_of_other_class)).to eq([ :inverse_name ])
          end
        end

        context 'when inverse_of is not specified' do

          it 'returns the :as attribute of this association' do
            expect(association.inverses(instance_of_other_class)).to match_array([ :containable ])
          end
        end
      end

      context 'when another object is not passed to the method' do

        context 'when inverse_of is specified' do

          before do
            options.merge!(inverse_of: :inverse_name)
          end

          it 'returns the :inverse_of value' do
            expect(association.inverses).to eq([ :inverse_name ])
          end
        end

        context 'when inverse_of is not specified' do

          it 'returns the :as attribute' do
            expect(association.inverses).to eq([ :containable ])
          end

          context 'when class_name is given and is a :: prefixed string' do

            let(:association) do
              EomCcParent.relations['child']
            end

            it 'returns the inverse in an array' do
              inverses = association.inverses
              expect(inverses).to eq([:parent])
            end

            context 'when other associations referencing unloaded classes exist' do
              let(:association) do
                EomDnlParent.relations['child']
              end

              it 'does not load other classes' do
                inverses = association.inverses
                expect(inverses).to eq([:parent])
                expect(Object.const_defined?(:EoDnlMarker)).to be false
              end
            end
          end
        end
      end
    end

    context 'when not polymorphic' do

      before do
        EmbeddedObject.embedded_in :container
      end

      context 'when inverse_of is specified' do

        before do
          options.merge!(inverse_of: :inverse_name)
        end

        it 'returns the :inverse_of value' do
          expect(association.inverses).to eq([ :inverse_name ])
        end
      end

      context 'when inverse_of is not specified' do

        it 'uses the inverse class to find the inverse name' do
          expect(association.inverses).to eq([ :container ])
        end
      end

      context 'when :cyclic is specified' do

        it 'returns the cyclic inverse name' do

        end
      end
    end
  end

  describe '##inverse' do

    context 'when polymorphic' do

      before do
        EmbeddedObject.embedded_in :containable, polymorphic: true
      end

      let(:options) do
        {
            as: :containable
        }
      end

      context 'when another object is passed to the method' do

        let(:instance_of_other_class) do
          EmbeddedObject.new
        end

        context 'when the relation class has only one relation whose class matches the owning class' do

          it 'returns the :as attribute of this association' do
            expect(association.inverse(instance_of_other_class)).to eq(:containable)
          end
        end

        context 'when :inverse_of is specified' do

          before do
            options.merge!(inverse_of: :inverse_name)
          end

          it 'returns the :inverse_of value' do
            expect(association.inverse(instance_of_other_class)).to eq(:inverse_name)
          end
        end

        context 'when inverse_of is not specified' do

          it 'returns the :as attribute of this association' do
            expect(association.inverse(instance_of_other_class)).to eq(:containable)
          end
        end
      end

      context 'when another object is not passed to the method' do

        context 'when inverse_of is specified' do

          before do
            options.merge!(inverse_of: :inverse_name)
          end

          it 'returns the :inverse_of value' do
            expect(association.inverse).to eq(:inverse_name)
          end
        end

        context 'when inverse_of is not specified' do

          it 'returns the :as attribute' do
            expect(association.inverse).to eq(:containable)
          end
        end
      end
    end

    context 'when not polymorphic' do

      before do
        EmbeddedObject.embedded_in :container
      end

      context 'when inverse_of is specified' do

        before do
          options.merge!(inverse_of: :inverse_name)
        end

        it 'returns the :inverse_of value' do
          expect(association.inverse).to eq(:inverse_name)
        end
      end

      context 'when inverse_of is not specified' do

        it 'uses the inverse class to find the inverse name' do
          expect(association.inverse).to eq(:container)
        end
      end

      context 'when :cyclic is specified' do

        it 'returns the cyclic inverse name' do

        end
      end
    end
  end

  describe '#inverse_association' do

  end

  describe '#autosave' do

    it 'returns false' do
      expect(association.autosave).to be(false)
    end
  end

  describe '#relation_class_name' do

    context 'when the :class_name option is specified' do

      let(:options) do
        { class_name: 'OtherEmbeddedObject' }
      end

      it 'returns the class name option' do
        expect(association.relation_class_name).to eq('OtherEmbeddedObject')
      end

      context ':class_name is a :: prefixed string' do
        let(:association) do
          EomCcParent.relations['child']
        end

        it 'returns the :: prefixed string' do
          expect(association.relation_class_name).to eq('::EomCcChild')
        end
      end
    end

    context 'when the class_name option is not specified' do

      it 'uses the name of the relation to deduce the class name' do
        expect(association.relation_class_name).to eq('EmbeddedObject')
      end
    end

    context 'when another association in the model is referencing a third model class' do
      let(:association) do
        EomDnlParent.relations['child']
      end

      it 'does not attempt to load the third class' do
        expect(association.relation_class_name).to eq('EomDnlChild')
        expect(Object.const_defined?(:EoDnlMarker)).to be false
      end
    end
  end

  describe '#klass' do

    context 'when the :class_name option is specified' do

      let!(:_class) do
        class OtherEmbeddedObject; end
        OtherEmbeddedObject
      end

      let(:options) do
        { class_name: 'OtherEmbeddedObject' }
      end

      it 'returns the class name option' do
        expect(association.klass).to eq(_class)
      end
    end

    context 'when the class_name option is not specified' do

      it 'uses the name of the relation to deduce the class name' do
        expect(association.klass).to eq(EmbeddedObject)
      end
    end
  end

  describe '#inverse_class_name' do

    it 'returns the name of the owner class' do
      expect(association.inverse_class_name).to eq(Container.name)
    end
  end

  describe '#inverse_class' do

    it 'returns the owner class' do
      expect(association.inverse_class).to be(Container)
    end
  end

  describe '#inverse_of' do

    context 'when :inverse_of is specified in the options' do

      let(:options) do
        { inverse_of: :objects_list }
      end

      it 'returns the inverse_of value' do
        expect(association.inverse_of).to eq(options[:inverse_of])
      end
    end

    context 'when :inverse_of is not specified in the options' do

      it 'returns nil' do
        expect(association.inverse_of).to be_nil
      end
    end
  end

  describe '#key' do

    it 'returns the name of the relation as a string' do
      expect(association.key).to eq(name.to_s)
    end
  end

  describe '#setter' do

    it 'returns a string of the name followed by =' do
      expect(association.setter).to eq("#{name}=")
    end
  end

  describe '#validate?' do

    context 'when :validate is specified in the options' do

      context 'when validate is true' do

        let(:options) do
          { validate: true }
        end

        it 'returns true' do
          expect(association.send(:validate?)).to be(true)
        end
      end

      context 'when validate is false' do

        let(:options) do
          { validate: false }
        end

        it 'returns false' do
          expect(association.send(:validate?)).to be(false)
        end
      end
    end

    context 'when :validate is not specified in the options' do

      it 'returns the validation_default' do
        expect(association.send(:validate?)).to eq(association.validation_default)
      end
    end
  end

  describe '#autobuilding?' do

    context 'when :autobuild is specified in the options' do

      context 'when autobuild is true' do

        let(:options) do
          { autobuild: true }
        end

        it 'returns true' do
          expect(association.autobuilding?).to be(true)
        end
      end

      context 'when autobuild is false' do

        let(:options) do
          { autobuild: false }
        end

        it 'returns true' do
          expect(association.autobuilding?).to be(false)
        end
      end
    end

    context 'when :validate is not specified in the options' do

      it 'returns false' do
        expect(association.autobuilding?).to be(false)
      end
    end
  end

  describe '#forced_nil_inverse?' do

    it 'returns false' do
      expect(association.forced_nil_inverse?).to be(false)
    end
  end

  describe '#stores_foreign_key?' do

    it 'returns false' do
      expect(association.stores_foreign_key?).to be(false)
    end
  end

  describe '#inverse_setter' do

    context 'when an inverse can be determined' do

      before do
        EmbeddedObject.embedded_in :container
      end

      it 'returns the name of the inverse followed by =' do
        expect(association.inverse_setter).to eq('container=')
      end
    end

    context 'when an inverse cannot be determined' do

      it 'returns nil' do
        expect(association.inverse_setter).to be_nil
      end
    end
  end

  describe '#extension' do

    context 'when a block is passed' do

      let(:association) do
        embeds_one_class.embeds_one name, options do; end
      end

      it 'defines an extension module' do
        expect(association.extension).to be_a(Module)
      end

      it 'returns the extension' do
        expect(association.extension).to eq(
          "#{embeds_one_class.name}::#{embeds_one_class.name}#{name.to_s.camelize}RelationExtension".constantize)
      end
    end

    context 'when an :extension is not specified in the options' do

      it 'returns false' do
        expect(association.extension).to be_nil
      end
    end
  end

  describe '#criteria' do

    it 'does not respond to the method' do
      expect {
        association.criteria
      }.to raise_exception(NoMethodError)
    end
  end

  describe '#destructive?' do

    it 'returns false' do
      expect(association.destructive?).to be(false)
    end
  end

  describe '#nested_builder' do

    it 'returns an instance of Association::Nested::One' do
      expect(association.nested_builder({}, {})).to be_a(Mongoid::Association::Nested::One)
    end
  end

  describe '#cascading_callbacks?' do

    context 'when cascade_callbacks is specified in the options' do

      context 'when :cascade_callbacks is true' do

        let(:options) do
          { cascade_callbacks: true }
        end

        it 'returns true' do
          expect(association.cascading_callbacks?).to be(true)
        end
      end

      context 'when :cascade_callbacks is false' do

        let(:options) do
          { cascade_callbacks: false }
        end

        it 'returns false' do
          expect(association.cascading_callbacks?).to be(false)
        end
      end
    end

    context 'when cascade_callbacks is not specified in the options' do

      it 'returns false' do
        expect(association.cascading_callbacks?).to be(false)
      end
    end
  end

  describe '#path' do

    it 'returns an instance of Mongoid::Atomic::Paths::Root' do
      expect(association.path(double( :_parent => true))).to be_a(Mongoid::Atomic::Paths::Embedded::One)
    end
  end

  describe '#create_relation' do

    let(:owner) do
      Container.new
    end

    let(:target) do
      EmbeddedObject.new
    end

    before do
      association
      EmbeddedObject.embedded_in :container
    end

    it 'returns an the target (EmbeddedObject)' do
      expect(Mongoid::Association::Embedded::EmbedsOne::Proxy).to receive(:new).and_call_original
      expect(association.create_relation(owner, target)).to be_a(EmbeddedObject)
    end
  end
end
