# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"

describe Mongoid::Factory do

  describe ".build" do

    context "when the type attribute is present" do

      let(:attributes) do
        { "_type" => "Person", "title" => "Sir" }
      end

      context "when the type is a class" do

        let(:person) do
          described_class.build(Person, attributes)
        end

        it "instantiates based on the type" do
          expect(person.title).to eq("Sir")
        end
      end

      context "when the type is a not a subclass" do

        let(:person) do
          described_class.build(Person, { "_type" => "Canvas" })
        end

        it "instantiates the provided class" do
          expect(person.class).to eq(Person)
        end
      end

      context "when the type is a subclass of the provided" do

        let(:person) do
          described_class.build(Person, { "_type" => "Doctor" })
        end

        it "instantiates the subclass" do
          expect(person.class).to eq(Doctor)
        end
      end

      context "when type is an empty string" do

        let(:attributes) do
          { "title" => "Sir", "_type" => "" }
        end

        let(:person) do
          described_class.build(Person, attributes)
        end

        it "instantiates based on the type" do
          expect(person.title).to eq("Sir")
        end
      end

      context "when type is the lower case class name" do

        let(:attributes) do
          { "title" => "Sir", "_type" => "person" }
        end

        let(:person) do
          described_class.build(Person, attributes)
        end

        it "instantiates based on the type" do
          expect(person.title).to eq("Sir")
        end
      end
    end

    context "when type is not preset" do
      context "when using the default discriminator key" do
        let(:attributes) do
          { "title" => "Sir" }
        end

        let(:person) do
          described_class.build(Person, attributes)
        end

        it "instantiates based on the provided class" do
          expect(person.title).to eq("Sir")
        end

        context "when the type is a symbol" do

          let(:person) do
            described_class.build(Person, { :_type => "Doctor" })
          end

          it "instantiates the subclass" do
            expect(person.class).to eq(Doctor)
          end
        end
      end

      context "when using a custom discriminator key" do
        before do
          Person.discriminator_key = "dkey"
        end

        after do
          Person.discriminator_key = nil
        end

        let(:attributes) do
          { "title" => "Sir" }
        end

        let(:person) do
          described_class.build(Person, attributes)
        end

        it "instantiates based on the provided class" do
          expect(person.title).to eq("Sir")
        end

        context "when the type is a symbol" do

          let(:person) do
            described_class.build(Person, { :dkey => "Doctor" })
          end

          it "instantiates the subclass" do
            expect(person.class).to eq(Doctor)
          end
        end
      end

      context "when using a custom discriminator key and value" do
        before do
          Person.discriminator_key = "dkey"
          Doctor.discriminator_value = "dvalue"
        end

        after do
          Person.discriminator_key = nil
          Doctor.discriminator_value = nil
        end

        let(:attributes) do
          { "title" => "Sir", "dkey" => "dvalue" }
        end

        let(:doctor) do
          described_class.build(Person, attributes)
        end

        it "instantiates based on the provided class" do
          expect(doctor.title).to eq("Sir")
        end

        it "generates based on the provided class" do
          expect(doctor).to be_a(Person)
        end

        it "sets the attributes" do
          expect(doctor.title).to eq("Sir")
        end

        it "has the correct discriminator key/value" do
          expect(doctor.dkey).to eq("dvalue")
        end
      end
    end
  end

  describe ".from_db" do

    context "when the attributes are nil" do

      let(:document) do
        described_class.from_db(model_cls, nil)
      end

      context 'when model class does not use inheritance' do
        context 'when model overwrites _id field to not have a default' do
          let(:model_cls) { Idnodef }

          it "generates based on the provided class" do
            expect(document).to be_a(model_cls)
          end

          it "sets the attributes to empty" do
            expect(document.attributes).to be_empty
          end
        end

        context 'with default _id auto-assignment behavior' do
          let(:model_cls) { Agency }

          it "generates based on the provided class" do
            expect(document).to be_a(model_cls)
          end

          it "sets the attributes to generated _id only" do
            document.attributes.should == {'_id' => document.id}
          end
        end
      end

      context 'when model class is an inheritance root' do
        let(:model_cls) { Address }

        before do
          # Ensure a child is defined
          ShipmentAddress.superclass.should be model_cls
        end

        it "generates based on the provided class" do
          expect(document).to be_a(model_cls)
        end

        it "sets the attributes to _type only" do
          skip 'https://jira.mongodb.org/browse/MONGOID-5179'
          # Note that Address provides the _id override.
          document.attributes.should == {'_type' => 'Address'}
        end
      end

      context 'when model class is an inheritance leaf' do
        let(:model_cls) { ShipmentAddress }

        it "generates based on the provided class" do
          expect(document).to be_a(model_cls)
        end

        it "sets the attributes to empty" do
          # Note that Address provides the _id override.
          document.attributes.should == {'_type' => 'ShipmentAddress'}
        end
      end
    end

    context "when a type is in the attributes" do

      context "when the type is a class" do

        let(:attributes) do
          { "_type" => "Person", "title" => "Sir" }
        end

        let(:document) do
          described_class.from_db(Address, attributes)
        end

        it "generates based on the type" do
          expect(document).to be_a(Person)
        end

        it "sets the attributes" do
          expect(document.title).to eq("Sir")
        end
      end

      context "when the type is empty" do

        let(:attributes) do
          { "_type" => "", "title" => "Sir" }
        end

        let(:document) do
          described_class.from_db(Person, attributes)
        end

        it "generates based on the provided class" do
          expect(document).to be_a(Person)
        end

        it "sets the attributes" do
          expect(document.title).to eq("Sir")
        end
      end

      context "when type is the lower case class name" do

        let(:attributes) do
          { "title" => "Sir", "_type" => "person" }
        end

        let(:person) do
          described_class.from_db(Person, attributes)
        end

        it "instantiates based on the type" do
          expect(person.title).to eq("Sir")
        end
      end
    end

    context "when a type is not in the attributes" do

      context "when using the default discriminator key" do
        let(:attributes) do
          { "title" => "Sir" }
        end

        let(:document) do
          described_class.from_db(Person, attributes)
        end

        it "generates based on the provided class" do
          expect(document).to be_a(Person)
        end

        it "sets the attributes" do
          expect(document.title).to eq("Sir")
        end
      end

      context "when using a custom discriminator key" do
        before do
          Person.discriminator_key = "dkey"
        end

        after do
          Person.discriminator_key = nil
        end

        let(:attributes) do
          { "title" => "Sir" }
        end

        let(:document) do
          described_class.from_db(Person, attributes)
        end

        it "generates based on the provided class" do
          expect(document).to be_a(Person)
        end

        it "sets the attributes" do
          expect(document.title).to eq("Sir")
        end
      end

      context "when using a custom discriminator key and discriminator value" do
        before do
          Person.discriminator_key = "dkey"
          Person.discriminator_value = "dvalue"
        end

        after do
          Person.discriminator_key = nil
          Person.discriminator_value = nil
        end

        let(:attributes) do
          { "title" => "Sir" }
        end

        let(:document) do
          described_class.from_db(Person, attributes)
        end

        it "generates based on the provided class" do
          expect(document).to be_a(Person)
        end

        it "sets the attributes" do
          expect(document.title).to eq("Sir")
        end

        it "has the correct discriminator key/value" do
          expect(document.dkey).to eq("dvalue")
        end
      end
    end

    context 'when type does not correspond to a Class name' do

      let(:attributes) do
        { "title" => "Sir", "_type" => "invalid_class_name" }
      end

      let(:person) do
        described_class.from_db(Person, attributes)
      end

      it 'raises a exception' do
        expect {
          person
        }.to raise_exception(Mongoid::Errors::UnknownModel)
      end
    end

    context 'when type does not correspond to a Class name with custom discriminator key' do

      before do
        Person.discriminator_key = "dkey"
      end

      after do
        Person.discriminator_key = nil
      end

      let(:attributes) do
        { "title" => "Sir", "dkey" => "invalid_class_name" }
      end

      let(:person) do
        described_class.from_db(Person, attributes)
      end

      it 'raises a exception' do
        expect {
          person
        }.to raise_exception(Mongoid::Errors::UnknownModel)
      end
    end

    context 'when type does not correspond to a custom discriminator_value' do
      before do
        Person.discriminator_value = "dvalue"
      end

      after do
        Person.discriminator_value = nil
      end

      let(:attributes) do
        { "title" => "Sir", "_type" => "dvalue" }
      end

      let(:person) do
        described_class.from_db(Person, attributes)
      end

      it "generates based on the provided class" do
        expect(person).to be_a(Person)
      end

      it "sets the attributes" do
        expect(person.title).to eq("Sir")
      end

      it "has the correct discriminator key/value" do
        expect(person._type).to eq("dvalue")
      end
    end

    context 'when type is correct but the instantiation raises a NoMethodError' do
      class BadPerson < Person
        def self.instantiate_document(*args)
          call_some_nonexistent_method(*args)
          super
        end
      end

      let(:attributes) do
        { "title" => "Sir", "_type" => "BadPerson" }
      end

      let(:person) do
        described_class.from_db(BadPerson, attributes)
      end

      it 'raises a exception' do
        expect {
          person
        }.to raise_exception(NoMethodError)
      end

    end

    context "when not deferring callbacks" do

      let(:person) do
        described_class.execute_from_db(Person, {}, execute_callbacks: true)
      end

      before do
        Person.set_callback :initialize, :after do |doc|
          doc.title = "Madam"
        end

        Person.set_callback :find, :after do |doc|
          doc.ssn = 1234
        end
      end

      after do
        Person.reset_callbacks(:initialize)
        Person.reset_callbacks(:find)
      end

      it "runs the initialize callbacks" do
        expect(person.title).to eq("Madam")
      end

      it "runs the find callbacks" do
        expect(person.ssn).to eq(1234)
      end
    end

    context "when deferring callbacks" do

      let(:person) do
        described_class.execute_from_db(Person, {}, nil, nil, execute_callbacks: false)
      end

      before do
        Person.set_callback :initialize, :after do |doc|
          doc.title = "Madam"
        end

        Person.set_callback :find, :after do |doc|
          doc.ssn = 1234
        end
      end

      after do
        Person.reset_callbacks(:initialize)
        Person.reset_callbacks(:find)
      end

      it "runs the initialize callbacks" do
        expect(person.title).to be nil
      end

      it "runs the find callbacks" do
        expect(person.ssn).to be nil
      end
    end
  end
end
