# frozen_string_literal: true
# rubocop:todo all

require 'spec_helper'
require_relative './callbacks_models'

describe 'callbacks integration tests' do
  context 'when modifying attributes in a callback' do

    context 'when creating top-level document' do
      context 'top level document' do
        let(:instance) do
          Galaxy.create!
        end

        it 'writes the attribute value into the model' do
          instance.age.should == 100_000
        end

        it 'persists the attribute value' do
          Galaxy.find(instance.id).age.should == 100_000
        end
      end

      context 'embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.stars.first.age.should == 42_000
          end

          it 'persists the attribute value' do
            Galaxy.find(instance.id).stars.first.age.should == 42_000
          end
        end

        context 'set as a document instance' do
          let(:instance) do
            Galaxy.create!(stars: [Star.new])
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          let(:instance) do
            Galaxy.create!(stars: [{}])
          end

          include_examples 'persists the attribute value'
        end
      end

      context 'nested embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.stars.first.planets.first.age.should == 2_000
          end

          it 'persists the attribute value' do
            Galaxy.find(instance.id).stars.first.planets.first.age.should == 2_000
          end
        end

        context 'set as a document instance' do
          let(:instance) do
            Galaxy.create!(stars: [Star.new(
              planets: [Planet.new],
            )])
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          let(:instance) do
            Galaxy.create!(stars: [
              planets: [{}],
            ])
          end

          include_examples 'persists the attribute value'
        end
      end
    end

    context 'when updating top-level document via #save' do
      let!(:instance) do
        Galaxy.create!
      end

      context 'embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.stars.first.age.should == 42_000
          end

          it 'persists the attribute value' do
            Galaxy.find(instance.id).stars.first.age.should == 42_000
          end
        end

        context 'set as a document instance' do
          before do
            instance.stars = [Star.new]
            instance.save!
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          before do
            instance.stars = [{}]
            instance.save!
          end

          include_examples 'persists the attribute value'
        end
      end

      context 'nested embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.stars.first.planets.first.age.should == 2_000
          end

          it 'persists the attribute value' do
            Galaxy.find(instance.id).stars.first.planets.first.age.should == 2_000
          end
        end

        context 'set as a document instance' do
          before do
            instance.stars = [Star.new(planets: [Planet.new])]
            instance.save!
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          before do
            instance.stars = [planets: [{}]]
            instance.save!
          end

          include_examples 'persists the attribute value'
        end
      end
    end

    context 'when updating top-level document via #update_attributes!' do
      let!(:instance) do
        Galaxy.create!
      end

      context 'embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.stars.first.age.should == 42_000
          end

          it 'persists the attribute value' do
            Galaxy.find(instance.id).stars.first.age.should == 42_000
          end
        end

        context 'set as a document instance' do
          before do
            instance.update_attributes!(stars: [Star.new])
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          before do
            instance.update_attributes!(stars: [{}])
          end

          include_examples 'persists the attribute value'
        end
      end

      context 'nested embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.stars.first.planets.first.age.should == 2_000
          end

          it 'persists the attribute value' do
            Galaxy.find(instance.id).stars.first.planets.first.age.should == 2_000
          end
        end

        context 'set as a document instance' do
          before do
            instance.update_attributes!(stars: [Star.new(planets: [Planet.new])])
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          before do
            instance.update_attributes!(stars: [planets: [{}]])
          end

          include_examples 'persists the attribute value'
        end
      end
    end

    context 'when updating top-level embeds_one document via #update_attributes!' do
      let!(:instance) do
        Country.create!
      end

      context 'embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.president.age.should == 79
          end

          it 'persists the attribute value' do
            Country.find(instance.id).president.age.should == 79
          end
        end

        context 'set as a document instance' do
          before do
            instance.update_attributes!(president: President.new)
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          before do
            instance.update_attributes!(president: { name: "Abraham Lincoln" })
          end

          include_examples 'persists the attribute value'
        end
      end

      context 'nested embedded document' do
        shared_examples 'persists the attribute value' do
          it 'writes the attribute value into the model' do
            instance.president.first_spouse.age.should == 70
          end

          it 'persists the attribute value' do
            Country.find(instance.id).president.first_spouse.age.should == 70
          end
        end

        context 'set as a document instance' do
          before do
            instance.update_attributes!(president: President.new(first_spouse: FirstSpouse.new))
          end

          include_examples 'persists the attribute value'
        end

        context 'set as attributes on parent' do
          before do
            instance.update_attributes!(president: { first_spouse: { name: "Mary Todd Lincoln" } })
          end

          include_examples 'persists the attribute value'
        end
      end
    end
  end

  context 'attribute_was value in after_save callback' do
    let!(:obj) { Emission.create!(frequency: 1) }

    it 'is set to the new value' do
      obj.frequency = 2
      obj.save!

      obj.previous.should == 2
    end
  end

  context 'atomic_selector in after_save callback' do
    let(:name) do
      'Alice'
    end

    let(:new_name) do
      'Bob'
    end

    class CBIntSpecProfile
      include Mongoid::Document
      field :name, type: String
      shard_key :name

      attr_reader :atomic_selector_in_after_save

      after_save do |document|
        @atomic_selector_in_after_save = document.atomic_selector
      end
    end

    it 'has updated attributes' do
      profile = CBIntSpecProfile.create!(name: name)
      profile.name = new_name
      profile.save!
      expect(
        profile.atomic_selector_in_after_save['name']
      ).to eq(new_name)
    end
  end

  context "When touching an embedded document" do
    let(:planet) { Planet.new }
    let(:star) { Star.new }
    let(:galaxy) { Galaxy.create! }

    before do
      star.planets << planet
      galaxy.stars << star
    end

    it "the parent document touch callback gets called before the child" do
      planet.touch
      expect(galaxy.was_touched).to be true
      expect(star.was_touched_after_parent).to be true
      expect(planet.was_touched_after_parent).to be true
    end
  end

  context "when reloading has_and_belongs_to_many after_save and after_remove callbacks" do

    let(:architect) { Architect.create }

    let(:b1) { Building.create }

    let(:b2) { Building.create }

    let(:b3) { Building.create }

    it "counts added/removed buildings correctly" do
      architect.buildings << b1
      expect(architect.after_add_num_buildings).to eq(1)

      architect.reload
      architect.buildings << b2
      expect(architect.after_add_num_buildings).to eq(2)

      architect.reload
      architect.buildings << b3
      expect(architect.after_add_num_buildings).to eq(3)

      architect.reload
      architect.buildings.delete(b3)
      expect(architect.after_remove_num_buildings).to eq(2)
    end
  end

  context '_previously was methods in after_save callback' do
    let(:title) do
      "Title"
    end

    let(:updated_title) do
      "Updated title"
    end

    let(:age) do
      10
    end

    it do
      class PreviouslyWasPerson
        include Mongoid::Document

        field :title, type: String
        field :age, type: Integer

        attr_reader :after_save_vals

        set_callback :save, :after do |doc|
          @after_save_vals ||= []
          @after_save_vals << [doc.title_previously_was, doc.age_previously_was]
        end
      end

      person = PreviouslyWasPerson.create!(title: title, age: age)
      person.title = updated_title
      person.save!
      expect(person.after_save_vals).to eq([
        # Field values are nil before create
        [nil, nil],
        [title, age]
        ])
    end
  end

  context 'previously_new_record? in after_save' do
    it do
      class PreviouslyNewRecordPerson
        include Mongoid::Document

        field :title, type: String
        field :age, type: Integer

        attr_reader :previously_new_record_value

        set_callback :save, :after do |doc|
          @previously_new_record_value = doc.previously_new_record?
        end
      end

      person = PreviouslyNewRecordPerson.create!(title: "title", age: 55)
      expect(person.previously_new_record_value).to be_truthy
      person.title = "New title"
      person.save!
      expect(person.previously_new_record_value).to be_falsey
    end
  end

  context 'previously_persisted? in after_destroy' do
    it do
      class PreviouslyPersistedPerson
        include Mongoid::Document

        field :title, type: String
        field :age, type: Integer

        attr_reader :previously_persisted_value

        set_callback :destroy, :after do |doc|
          @previously_persisted_value = doc.previously_persisted?
        end
      end

      unsaved_person = PreviouslyPersistedPerson.new(title: "title", age: 55)
      unsaved_person.destroy
      expect(unsaved_person.previously_persisted_value).to be_falsey

      saved_person = PreviouslyPersistedPerson.create(title: "title", age: 55)
      saved_person.destroy
      expect(saved_person.previously_persisted_value).to be_truthy
    end
  end

  context 'saved_change_to_attribute, attribute_before_last_save, will_save_change_to_attribute' do
    class TestSCTAAndABLSInCallbacks
      include Mongoid::Document

      field :name, type: String
      field :age, type: Integer

      set_callback :save, :before do |doc|
        [:name, :age].each do |attr|
          saved_change_to_attribute_values_before[attr] += [saved_change_to_attribute(attr)]
          attribute_before_last_save_values_before[attr] += [attribute_before_last_save(attr)]
          will_save_change_to_attribute_values_before[attr] += [will_save_change_to_attribute?(attr)]
        end
      end

      set_callback :save, :after do |doc|
        [:name, :age].each do |attr|
          saved_change_to_attribute_values_after[attr] += [saved_change_to_attribute(attr)]
          saved_change_to_attribute_q_values_after[attr] += [saved_change_to_attribute?(attr)]
          attribute_before_last_save_values_after[attr] += [attribute_before_last_save(attr)]
        end
      end

      def saved_change_to_attribute_values_before
        @saved_change_to_attribute_values_before ||= Hash.new do
          []
        end
      end

      def attribute_before_last_save_values_before
        @attribute_before_last_save_values_before ||= Hash.new do
          []
        end
      end

      def saved_change_to_attribute_values_after
        @saved_change_to_attribute_values_after ||= Hash.new do
          []
        end
      end

      def saved_change_to_attribute_q_values_after
        @saved_change_to_attribute_q_values_after ||= Hash.new do
          []
        end
      end

      def attribute_before_last_save_values_after
        @attribute_before_last_save_values_after ||= Hash.new do
          []
        end
      end

      def will_save_change_to_attribute_values_before
        @will_save_change_to_attribute_values_before ||= Hash.new do
          []
        end
      end
    end

    it 'reproduces ActiveRecord::AttributeMethods::Dirty behavior' do
      subject = TestSCTAAndABLSInCallbacks.new(name: 'Name 1')
      subject.save!
      subject.age = 18
      subject.save!
      subject.name = 'Name 2'
      subject.save!

      expect(subject.saved_change_to_attribute_values_before).to eq(
        {
          :name => [nil, [nil, "Name 1"], nil],
          :age => [nil, nil, [nil, 18]],
        }
      )
      expect(subject.saved_change_to_attribute_values_after).to eq(
        {
          :name => [[nil, "Name 1"], nil, ["Name 1", "Name 2"]],
          :age => [nil, [nil, 18], nil],
        }
      )
      expect(subject.saved_change_to_attribute_q_values_after).to eq(
        {
          :name => [true, false, true],
          :age => [false, true, false],
        }
      )
      expect(subject.attribute_before_last_save_values_before).to eq(
        {
          :name => [nil, nil, "Name 1"],
          :age => [nil, nil, nil]
        }
      )
      expect(subject.attribute_before_last_save_values_after).to eq(
        {
          :name => [nil, "Name 1", "Name 1"],
          :age => [nil, nil, 18]
        }
      )
      expect(subject.will_save_change_to_attribute_values_before).to eq(
        {
          :name => [true, false, true],
          :age => [false, true, false]
        }
      )
    end
  end

  context 'nested embedded documents' do
    config_override :prevent_multiple_calls_of_embedded_callbacks, true
    config_override :around_callbacks_for_embeds, true

    let(:logger) { Array.new }

    let(:root) do
      Root.new(
        embedded_once: [
          EmbeddedOnce.new(
            embedded_twice: [EmbeddedTwice.new]
          )
        ]
      )
    end

    before(:each) do
      root.logger = logger
      root.embedded_once.first.logger = logger
      root.embedded_once.first.embedded_twice.first.logger = logger
    end

    it 'runs callbacks in the correct order' do
      root.save!
      expect(logger).to eq(%i[embedded_twice embedded_once root])
    end
  end

  context 'cascade callbacks' do
    ruby_version_gte '3.0'
    require_mri

    let(:book) do
      Book.new
    end

    before do
      1500.times do
        book.pages.build
      end
    end

    # https://jira.mongodb.org/browse/MONGOID-5658
    it 'does not raise SystemStackError' do
      expect { book.save! }.not_to raise_error(SystemStackError)
    end
  end
end
