# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"
require "support/immutable_ids"

describe Mongoid::Persistable::Savable do
  extend Mongoid::ImmutableIds
  immutable_id_examples_as "persisted _ids are immutable"

  describe "#save" do

    let(:person) do
      Person.create!
    end

    let(:contextable_item) do
      ContextableItem.new
    end

    let(:persisted_contextable_item) do
      ContextableItem.create!(title: 'sir')
    end

    context "when skipping validation" do

      context "when no relations are involved" do

        let(:account) do
          Account.new
        end

        let!(:saved) do
          account.save(validate: false)
        end

        it "returns true" do
          expect(saved).to be true
        end

        it "saves the document" do
          expect(account).to be_persisted
        end

        it "does not add any validation errors" do
          expect(account.errors).to be_empty
        end
      end

      context "when saving document that is a belongs to child" do

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

        let(:alert) do
          Alert.new(account: account)
        end

        context "when validating presence of the parent" do

          before do
            Alert.validates(:message, :account, presence: true)
          end

          after do
            Alert.reset_callbacks(:validate)
          end

          context "when the parent validates associated on the child" do

            before do
              alert.save(validate: false)
            end

            it "clears any errors off the document" do
              expect(alert.errors).to be_empty
            end

            context "when the document is not new" do

              before do
                alert.save(validate: false)
              end

              it "clears any errors off the document" do
                expect(alert.errors).to be_empty
              end
            end
          end
        end

        context 'when the embedded document is unchanged' do

          let(:kangaroo) do
            Kangaroo.new
          end

          after do
            Kangaroo.destroy_all
          end

          it 'only makes one call to the database' do
            allow(Kangaroo.collection).to receive(:insert).once
            expect_any_instance_of(Mongo::Collection::View).to receive(:update_one).never
            kangaroo.build_baby
            kangaroo.save
          end
        end
      end
    end

    context "when the document has been instantiated with limited fields" do

      before do
        person.age = 20
        person.save
      end

      context "when a default is excluded" do

        let(:limited) do
          Person.only(:_id).find(person.id)
        end

        it "does not flag the excluded fields as dirty" do
          expect(limited.changes).to be_empty
        end
      end

      context "when iterating over the documents" do

        let(:limited) do
          Person.only(:_id)
        end

        it "does not flag any changes" do
          limited.each do |person|
            expect(person.changes).to be_empty
          end
        end
      end
    end

    context "when validation passes" do

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

    context "when validation fails" do

      let(:address) do
        person.addresses.create!(city: "London")
      end

      before do
        address.save
      end

      it "has the appropriate errors" do
        expect(address.errors[:street]).to eq(["can't be blank"])
      end
    end

    context "when modifying the entire hierarchy" do

      context "when performing modification and insert ops" do

        let(:owner) do
          Owner.create!(name: "Blah")
        end

        let!(:birthday) do
          owner.birthdays.build(title: "First")
        end

        let!(:scribe) do
          owner.create_scribe(name: "Josh")
        end

        let(:from_db) do
          Owner.find(owner.id)
        end

        before do
          owner.name = "King"
          scribe.name = "Tosh"
        end

        it "persists with proper set and push modifiers" do
          expect(owner.atomic_updates).to eq({
            "$set" => {
              "name" => "King",
              "scribe.name" => "Tosh"
            },
            "$push"=> {
              "birthdays" => { '$each' => [ { "_id" => birthday.id, "title" => "First" } ] }
            }
          })
        end

        context "when saving the document" do

          it "saves the root document" do
            expect(owner.name).to eq("King")
          end

          it "saves embedded many relations" do
            expect(owner.birthdays.first.title).to eq("First")
          end

          it "saves embedded one relations" do
            expect(owner.scribe.name).to eq("Tosh")
          end
        end
      end

      context "when combining modifications and pushes" do

        let!(:location) do
          Location.new(name: 'Work')
        end

        let!(:address) do
          Address.new(
            number: 101,
            street: 'South St',
            locations: [ location ]
          )
        end

        let!(:person) do
          Person.create!(
            title: "Blah",
            addresses: [ address ]
          )
        end

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

        before do
          address.number = 102
          person.addresses << Address.new(street: "North Ave")
          person.save
        end

        it "saves modifications to existing embedded docs" do
          expect(from_db.addresses[0].number).to eq(102)
        end

        it "saves modifications to new embedded docs" do
          expect(from_db.addresses[1].street).to eq('North Ave')
        end

        it "saves modifications to deeply embedded docs" do
          expect(from_db.addresses[0].locations.first.name).to eq('Work')
        end
      end

      context 'when adding documents to embedded associations on multiple levels' do
        let!(:truck) { Truck.create! }
        let!(:crate) { truck.crates.create!(volume: 0.4) }

        it 'persists the new documents' do
          expect(truck.crates.size).to eq 1
          expect(truck.crates[0].volume).to eq 0.4
          expect(truck.crates[0].toys.size).to eq 0

          truck.crates.first.toys.build(name: "Teddy bear")
          truck.crates.build(volume: 0.8)

          # The following is equivalent to the two lines above:
          #
          # truck.crates_attributes = {
          #   '0' => {
          #     "toys_attributes" => {
          #       "0" => {
          #         "name" => "Teddy bear"
          #       }
          #     },
          #     "id" => crate.id.to_s
          #   },
          #   "1" => {
          #     "volume" => 0.8
          #   }
          # }

          expect(truck.crates.size).to eq 2
          expect(truck.crates[0].volume).to eq 0.4
          expect(truck.crates[0].toys.size).to eq 1
          expect(truck.crates[0].toys[0].name).to eq "Teddy bear"
          expect(truck.crates[1].volume).to eq 0.8
          expect(truck.crates[1].toys.size).to eq 0
          expect { truck.save! }.not_to raise_error

          _truck = Truck.find(truck.id)
          expect(_truck.crates.size).to eq 2
          expect(_truck.crates[0].volume).to eq 0.4
          expect(_truck.crates[0].toys.size).to eq 1
          expect(_truck.crates[0].toys[0].name).to eq "Teddy bear"
          expect(_truck.crates[1].volume).to eq 0.8
          expect(_truck.crates[1].toys.size).to eq 0
        end
      end

      context 'when adding documents to embedded association and updating parent fields' do
        let!(:truck) { Truck.create! }
        let!(:crate) { truck.crates.create!(volume: 0.4) }

        it 'performs all writes' do
          truck.crates.build(volume: 1)
          truck.crates.first.volume = 2

          truck.save!

          _truck = Truck.find(truck.id)
          _truck.crates.length.should == 2
          _truck.crates.first.volume.should == 2
          _truck.crates.last.volume.should == 1
        end
      end

      context 'when adding documents to nested embedded association and updating first association fields' do
        let!(:truck) { Truck.create! }
        let!(:seat) { truck.seats.create!(rating: 1) }

        it 'performs all writes' do
          truck.seats.first.armrests.build(side: 'left')
          truck.seats.first.rating = 2

          truck.save!

          _truck = Truck.find(truck.id)
          _truck.seats.length.should == 1
          _truck.seats.first.armrests.length.should == 1
          _truck.seats.first.armrests.first.side.should == 'left'
        end
      end

      context 'when adding documents to nested embedded association and adding another top level association' do
        let!(:truck) { Truck.create! }
        let!(:crate) { truck.crates.create!(volume: 1) }

        it 'performs all writes' do
          truck.crates.first.toys.build(name: 'Bear')
          truck.crates.build

          truck.save!

          _truck = Truck.find(truck.id)
          _truck.crates.length.should == 2
          _truck.crates.first.toys.length.should == 1
          _truck.crates.first.toys.first.name.should == 'Bear'
          _truck.crates.last.toys.length.should == 0
        end

        context 'when also updating first embedded top level association' do
          it 'performs all writes' do
            truck.crates.first.volume = 2
            truck.crates.first.toys.build(name: 'Bear')
            truck.crates.build

            truck.save!

            _truck = Truck.find(truck.id)
            _truck.crates.length.should == 2
            _truck.crates.first.toys.length.should == 1
            _truck.crates.first.toys.first.name.should == 'Bear'
            _truck.crates.last.toys.length.should == 0
          end
        end
      end

      context 'when adding documents to embedded associations with cascaded callbacks on update' do
        let!(:truck) { Truck.create! }
        let!(:seat) { truck.seats.create!(rating: 1) }

        it 'persists the new documents' do
          expect(truck.seats.size).to eq 1
          expect(truck.seats[0].rating).to eq 1

          truck.seats.build

          expect { truck.save! }.not_to raise_error

          _truck = Truck.find(truck.id)
          expect(_truck.seats.size).to eq 2
          expect(_truck.seats[0].rating).to eq 1
          expect(_truck.seats[1].rating).to eq 100
        end

        context 'when embedded association embeds another association' do
          it 'persists the new documents' do
            expect(truck.seats.size).to eq 1
            expect(truck.seats[0].rating).to eq 1

            truck.seats.first.armrests.build
            truck.seats.build

            expect { truck.save! }.not_to raise_error

            _truck = Truck.find(truck.id)
            expect(_truck.seats.size).to eq 2
            expect(_truck.seats[0].rating).to eq 2
            expect(_truck.seats[0].armrests.length).to eq 1
            expect(_truck.seats[1].rating).to eq 100
          end
        end
      end

      context "when removing elements without using delete or destroy" do

        let!(:person) do
          Person.create!(title: "Blah")
        end

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

        before do
          person.create_name(first_name: "Tony")
          person.name = nil
          person.save
        end

        it "saves the hierarchy" do
          expect(person.name).to be_nil
        end
      end
    end

    context "when setting floating point numbers" do

      context "when value is an empty string" do

        let(:person) do
          Person.new
        end

        before do
          Person.validates_numericality_of :blood_alcohol_content, allow_blank: true
        end

        it "does not set the value" do
          expect(person.save).to be true
        end
      end
    end

    context "when the changed attribute is not writable" do

      before do
        Person.create!(title: "sir")
      end

      let(:person) do
        Person.only(:title).first
      end

      it "raises an error" do
        expect {
          person.username = 'unloaded-attribute'
          person.save
        }.to raise_error(Mongoid::Errors::AttributeNotLoaded, /Attempted to access attribute 'username' on Person which was not loaded/)
      end

      context 'when the changed attribute is aliased' do

        before do
          Person.create!(at: Time.now)
        end

        let(:person) do
          Person.only(:at).first
        end

        it "saves the document" do
          person.aliased_timestamp = Time.now
          expect(person.save!(validate: false)).to be true
        end
      end
    end

    context "when validation context isn't assigned" do
      it "returns true" do
        expect(contextable_item.save).to be true
      end
    end

    context "when validation context exists" do

      context "on new document" do

        it "returns true" do
          contextable_item.title = "sir"
          expect(contextable_item.save(context: :in_context)).to be true
        end

        it "returns false" do
          expect(contextable_item.save(context: :in_context)).to be false
        end
      end

      context "on persisted document" do

        it "returns true" do
          persisted_contextable_item.title = "lady"
          expect(persisted_contextable_item.save(context: :in_context)).to be true
        end

        it "returns false" do
          persisted_contextable_item.title = nil
          expect(persisted_contextable_item.save(context: :in_context)).to be false
        end
      end
    end

    context "when saving a readonly document" do

      context "when legacy_readonly is true" do
        config_override :legacy_readonly, true

        context "when its a new document" do

          let(:document) do
            Band.new
          end

          before do
            document.__selected_fields = { 'test' => 1 }
            expect(document).to be_readonly
          end

          it "persists the document" do
            expect(Band.count).to eq(0)
            document.save!
            expect(Band.count).to eq(1)
          end
        end

        context "when its an old document" do

          let(:document) do
            Band.only(:name).first
          end

          before do
            Band.create!
            expect(document).to be_readonly
          end

          it "updates the document" do
            document.name = "The Rolling Stones"
            document.save!
            expect(Band.first.name).to eq("The Rolling Stones")
          end
        end
      end

      context "when legacy_readonly is false" do
        config_override :legacy_readonly, false

        context "when its a new document" do

          let(:document) do
            Band.new
          end

          before do
            document.readonly!
            expect(document).to be_readonly
          end

          it "raises a readonly error" do
            expect do
              document.save!
            end.to raise_error(Mongoid::Errors::ReadonlyDocument)
          end
        end

        context "when its an old document" do

          let(:document) do
            Band.first.tap(&:readonly!)
          end

          before do
            Band.create!
            expect(document).to be_readonly
          end

          it "raises a readonly error" do
            document.name = "The Rolling Stones"
            expect do
              document.save!
            end.to raise_error(Mongoid::Errors::ReadonlyDocument)
          end
        end
      end
    end

    context "when the _id has been modified" do
      def invoke_operation!
        object._id = new_id_value
        object.save
      end

      it_behaves_like "persisted _ids are immutable"
    end
  end

  describe "save!" do

    context "inserting with a field that is not unique" do

      context "when a unique index exists" do

        let(:person) do
          Person.new(ssn: "555-55-9999")
        end

        before do
          Person.index({ ssn: 1 }, { unique: true })
          Person.create_indexes
          Person.create!(ssn: "555-55-9999")
        end

        after do
          Person.collection.drop
        end

        it "raises an OperationFailure" do
          expect { person.save! }.to raise_error(Mongo::Error::OperationFailure)
        end
      end
    end

    context "with a validation error" do

      let(:person) do
        Person.new
      end

      let!(:service) do
        Service.new(person: person, sid: "a")
      end

      it "raises an error with multiple save attempts" do
        expect { service.save! }.to raise_error(Mongoid::Errors::Validations)
        expect { service.save! }.to raise_error(Mongoid::Errors::Validations)
      end
    end

    context "when a callback aborts the callback chain" do

      let(:oscar) do
        Oscar.new
      end

      it "raises a callback error" do
        expect { oscar.save! }.to raise_error(Mongoid::Errors::Callback)
      end
    end

    context "when a callback destroys the document" do

      let(:oscar) do
        Oscar.new(:destroy_after_save => true)
      end

      before do
        oscar.save!
      end

      it "flags the document as destroyed" do
        expect(oscar).to be_destroyed
      end
    end

    context "when a DateTime attribute is updated and persisted" do

      let(:user) do
        User.create!(last_login: 2.days.ago).tap do |u|
          u.last_login = DateTime.now
        end
      end

      it "reads for persistence as a UTC Time" do
        expect(user.changes["last_login"].last.class).to eq(Time)
      end

      it "persists with no exceptions thrown" do
        expect {
          user.save!
        }.not_to raise_error
      end
    end

    context "when a Date attribute is persisted" do

      let(:user) do
        User.create!(account_expires: 2.years.from_now).tap do |u|
          u.account_expires = "2/2/2002".to_date
        end
      end

      it "reads for persistence as a UTC Time" do
        expect(user.changes["account_expires"].last.class).to eq(Time)
      end

      it "persists with no exceptions thrown" do
        expect {
          user.save!
        }.not_to raise_error
      end
    end

    context "when the document has associations" do

      let!(:firefox) do
        Firefox.create!(name: "firefox")
      end

      let!(:writer) do
        HtmlWriter.new(speed: 100)
      end

      let!(:circle) do
        Circle.new(radius: 50)
      end

      let!(:square) do
        Square.new(width: 300, height: 150)
      end

      let(:from_db) do
        Firefox.find(firefox.id)
      end

      before do
        firefox.writer = writer
        firefox.shapes << [ circle, square ]
        firefox.save!
      end

      it "properly persists the one-to-one type" do
        expect(from_db).to be_a_kind_of(Firefox)
      end

      it "properly persists the one-to-one relations" do
        expect(from_db.writer).to eq(writer)
      end

      it "properly persists the one-to-many type" do
        expect(from_db.shapes.first).to eq(circle)
      end

      it "properly persists the one-to-many relations" do
        expect(from_db.shapes.last).to eq(square)
      end

      it "properly sets up the parent relation" do
        expect(from_db.shapes.first).to eq(circle)
      end

      it "properly sets up the entire hierarchy" do
        expect(from_db.shapes.first.canvas).to eq(firefox)
      end
    end
  end
end
