# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"
require "support/immutable_ids"

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

  describe "#update_attribute" do

    context "when the field is aliased" do

      let(:person) do
        Person.create!
      end

      context "when the setter is overridden" do

        before do
          person.update_attribute(:overridden_setter, "value")
        end

        it "updates the field" do
          expect(person.overridden_setter).to eq("value")
        end

        it "clears the dirty attributes" do
          expect(person).to_not be_changed
        end

        it "calls the overridden setter" do
          expect(person.instance_variable_get(:@override_called)).to be true
        end

        it "persists the changes" do
          expect(person.reload.overridden_setter).to eq("value")
        end
      end

      context "when setting via the field name" do

        before do
          person.update_attribute(:t, "testing")
        end

        it "updates the field" do
          expect(person.t).to eq("testing")
        end

        it "persists the changes" do
          expect(person.reload.t).to eq("testing")
        end
      end

      context "when setting via the field alias" do

        before do
          person.update_attribute(:test, "testing")
        end

        it "updates the field" do
          expect(person.t).to eq("testing")
        end

        it "persists the changes" do
          expect(person.reload.t).to eq("testing")
        end
      end
    end

    context "when setting an array field" do

      let(:person) do
        Person.create!(aliases: [])
      end

      before do
        person.update_attribute(:aliases, person.aliases << "Bond")
      end

      it "sets the new value in the document" do
        expect(person.aliases).to eq([ "Bond" ])
      end

      it "persists the changes" do
        expect(person.reload.aliases).to eq([ "Bond" ])
      end
    end

    context "when setting a boolean field" do

      context "when the field is true" do

        let(:person) do
          Person.new(terms: true)
        end

        context "when setting to false" do

          before do
            person.update_attribute(:terms, false)
          end

          it "persists the document" do
            expect(person).to be_persisted
          end

          it "changes the attribute value" do
            expect(person.terms).to be false
          end

          it "persists the changes" do
            expect(person.reload.terms).to be false
          end
        end
      end
    end

    context "when dynamic attributes are not enabled" do

      it "raises exception for an unknown attribute " do
        account = Account.create!(name: 'Foobar')

        expect {
          account.update_attribute(:somethingnew, "somethingnew")
        }.to raise_error(Mongoid::Errors::UnknownAttribute)
      end

      it "will update value of aliased field" do
        person = Person.create!
        person.update_attribute(:t, "test_value")
        expect(person.reload.t).to eq "test_value"
        expect(person.test).to eq "test_value"
      end
    end

    context "when provided a symbol attribute name" do

      let(:post) do
        Post.new
      end

      context "when the document is valid" do

        before do
          post.update_attribute(:title, "Testing")
        end

        it "sets the attribute" do
          expect(post.title).to eq("Testing")
        end

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

      context "when updating to the same value" do

        before do
          post.update_attribute(:title, "Testing")
        end

        it "returns true" do
          expect(post.update_attribute(:title, "Testing")).to be true
        end
      end

      context "when the document is invalid" do

        before do
          post.update_attribute(:title, "$invalid")
        end

        it "sets the attribute" do
          expect(post.title).to eq("$invalid")
        end

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

      context "when the document has been destroyed" do

        before do
          post.delete
        end

        it "raises an error" do
          expect {
            post.update_attribute(:title, "something")
          }.to raise_error(RuntimeError)
        end
      end
    end

    context "when provided a string attribute name" do

      let(:post) do
        Post.new
      end

      context "when the document is valid" do

        before do
          post.update_attribute("title", "Testing")
        end

        it "sets the attribute" do
          expect(post.title).to eq("Testing")
        end

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

      context "when the document is invalid" do

        before do
          post.update_attribute("title", "$invalid")
        end

        it "sets the attribute" do
          expect(post.title).to eq("$invalid")
        end

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

    context "when persisting a localized field" do
      with_default_i18n_configs

      let!(:product) do
        Product.create!(description: "The bomb")
      end

      before do
        ::I18n.locale = :de
        product.update_attribute(:description, "Die Bombe")
      end

      let(:attributes) do
        product.attributes["description"]
      end

      it "persists the en locale" do
        expect(attributes["en"]).to eq("The bomb")
      end

      it "persists the de locale" do
        expect(attributes["de"]).to eq("Die Bombe")
      end
    end

    context "when updating a deeply embedded document" do

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

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

      let!(:location) do
        address.locations.create!(name: "work")
      end

      let(:from_db) do
        Person.last.addresses.last.locations.last
      end

      before do
        from_db.update_attribute(:name, "home")
      end

      it "updates the attribute" do
        expect(from_db.name).to eq("home")
      end

      it "persists the changes" do
        expect(from_db.reload.name).to eq("home")
      end
    end

    context 'when the field is read-only' do

      before do
        Person.attr_readonly :species
      end

      after do
        Person.readonly_attributes.reject! { |a| a.to_s == 'species' }
      end

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

      it 'raises an error when trying to set the attribute' do
        expect {
          person.update_attribute(:species, :reptile)
        }.to raise_exception(Mongoid::Errors::ReadonlyAttribute)
      end

      context 'when referring to the attribute with a string' do

        it 'raises an error when trying to set the attribute' do
          expect {
            person.update_attribute('species', :reptile)
          }.to raise_exception(Mongoid::Errors::ReadonlyAttribute)
        end
      end

      context 'when the field is aliased' do

        before do
          Person.attr_readonly :at
        end

        after do
          Person.readonly_attributes.reject! { |a| a.to_s == 'at' }
        end

        it 'raises an error when trying to set the attribute using the db name' do
          expect {
            person.update_attribute(:at, Time.now)
          }.to raise_exception(Mongoid::Errors::ReadonlyAttribute)
        end

        it 'raises an error when trying to set the attribute using the aliased name' do
          expect {
            person.update_attribute(:aliased_timestamp, Time.now)
          }.to raise_exception(Mongoid::Errors::ReadonlyAttribute)
        end
      end
    end

    context 'when the field is loaded explicitly' do

      before do
        Person.create!(title: 'Captain')
      end

      context 'when the loaded attribute is updated' do

        let(:person) do
          Person.only(:title).first.tap do |_person|
            _person.update_attribute(:title, 'Esteemed')
          end
        end

        it 'allows the field to be updated' do
          expect(person.title).to eq('Esteemed')
        end

        it 'persists the updated field' do
          expect(person.reload.title).to eq('Esteemed')
        end
      end

      context 'when the an attribute other than the loaded one is updated' do

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

        it 'does not allow the field to be updated' do
          expect {
            person.update_attribute(:age, 20)
          }.to raise_exception(Mongoid::Errors::ReadonlyAttribute)
        end

        it 'does not persist the change' do
          expect(person.reload.age).to eq(100)
        end

        context 'when referring to the attribute with a string' do

          it 'does not allow the field to be updated' do
            expect {
              person.update_attribute('age', 20)
            }.to raise_exception(Mongoid::Errors::ReadonlyAttribute)
          end

          it 'does not persist the change' do
            expect(person.reload.age).to eq(100)
          end
        end
      end
    end

    context 'when fields are explicitly not loaded' do

      before do
        Person.create!(title: 'Captain')
      end

      context 'when the loaded attribute is updated' do

        let(:person) do
          Person.without(:age).first.tap do |_person|
            _person.update_attribute(:title, 'Esteemed')
          end
        end

        it 'allows the field to be updated' do
          expect(person.title).to eq('Esteemed')
        end

        it 'persists the updated field' do
          expect(person.reload.title).to eq('Esteemed')
        end
      end

      context 'when the non-loaded attribute is updated' do

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

        it 'does not allow the field to be updated' do
          expect {
            person.update_attribute(:title, 'Esteemed')
          }.to raise_exception(Mongoid::Errors::AttributeNotLoaded, /Attempted to access attribute 'title' on Person which was not loaded/)
        end

        it 'does not persist the change' do
          expect(person.reload.title).to eq('Captain')
        end

        context 'when referring to the attribute with a string' do

          it 'does not allow the field to be updated' do
            expect {
              person.update_attribute('title', 'Esteemed')
            }.to raise_exception(Mongoid::Errors::AttributeNotLoaded, /Attempted to access attribute 'title' on Person which was not loaded/)
          end

          it 'does not persist the change' do
            expect(person.reload.title).to eq('Captain')
          end
        end
      end
    end

    context 'when the field is _id' do
      def invoke_operation!
        object.update_attribute "_id", new_id_value
      end

      it_behaves_like "persisted _ids are immutable"
    end
  end

  [:update_attributes, :update].each do |method|

    describe "##{method}" do

      context "when saving with a hash field with invalid keys" do
        max_server_version '4.9'

        let(:person) do
          Person.create!
        end

        it "raises an error" do
          expect {
            person.update_attributes!(map: { "$bad.key" => "value" })
          }.to raise_error(Mongo::Error::OperationFailure)
        end
      end

      context "when the document has been destroyed" do
        max_server_version '4.9'

        let(:person) do
          Person.create!
        end

        it "raises an error" do
          expect {
            person.send(method, map: { "$bad.key" => "value" })
          }.to raise_error(Mongo::Error::OperationFailure)
        end
      end

      context "when validation passes" do

        let(:person) do
          Person.create!
        end

        let!(:saved) do
          person.send(method, pets: false)
        end

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

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

        it "saves the attributes" do
          expect(from_db.pets).to be false
        end
      end

      context "when the document has been destroyed" do

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

        before do
          person.delete
        end

        it "raises an error" do
          expect {
            person.send(method, title: "something")
          }.to raise_error(RuntimeError)
        end
      end

      context "when updating through a one-to-one relation" do

        let(:person) do
          Person.create!
        end

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

        before do
          person.send(method, ssn: "444-44-4444")
          game.person.send(method, ssn: "555-66-7777")
        end

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

        it "saves the attributes" do
          expect(person.ssn).to eq("555-66-7777")
        end
      end

      context "on a new record" do

        let(:person) do
          Person.new
        end

        before do
          person.send(method, pets: false, title: nil)
        end

        it "saves the new record" do
          expect(Person.find(person.id)).to_not be_nil
        end
      end

      context "when passing in a relation" do

        context "when providing an embedded child" do

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

          let!(:name) do
            person.create_name(first_name: "test", last_name: "user")
          end

          let(:new_name) do
            Name.new(first_name: "Rupert", last_name: "Parkes")
          end

          before do
            person.send(method, name: new_name)
          end

          it "updates the embedded document" do
            expect(person.name).to eq(new_name)
          end

          it "persists the changes" do
            expect(person.reload.name).to eq(new_name)
          end
        end

        context "when providing a parent to a referenced in" do

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

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

          context "when the relation has not yet been touched" do

            before do
              post.send(method, person: person)
            end

            it "sets the instance of the relation" do
              expect(person.posts).to eq([ post ])
            end

            it "sets properly through method_missing" do
              expect(person.posts.to_a).to eq([ post ])
            end

            it "persists the reference" do
              expect(person.posts(true)).to eq([ post ])
            end
          end

          context "when the relation has been touched" do

            before do
              person.posts
              post.send(method, person: person)
            end

            it "sets the instance of the relation" do
              expect(person.posts).to eq([ post ])
            end

            it "sets properly through method_missing" do
              expect(person.posts.to_a).to eq([ post ])
            end

            it "persists the reference" do
              expect(person.posts(true)).to eq([ post ])
            end
          end
        end
      end

      context "when in a deeply nested hierarchy" do

        let!(:person) do
          Person.new(title: "The Boss")
        end

        let!(:phone_number) do
          Phone.new(number: "123-456-7890")
        end

        let!(:country_code) do
          CountryCode.new(code: 1)
        end

        before do
          phone_number.country_code = country_code
          person.phone_numbers << phone_number
          person.save!
        end

        it "sets the first level document" do
          expect(person.phone_numbers.first).to eq(phone_number)
        end

        it "sets the second level document" do
          expect(person.phone_numbers.first.country_code).to eq(country_code)
        end

        context "when updating the first level document" do

          let(:phone) do
            person.phone_numbers.first
          end

          before do
            phone.number = "098-765-4321"
            phone.send(method, number: "098-765-4321")
          end

          it "sets the new attributes" do
            expect(phone.number).to eq("098-765-4321")
          end

          context "when reloading the root" do

            let(:reloaded) do
              person.reload
            end

            it "saves the new attributes" do
              expect(reloaded.phone_numbers.first.number).to eq("098-765-4321")
            end
          end
        end
      end

      context 'when the _id is one of the fields' do
        def invoke_operation!
          object.update_attributes _id: new_id_value
        end
  
        it_behaves_like "persisted _ids are immutable"
      end
    end
  end

  describe "#update!" do

    context "when a callback aborts the callback chain" do

      let(:oscar) do
        Oscar.new
      end

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

  describe "#update_attributes!" do

    let(:person) do
      Person.create!
    end

    let(:attributes) do
      { security_code: 'secret' }
    end

    it 'calls update_attributes' do
      person.should receive(:update_attributes).with(attributes).and_call_original
      lambda do
        person.update_attributes!(attributes)
      end.should_not raise_error
    end
  end
end
