# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"

describe Mongoid::Changeable do

  describe "#attribute_change" do

    context "when the attribute has changed from the persisted value" do

      context "when using the setter" do

        let(:person) do
          Person.new(title: "Grand Poobah").tap(&:move_changes)
        end

        before do
          person.title = "Captain Obvious"
        end

        it "returns an array of the old value and new value" do
          expect(person.send(:attribute_change, "title")).to eq(
            [ "Grand Poobah", "Captain Obvious" ]
          )
        end

        it "allows access via (attribute)_change" do
          expect(person.title_change).to eq(
            [ "Grand Poobah", "Captain Obvious" ]
          )
        end

        context "when the field is aliased" do

          let(:person) do
            Person.new(test: "Aliased 1").tap(&:move_changes)
          end

          before do
            person.test = "Aliased 2"
          end

          it "returns an array of the old value and new value" do
            expect(person.send(:attribute_change, "test")).to eq(
              [ "Aliased 1", "Aliased 2" ]
            )
          end

          it "allows access via (attribute)_change" do
            expect(person.test_change).to eq(
              [ "Aliased 1", "Aliased 2" ]
            )
          end
        end
      end

      context "when using [] methods" do

        let(:person) do
          Person.new(title: "Grand Poobah").tap(&:move_changes)
        end

        before do
          person[:title] = "Captain Obvious"
        end

        it "returns an array of the old value and new value" do
          expect(person.send(:attribute_change, "title")).to eq(
            [ "Grand Poobah", "Captain Obvious" ]
          )
        end

        it "allows access via (attribute)_change" do
          expect(person.title_change).to eq(
            [ "Grand Poobah", "Captain Obvious" ]
          )
        end
      end
    end

    context "when the attribute has changed from the default value" do

      context "when using the setter" do

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

        it "returns an array of nil and new value" do
          expect(person.send(:attribute_change, "pets")).to eq([ nil, true ])
        end

        it "allows access via (attribute)_change" do
          expect(person.pets_change).to eq([ nil, true ])
        end
      end

      context "when using [] methods" do

        context "when the field is defined" do

          let(:person) do
            Person.new
          end

          before do
            person[:pets] = true
          end

          it "returns an array of nil and new value" do
            expect(person.send(:attribute_change, "pets")).to eq([ nil, true ])
          end

          it "allows access via (attribute)_change" do
            expect(person.pets_change).to eq([ nil, true ])
          end
        end

        context "when the field is not defined" do

          let(:person) do
            Person.new
          end

          before do
            person[:t] = "test"
          end

          it "returns an array of nil and new value" do
            expect(person.send(:attribute_change, "t")).to eq([ nil, "test" ])
          end
        end
      end
    end

    context "when the attribute changes multiple times" do

      let(:person) do
        Person.new(title: "Grand Poobah").tap(&:move_changes)
      end

      before do
        person.title = "Captain Obvious"
        person.title = "Dark Helmet"
      end

      it "returns an array of the original value and new value" do
        expect(person.send(:attribute_change, "title")).to eq(
          [ "Grand Poobah", "Dark Helmet" ]
        )
      end

      it "allows access via (attribute)_change" do
        expect(person.title_change).to eq(
          [ "Grand Poobah", "Dark Helmet" ]
        )
      end
    end

    context "when the attribute is modified in place" do

      context "when the attribute is an array" do

        let(:person) do
          Person.new(aliases: [ "Grand Poobah" ]).tap(&:move_changes)
        end

        before do
          person.aliases[0] = "Dark Helmet"
        end

        it "returns an array of the original value and new value" do
          expect(person.send(:attribute_change, "aliases")).to eq(
            [[ "Grand Poobah" ],  [ "Dark Helmet" ]]
          )
        end

        it "allows access via (attribute)_change" do
          expect(person.aliases_change).to eq(
            [[ "Grand Poobah" ],  [ "Dark Helmet" ]]
          )
        end

        context "when the attribute changes multiple times" do

          before do
            person.aliases << "Colonel Sanders"
          end

          it "returns an array of the original value and new value" do
            expect(person.send(:attribute_change, "aliases")).to eq(
              [[ "Grand Poobah" ], [ "Dark Helmet", "Colonel Sanders" ]]
            )
          end
        end
      end

      context "when the attribute is a hash" do

        let(:person) do
          Person.new(map: { location: "Home" }).tap(&:move_changes)
        end

        before do
          person.map[:location] = "Work"
        end

        it "returns an array of the original value and new value" do
          expect(person.send(:attribute_change, "map")).to eq(
            [{ "location" => "Home" }, { "location" => "Work" }]
          )
        end

        it "allows access via (attribute)_change" do
          expect(person.map_change).to eq(
            [{ "location" => "Home" }, { "location" => "Work" }]
          )
        end

        context "when the attribute changes multiple times" do

          before do
            person.map[:lat] = 20.0
          end

          it "returns an array of the original value and new value" do
            expect(person.send(:attribute_change, "map")).to eq(
              [{ "location" => "Home" }, { "location" => "Work", "lat" => 20.0 }]
            )
          end
        end

        context "when the values are arrays" do

          let(:map) do
            {
              "stack1" => [ 1, 2, 3, 4 ],
              "stack2" => [ 1, 2, 3, 4 ],
              "stack3" => [ 1, 2, 3, 4 ]
            }
          end

          before do
            person.map = map
            person.move_changes
          end

          context "when reordering the arrays inline" do

            before do
              person.map["stack1"].reverse!
            end

            it "flags the attribute as changed" do
              expect(person.send(:attribute_change, "map")).to eq(
                [
                  {
                    "stack1" => [ 1, 2, 3, 4 ],
                    "stack2" => [ 1, 2, 3, 4 ],
                    "stack3" => [ 1, 2, 3, 4 ]
                  },
                  {
                    "stack1" => [ 4, 3, 2, 1 ],
                    "stack2" => [ 1, 2, 3, 4 ],
                    "stack3" => [ 1, 2, 3, 4 ]
                  },
                ]
              )
            end
          end
        end
      end
    end

    context "when the attribute has not changed from the persisted value" do

      let(:person) do
        Person.new(title: nil)
      end

      it "returns nil" do
        expect(person.send(:attribute_change, "title")).to be_nil
      end
    end

    context "when the attribute has not changed from the default value" do

      context "when the attribute differs from the persisted value" do

        let(:person) do
          Person.new
        end

        it "returns the change" do
          expect(person.send(:attribute_change, "pets")).to eq([ nil, false ])
        end
      end

      context "when the attribute does not differ from the persisted value" do

        let(:person) do
          Person.instantiate("pets" => false)
        end

        it "returns nil" do
          expect(person.send(:attribute_change, "pets")).to be_nil
        end
      end
    end

    context "when the attribute has been set with the same value" do

      let(:person) do
        Person.new(title: "Grand Poobah").tap(&:move_changes)
      end

      before do
        person.title = "Grand Poobah"
      end

      it "returns an empty array" do
        expect(person.send(:attribute_change, "title")).to be_nil
      end
    end

    context "when the attribute is removed" do

      let(:person) do
        Person.new(title: "Grand Poobah").tap(&:move_changes)
      end

      before do
        person.remove_attribute(:title)
      end

      it "returns an empty array" do
        expect(person.send(:attribute_change, "title")).to eq(
          [ "Grand Poobah", nil ]
        )
      end
    end
  end

  describe "#attribute_changed?" do

    context "when the attribute has changed from the persisted value" do

      let(:person) do
        Person.new(title: "Grand Poobah")
      end

      before do
        person.title = "Captain Obvious"
      end

      it "returns true" do
        expect(person.send(:attribute_changed?, "title")).to be true
      end

      it "allows access via (attribute)_changed?" do
        expect(person.title_changed?).to be true
      end

      context "when the field is aliased" do

        let(:person) do
          Person.new(test: "Aliased 1")
        end

        before do
          person.test = "Aliased 2"
        end

        it "returns true" do
          expect(person.send(:attribute_changed?, "test")).to be true
        end

        it "allows access via (attribute)_changed?" do
          expect(person.test_changed?).to be true
        end
      end
    end

    context "when the attribute has changed from the default value" do

      let(:person) do
        Person.new
      end

      before do
        person.pets = true
      end

      it "returns true" do
        expect(person.send(:attribute_changed?, "pets")).to be true
      end

      it "allows access via (attribute)_changed?" do
        expect(person.pets_changed?).to be true
      end
    end

    context "when the attribute has not changed the persisted value" do

      let!(:person) do
        Person.new(title: "Grand Poobah").tap(&:move_changes)
      end

      it "returns false" do
        expect(person.send(:attribute_changed?, "title")).to be false
      end
    end

    context "when the attribute has not changed from the default value" do

      context "when the attribute is not enumerable" do

        context "when the attribute differs from the persisted value" do

          let!(:person) do
            Person.new
          end

          it "returns true" do
            expect(person.send(:attribute_changed?, "pets")).to be true
          end
        end

        context "when the attribute does not differ from the persisted value" do

          let!(:person) do
            Person.instantiate("pets" => false)
          end

          it "returns false" do
            expect(person.send(:attribute_changed?, "pets")).to be false
          end
        end
      end

      context "when the attribute is an array" do

        let!(:person) do
          Person.new(aliases: [ "Bond" ])
        end

        context "when the array is only accessed" do

          before do
            person.move_changes
            person.aliases
          end

          it "returns false" do
            expect(person).to_not be_aliases_changed
          end
        end
      end

      context "when the attribute is a hash" do

        let!(:person) do
          Person.new(map: { key: "value" })
        end

        context "when the hash is only accessed" do

          before do
            person.move_changes
            person.map
          end

          it "returns false" do
            expect(person).to_not be_map_changed
          end
        end
      end
    end

    context "when including key word args" do

      let(:person) { Person.new }

      context "when only including from" do

        context "when the object has not changed" do

          it "returns false" do
            expect(person.send(:attribute_changed?, :score, from: nil)).to be false
          end

          it "returns false using (attribute)_changed?" do
            expect(person.score_changed?(from: nil)).to be false
          end
        end

        context "when the object has changed from the wrong item" do

          before do
            person.score = 2
          end

          it "returns false" do
            expect(person.send(:attribute_changed?, :score, from: 1)).to be false
          end

          it "returns false using (attribute)_changed?" do
            expect(person.score_changed?(from: 1)).to be false
          end
        end

        context "when the object has changed from the correct item" do

          before do
            person.score = 2
          end

          it "returns true" do
            expect(person.send(:attribute_changed?, :score, from: nil)).to be true
          end

          it "returns true using (attribute)_changed?" do
            expect(person.score_changed?(from: nil)).to be true
          end
        end
      end

      context "when only including to" do

        context "when the object has not changed" do

          it "returns false" do
            expect(person.send(:attribute_changed?, :score, to: nil)).to be false
          end

          it "returns false using (attribute)_changed?" do
            expect(person.score_changed?(to: nil)).to be false
          end
        end

        context "when the object has changed to the wrong item" do

          before do
            person.score = 2
          end

          it "returns false" do
            expect(person.send(:attribute_changed?, :score, to: 1)).to be false
          end

          it "returns false using (attribute)_changed?" do
            expect(person.score_changed?(to: 1)).to be false
          end
        end

        context "when the object has changed to the correct item" do

          before do
            person.score = 2
          end

          it "returns true" do
            expect(person.send(:attribute_changed?, :score, to: 2)).to be true
          end

          it "returns true using (attribute)_changed?" do
            expect(person.score_changed?(to: 2)).to be true
          end
        end
      end

      context "when including from and to" do

        context "when the object has not changed" do

          it "returns false" do
            expect(person.send(:attribute_changed?, :score, from: nil, to: nil)).to be false
          end

          it "returns false using (attribute)_changed?" do
            expect(person.score_changed?(from: nil, to: nil)).to be false
          end
        end

        context "when only the from is correct" do

          before do
            person.score = 2
          end

          it "returns false" do
            expect(person.send(:attribute_changed?, :score, from: nil, to: 3)).to be false
          end

          it "returns false using (attribute)_changed?" do
            expect(person.score_changed?(from: nil, to: 3)).to be false
          end
        end

        context "when only the to is correct" do

          before do
            person.score = 2
          end

          it "returns false" do
            expect(person.send(:attribute_changed?, :score, from: 1, to: 2)).to be false
          end

          it "returns false using (attribute)_changed?" do
            expect(person.score_changed?(from: 1, to: 2)).to be false
          end
        end

        context "when the from and to are correct" do

          before do
            person.score = 2
          end

          it "returns true" do
            expect(person.send(:attribute_changed?, :score, from: nil, to: 2)).to be true
          end

          it "returns true using (attribute)_changed?" do
            expect(person.score_changed?(from: nil, to: 2)).to be true
          end
        end

        context "when value is mongoized" do

          before do
            person.score = "2"
          end

          it "returns true with mongoized value" do
            expect(person.send(:attribute_changed?, :score, from: nil, to: 2)).to be true
          end

          it "returns true with mongoized value using (attribute)_changed?" do
            expect(person.score_changed?(from: nil, to: 2)).to be true
          end
        end

        context "when value is mongoized" do

          before do
            person.score = "2"
          end

          it "returns false with unmongoized value" do
            expect(person.send(:attribute_changed?, :score, from: nil, to: "2")).to be false
          end

          it "returns false with unmongoized value using (attribute)_changed?" do
            expect(person.score_changed?(from: nil, to: "2")).to be false
          end
        end
      end
    end
  end

  describe "#attribute_changed_from_default?" do

    context "when the attribute differs from the default value" do

      let(:person) do
        Person.new(age: 33)
      end

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

    context "when the attribute is the same as the default" do

      let(:person) do
        Person.new
      end

      it "returns false" do
        expect(person).to_not be_age_changed_from_default
      end
    end
  end

  describe "#attribute_was" do

    context "when the attribute has changed from the persisted value" do

      let(:person) do
        Person.new(title: "Grand Poobah").tap(&:move_changes)
      end

      before do
        person.title = "Captain Obvious"
      end

      it "returns the old value" do
        expect(person.send(:attribute_was, "title")).to eq("Grand Poobah")
      end

      it "allows access via (attribute)_was" do
        expect(person.title_was).to eq("Grand Poobah")
      end

      context "when the field is aliased" do

        let(:person) do
          Person.new(test: "Aliased 1").tap(&:move_changes)
        end

        before do
          person.test = "Aliased 2"
        end

        it "returns the old value" do
          expect(person.send(:attribute_was, "test")).to eq("Aliased 1")
        end

        it "allows access via (attribute)_was" do
          expect(person.test_was).to eq("Aliased 1")
        end
      end
    end

    context "when the attribute has not changed from the persisted value" do

      let!(:person) do
        Person.new(title: "Grand Poobah").tap(&:move_changes)
      end

      it "returns the original value" do
        expect(person.send(:attribute_was, "title")).to eq("Grand Poobah")
      end
    end
  end

  describe '#attribute_previously_was' do
    let(:previous_title) do
      "Grand Poobah"
    end

    let(:age) do
      10
    end

    let(:person) do
      Person.create!(title: previous_title, age: age)
    end

    let(:updated_title) do
      "Captain Obvious"
    end

    before do
      person.title = updated_title
      person.save!
    end

    context 'when attribute changed' do
      it "returns the old value" do
        expect(person.send(:attribute_previously_was, "title")).to eq(previous_title)
      end

      it "allows access via (attribute)_was" do
        expect(person.title_previously_was).to eq(previous_title)
      end
    end

    context 'when attribute did not change' do
      it "returns the same value" do
        expect(person.send(:attribute_previously_was, "age")).to eq(age)
      end

      it "allows access via (attribute)_was" do
        expect(person.age_previously_was).to eq(age)
      end
    end

    it 'clears after reload' do
      person.reload
      expect(person.title_previously_was).to be_nil
      expect(person.age_previously_was).to be_nil
    end

  end

  describe "#attribute_will_change!" do

    let(:aliases) do
      [ "007" ]
    end

    let(:person) do
      Person.new(aliases: aliases, test: "Aliased 1")
    end

    before do
      person.changed_attributes.clear
    end

    context "when the value has not changed" do

      before do
        person.aliases_will_change!
      end

      let(:changes) do
        person.changes
      end

      it "does not return the value in the changes" do
        expect(changes).to be_empty
      end

      it "is not flagged as changed" do
        expect(person).to_not be_changed
      end
    end

    context "when the value has changed" do

      before do
        person.aliases_will_change!
        person.aliases << "008"
      end

      let(:changes) do
        person.changes
      end

      it "returns the value in the changes" do
        expect(changes).to eq({ "aliases" => [[ "007" ], [ "007", "008" ]] })
      end
    end

    context "when the value is duplicable" do

      context "when the attribute has not been cloned" do

        before do
          person.aliases_will_change!
        end

        let(:changed) do
          person.changed_attributes
        end

        it "clones the value" do
          expect(changed["aliases"]).to_not equal(aliases)
        end

        it "puts the old value in the changes" do
          expect(changed["aliases"]).to eq(aliases)
        end
      end

      context "when the attribute has been flagged" do

        before do
          person.changed_attributes["aliases"] = aliases
          expect(aliases).to receive(:clone).never
          person.aliases_will_change!
        end

        let(:changed) do
          person.changed_attributes
        end

        it "does not clone the value" do
          expect(changed["aliases"]).to equal(aliases)
        end

        it "retains the first value in the changes" do
          expect(changed["aliases"]).to eq(aliases)
        end
      end
    end
  end

  describe "#changed" do

    context "when the document has changed" do

      let(:person) do
        Person.instantiate(title: "Grand Poobah")
      end

      before do
        person.title = "Captain Obvious"
      end

      it "returns an array of changed field names" do
        expect(person.changed).to include("title")
      end

    end

    context "When the document has changed but changed back to the original" do

      let(:person) do
        Person.instantiate(title: "Grand Poobah")
      end

      before do
        person.title = "Captain Obvious"
        person.title = nil
      end

      it "returns an array of changed field names" do
        expect(person.changed).not_to include("title")
      end

    end

    context "when the document has not changed" do

      let(:person) do
        Person.instantiate({})
      end

      it "does not include non changed fields" do
        expect(person.changed).to_not include("title")
      end
    end

    context "when the document is embedded" do

      let(:person) do
        Person.create!
      end

      let!(:name) do
        person.create_name(first_name: "Layne", last_name: "Staley")
      end

      context "when changing attributes via []" do

        before do
          person.name["a"] = "testing"
        end

        it "returns true" do
          expect(person.name).to be_changed
        end
      end
    end
  end

  describe "#changed?" do

    context "when the document has changed" do

      let(:person) do
        Person.new(title: "Grand Poobah")
      end

      before do
        person.title = "Captain Obvious"
      end

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

    context "when a hash field has been accessed" do

      context "when the field has not changed" do

        let(:person) do
          Person.create!(map: { name: "value" })
        end

        before do
          person.map
        end

        it "returns false" do
          expect(person).to_not be_changed
        end
      end

      context "when the field is changed" do

        let(:person) do
          Person.create!(map: { name: "value" })
        end

        before do
          person.map = { name: "another" }
        end

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

      context "when a dynamic field is changed in place" do

        let(:person) do
          Person.create!(other_name: { full: {first: 'first', last: 'last'} })
        end

        before do
          person.other_name[:full][:first] = 'Name'
        end

        it "returns true" do
          expect(person.changes).to_not be_empty
          expect(person).to be_changed
        end
      end
    end

    context "when the document has not changed" do

      let(:acolyte) do
        Acolyte.instantiate("_id" => BSON::ObjectId.new)
      end

      it "returns false" do
        expect(acolyte).to_not be_changed
      end
    end

    context "when a child has changed" do

      let(:person) do
        Person.create!
      end

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

      before do
        address.number = 10
      end

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

    context "when changed? has been called before child elements size change" do

      let(:person) do
        Person.create!
      end

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

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

      before do
        person.changed?
      end

      context "when adding via new" do

        before do
          address.locations.new
        end

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

      context "when adding via build" do

        before do
          address.locations.build
        end

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

      context "when adding via create" do

        before do
          address.locations.create!
        end

        it "returns false" do
          expect(person).to_not be_changed
        end
      end
    end

    context "when a deeply embedded child has changed" do

      let(:person) do
        Person.create!
      end

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

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

      before do
        location.name = "work"
      end

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

    context "when a child is new" do

      let(:person) do
        Person.create!
      end

      let!(:address) do
        person.addresses.build(street: "hobrecht")
      end

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

    context "when a deeply embedded child is new" do

      let(:person) do
        Person.create!
      end

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

      let!(:location) do
        address.locations.build(name: "home")
      end

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

  describe "#changes" do

    context "when the document has changed" do

      let(:person) do
        Person.instantiate(title: "Grand Poobah")
      end

      before do
        person.title = "Captain Obvious"
      end

      it "returns a hash of changes" do
        expect(person.changes["title"]).to eq(
          [ nil, "Captain Obvious" ]
        )
      end

      it "returns a hash with indifferent access" do
        expect(person.changes[:title]).to eq(
          [ nil, "Captain Obvious" ]
        )
      end
    end

    context 'when habtm association changes' do

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

      let(:user_account) do
        UserAccount.create!
      end

      before do
        person.user_accounts << user_account
      end

      it 'should not add to the changes or changed_attributes hash' do
        person.changes.should == {}
        person.changed_attributes.should == {}
      end
    end

    context 'when habtm association _ids changes' do

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

      let(:user_account) do
        UserAccount.create!
      end

      before do
        person.user_account_ids << user_account._id
      end

      it 'should add to the changes or changed_attributes hash' do
        person.changes.should == { "user_account_ids" => [ nil, [ user_account._id ] ] }
        person.changed_attributes.should == { "user_account_ids" => nil }
      end
    end

    context 'when assigning empty list to habtm association' do

      let(:person) do
        Person.create!(title: "Grand Poobah", user_accounts: [user_account])
      end

      let(:user_account) do
        UserAccount.create!
      end

      before do
        person.user_accounts = []
      end

      it 'should not add to the changes or changed_attributes hash' do
        person.changes.should == {}
        person.changed_attributes.should == {}
      end
    end

    context 'when assigning empty list to habtm association _ids' do

      let(:person) do
        Person.create!(title: "Grand Poobah", user_accounts: [user_account])
      end

      let(:user_account) do
        UserAccount.create!
      end

      before do
        person.user_account_ids = []
      end

      it 'should not add to the changes or changed_attributes hash' do
        person.changes.should == { "user_account_ids" => [ [ user_account._id ], [] ] }
        person.changed_attributes.should ==  { "user_account_ids" => [ user_account._id ] }
      end
    end

    context "when the document has not changed" do

      let(:acolyte) do
        Acolyte.instantiate("_id" => BSON::ObjectId.new)
      end

      it "returns an empty hash" do
        expect(acolyte.changes).to be_empty
      end
    end
  end

  describe "#setters" do

    context "when the document has changed" do

      let(:person) do
        Person.new(aliases: [ "007" ]).tap do |p|
          p.new_record = false
          p.move_changes
        end
      end

      context "when an array field has changed" do

        context "when the array has values removed" do

          before do
            person.aliases.delete_one("007")
          end

          let!(:setters) do
            person.setters
          end

          it "contains array changes in the setters" do
            expect(setters).to eq({ "aliases" => [] })
          end
        end

        context "when the array has values added" do

          before do
            person.aliases << "008"
          end

          let!(:setters) do
            person.setters
          end

          it "contains array changes in the setters" do
            expect(setters).to eq({ "aliases" => [ "007", "008" ] })
          end
        end

        context "when the array has changed completely" do

          before do
            person.aliases << "008"
            person.aliases.delete_one("007")
          end

          let!(:setters) do
            person.setters
          end

          it "does not contain array changes in the setters" do
            expect(setters).to eq({ "aliases" => [ "008" ]})
          end
        end
      end

      context "when the document is a root document" do

        let(:person) do
          Person.instantiate(title: "Grand Poobah")
        end

        before do
          person.title = "Captain Obvious"
        end

        it "returns a hash of field names and new values" do
          expect(person.setters["title"]).to eq("Captain Obvious")
        end
      end

      context "when the document is embedded" do

        let(:person) { Person.create(title: "Grand Poobah") }
        let(:address) { person.addresses.create(street: "Oxford St") }

        before { address.street = "Bond St" }

        it "returns a hash of field names and new values" do
          expect(address.setters).to eq(
            { "addresses.0.street" => "Bond St" }
          )
        end

        context "when the document is embedded multiple levels" do
          let(:location) { address.locations.create(name: "Home") }
          before { location.name = "Work" }
          
          it "returns the proper hash with locations" do
            expect(location.setters).to eq(
              { "addresses.0.locations.0.name" => "Work" }
            )
          end
        end
      end
    end

    context "when the document has not changed" do

      let(:acolyte) do
        Acolyte.instantiate("_id" => BSON::ObjectId.new)
      end

      it "returns an empty hash" do
        expect(acolyte.setters).to be_empty
      end
    end
  end

  describe "#previous_changes" do

    let(:person) do
      Person.new(title: "Grand Poobah")
    end

    before do
      person.title = "Captain Obvious"
    end

    context "when the document has been saved" do

      before do
        person.save!
      end

      it "returns the changes before the save" do
        expect(person.previous_changes["title"]).to eq(
          [ nil, "Captain Obvious" ]
        )
      end
    end

    context "when the document has not been saved" do

      it "returns an empty hash" do
        expect(person.previous_changes).to be_empty
      end
    end
  end

  describe "#move_changes" do

    let(:person) do
      Person.new(title: "Sir")
    end

    before do
      person.atomic_pulls["addresses"] = Address.new
      person.atomic_unsets << Address.new
      person.delayed_atomic_sets["addresses"] = Address.new
      person.move_changes
    end

    it "clears the atomic pulls" do
      expect(person.atomic_pulls).to be_empty
    end

    it "clears the atomic unsets" do
      expect(person.atomic_unsets).to be_empty
    end

    it "clears the delayed atomic sets" do
      expect(person.delayed_atomic_sets).to be_empty
    end

    it "clears the changed attributes" do
      expect(person.changed_attributes).to be_empty
    end
  end

  describe "#reset_attribute!" do

    context "when the attribute has changed" do

      let(:person) do
        Person.instantiate(title: "Grand Poobah")
      end

      before do
        person.title = "Captain Obvious"
        person.send(:reset_attribute!, "title")
      end

      it "resets the value to the original" do
        expect(person.title).to be_nil
      end

      it "allows access via reset_(attribute)!" do
        expect(person.title).to be_nil
      end

      it "removes the field from the changes" do
        expect(person.changed).to_not include("title")
      end

      context "when the field is aliased" do

        let(:person) do
          Person.instantiate(test: "Aliased 1")
        end

        before do
          person.test = "Aliased 2"
          person.send(:reset_attribute!, "test")
        end

        it "resets the value to the original" do
          expect(person.test).to be_nil
        end

        it "removes the field from the changes" do
          expect(person.changed).to_not include("test")
        end
      end
    end

    context "when the attribute has not changed" do

      let(:person) do
        Person.instantiate(title: "Grand Poobah")
      end

      before do
        person.send(:reset_attribute!, "title")
      end

      it "does nothing" do
        expect(person.title).to be_nil
      end
    end
  end

  describe "#reset_attribute_to_default!" do

    context "when a default is defined" do

      context "when the document is new" do

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

        before do
          person.reset_pets_to_default!
        end

        it "resets to the default value" do
          expect(person.pets).to eq(false)
        end
      end

      context "when the document is persisted" do

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

        before do
          person.reset_pets_to_default!
        end

        it "resets to the default value" do
          expect(person.pets).to eq(false)
        end

        it "flags the document dirty" do
          expect(person).to be_pets_changed
        end
      end
    end

    context "when a default is not defined" do

      context "when the document is new" do

        let(:person) do
          Person.new(title: "test")
        end

        before do
          person.reset_title_to_default!
        end

        it "resets to nil" do
          expect(person.title).to be_nil
        end
      end

      context "when the document is persisted" do

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

        before do
          person.reset_title_to_default!
        end

        it "resets to nil" do
          expect(person.title).to be_nil
        end

        it "flags the document dirty" do
          expect(person).to be_title_changed
        end
      end
    end
  end

  describe '#previously_changed?' do

    let(:person) do
      Person.new(title: "Grand Poobah")
    end

    before do
      person.title = "Captain Obvious"
    end

    context "when the document has been saved" do

      before do
        person.save!
      end

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

    context "when the document has not been saved" do

      it "returns false" do
        expect(person.title_previously_changed?).to be(false)
      end
    end
  end

  describe '#previous_change' do

    let(:person) do
      Person.new(title: "Grand Poobah")
    end

    before do
      person.title = "Captain Obvious"
    end

    context "when the document has been saved" do

      before do
        person.save!
      end

      it "returns the changes" do
        expect(person.title_previous_change).to eq([ nil, "Captain Obvious" ])
      end
    end

    context "when the document has not been saved" do

      it "returns no changes" do
        expect(person.title_previous_change).to eq(nil)
      end
    end
  end

  describe '#attribute_before_last_save' do
    let(:person) do
      Person.create!(title: "Grand Poobah")
    end

    before do
      person.title = "Captain Obvious"
    end

    context "when the document has been saved" do
      before do
        person.save!
      end

      it "returns the changes" do
        expect(person.attribute_before_last_save(:title)).to eq("Grand Poobah")
        expect(person.title_before_last_save).to eq("Grand Poobah")
      end
    end

    context "when the document has not been saved" do
      it "returns no changes" do
        expect(person.attribute_before_last_save(:title)).to be_nil
        expect(person.title_before_last_save).to be_nil
      end
    end
  end

  describe '#saved_change_to_attribute' do
    let(:person) do
      Person.create!(title: "Grand Poobah")
    end

    before do
      person.title = "Captain Obvious"
    end

    context "when the document has been saved" do
      before do
        person.save!
      end

      it "returns the changes" do
        expect(person.saved_change_to_attribute(:title)).to eq(["Grand Poobah", "Captain Obvious"])
        expect(person.saved_change_to_title).to eq(["Grand Poobah", "Captain Obvious"])
      end
    end

    context "when the document has not been saved" do
      it "returns changes for the previous save" do
        expect(person.saved_change_to_attribute(:title)).to eq([nil, "Grand Poobah"])
        expect(person.saved_change_to_title).to eq([nil, "Grand Poobah"])
      end
    end
  end

  describe '#saved_change_to_attribute?' do
    context "when the document has been saved" do
      let(:person) do
        Person.create!(title: "Grand Poobah")
      end

      before do
        person.title = "Captain Obvious"
      end

      before do
        person.save!
      end

      it "detects the changes" do
        expect(person.saved_change_to_attribute?(:title)).to be_truthy
        expect(person.saved_change_to_attribute?(:title, from: "Grand Poobah")).to be_truthy
        expect(person.saved_change_to_attribute?(:title, to: "Captain Obvious")).to be_truthy
        expect(person.saved_change_to_attribute?(:title, from: "Grand Poobah", to: "Captain Obvious")).to be_truthy
        expect(person.saved_change_to_title?(from: "Grand Poobah", to: "Captain Obvious")).to be_truthy
        expect(person.saved_change_to_attribute?(:age)).to be_falsey
        expect(person.saved_change_to_age?).to be_falsey
      end
    end

    context "when the document has not been saved" do
      let(:person) do
        Person.new(title: "Grand Poobah")
      end

      it "returns changes for the previous save" do
        expect(person.saved_change_to_attribute?(:title)).to be_falsey
        expect(person.saved_change_to_title?).to be_falsey
      end
    end
  end

  describe '#will_save_change_to_attribute?' do
    let(:person) do
      Person.create!(title: "Grand Poobah")
    end

    before do
      person.title = "Captain Obvious"
    end

    it 'correctly detects changes' do
      expect(person.will_save_change_to_attribute?(:title)).to eq(true)
      expect(person.will_save_change_to_title?).to eq(true)
      expect(person.will_save_change_to_attribute?(:score)).to eq(false)
      expect(person.will_save_change_to_score?).to eq(false)
    end

  end

  context "when fields have been defined pre-dirty inclusion" do

    let(:document) do
      Dokument.new
    end

    it "defines a _change method" do
      expect(document.updated_at_change).to be_nil
    end

    it "defines a _changed? method" do
      expect(document.updated_at_changed?).to be false
    end

    it "defines a _changes method" do
      expect(document.updated_at_was).to be_nil
    end
  end

  context "when only embedded documents change" do

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

    context "when the child is an embeds one" do

      context "when the child is new" do

        let!(:name) do
          person.build_name(first_name: "Gordon", last_name: "Ramsay")
        end

        it "flags the parent as changed" do
          expect(person).to be_changed
        end
      end

      context "when the child is modified" do

        let!(:name) do
          person.create_name(first_name: "Gordon", last_name: "Ramsay")
        end

        before do
          name.first_name = "G"
        end

        it "flags the parent as changed" do
          expect(person).to be_changed
        end
      end

      context "when the child is not modified" do

        let!(:name) do
          person.create_name(first_name: "Gordon", last_name: "Ramsay")
        end

        it "does not flag the parent as changed" do
          expect(person).to_not be_changed
        end
      end
    end

    context "when the child is an embeds many" do

      context "when a child is new" do

        let!(:address) do
          person.addresses.build(street: "jakobstr.")
        end

        it "flags the parent as changed" do
          expect(person).to be_changed
        end
      end

      context "when a child is modified" do

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

        before do
          address.city = "Berlin"
        end

        it "flags the parent as changed" do
          expect(person).to be_changed
        end
      end

      context "when no child is modified" do

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

        it "does not flag the parent as changed" do
          expect(person).to_not be_changed
        end
      end
    end
  end

  context "when changing a hash of hashes" do

    let!(:person) do
      Person.create!(map: { "test" => {}})
    end

    before do
      person.map["test"]["value"] = 10
    end

    it "records the changes" do
      expect(person.changes).to eq(
        { "map" => [{ "test" => {}}, { "test" => { "value" => 10 }}]}
      )
    end
  end

  context "when modifying a many to many key" do

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

    let!(:preference) do
      Preference.create!(name: "dirty")
    end

    before do
      person.update_attributes!(preference_ids: [ preference.id ])
    end

    it "records the foreign key dirty changes" do
      expect(person.previous_changes["preference_ids"]).to eq(
        [nil, [ preference.id ]]
      )
    end
  end

  context "when accessing an array field" do

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

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

    context "when the field is not changed" do

      before do
        from_db.preference_ids
      end

      it "flags the change" do
        expect(from_db.changes["preference_ids"]).to eq([ nil, []])
      end

      it "does not include the changes in the setters" do
        expect(from_db.setters).to be_empty
      end
    end
  end

  context "when reloading an unchanged document" do

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

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

    before do
      from_db.reload
    end

    it "clears the changed attributes" do
      expect(from_db.changed_attributes).to be_empty
    end
  end

  context "when fields are getting changed" do

    let(:person) do
      Person.create!(
        title: "MC",
        some_dynamic_field: 'blah'
      )
    end

    before do
      person.title = "DJ"
      person.write_attribute(:ssn, "222-22-2222")
      person.some_dynamic_field = 'bloop'
    end

    it "marks the document as changed" do
      expect(person).to be_changed
    end

    it "marks field changes" do
      expect(person.changes).to eq({
        "title" => [ "MC", "DJ" ],
        "ssn" => [ nil, "222-22-2222" ],
        "some_dynamic_field" => [ "blah", "bloop" ]
      })
    end

    it "marks changed fields" do
      expect(person.changed).to eq([ "title", "ssn", "some_dynamic_field" ])
    end

    it "marks the field as changed" do
      expect(person.title_changed?).to be true
    end

    it "stores previous field values" do
      expect(person.title_was).to eq("MC")
    end

    it "marks field changes" do
      expect(person.title_change).to eq([ "MC", "DJ" ])
    end

    it "allows reset of field changes" do
      person.reset_title!
      expect(person.title).to eq("MC")
      expect(person.changed).to eq([ "ssn", "some_dynamic_field" ])
    end

    context "after a save" do

      before do
        person.save!
      end

      it "clears changes" do
        expect(person).to_not be_changed
      end

      it "stores previous changes" do
        expect(person.previous_changes["title"]).to eq([ "MC", "DJ" ])
        expect(person.previous_changes["ssn"]).to eq([ nil, "222-22-2222" ])
      end
    end

    context "when the previous value is nil" do

      before do
        person.score = 100
        person.reset_score!
      end

      it "removes the attribute from the document" do
        expect(person.score).to be_nil
      end
    end
  end

  context "when accessing dirty attributes in callbacks" do

    context "when the document is persisted" do

      let!(:acolyte) do
        Acolyte.create!(name: "callback-test")
      end

      before do
        Acolyte.set_callback(:save, :after, if: :callback_test?) do |doc|
          doc[:changed_in_after_callback] = doc.changes.dup
        end

        Acolyte.set_callback(:save, :before, if: :callback_test?) do |doc|
          doc[:changed_in_before_callback] = doc.changes.dup
        end
      end

      after do
        Acolyte._save_callbacks.select do |callback|
          [:before, :after].include?(callback.kind)
        end.each do |callback|
          Acolyte._save_callbacks.delete(callback)
        end
      end

      it "does not retain the changes until after all callbacks" do
        acolyte.update_attribute(:status, "testing")
        expect(acolyte.changed_in_before_callback).to eq({"status"=>[nil, "testing"]})
        expect(acolyte.changed_in_after_callback).to eq({  })
      end
    end

    context "when the document is new" do

      let!(:acolyte) do
        Acolyte.new(name: "callback-test")
      end

      before do
        Acolyte.set_callback(:save, :after, if: :callback_test?) do |doc|
          doc[:changed_after_in_callback] = doc.changes.dup
        end

        Acolyte.set_callback(:save, :before, if: :callback_test?) do |doc|
          doc[:changed_before_in_callback] = doc.changes.dup
        end
      end

      after do
        Acolyte._save_callbacks.select do |callback|
          [:before, :after].include?(callback.kind)
        end.each do |callback|
          Acolyte._save_callbacks.delete(callback)
        end
      end

      it "does not retain the changes until after all callbacks" do
        acolyte.save!
        expect(acolyte.changed_before_in_callback["name"]).to eq([ nil, "callback-test" ])
        expect(acolyte.changed_after_in_callback["name"]).to be_nil
      end
    end
  end

  context "when associations are getting changed" do

    let(:person) do
      Person.create!(addresses: [ Address.new ])
    end

    before do
      person.addresses = [ Address.new ]
    end

    it "does not set the association to nil when hitting the database" do
      expect(person.setters).to_not eq({ "addresses" => nil })
    end
  end

  context 'when nesting deeply embedded documents' do

    context 'when persisting the root document' do

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

      it 'is not marked as changed' do
        expect(person).to_not be_changed
      end

      context 'when creating a new first level embedded document' do

        let!(:address) do
          person.addresses.new(street: 'goltzstr.')
        end

        it 'flags the root document as changed' do
          expect(person).to be_changed
        end

        it 'flags the first level child as changed' do
          expect(address).to be_changed
        end

        context 'when building the lowest level document' do

          before do
            person.save!
          end

          let!(:code) do
            address.build_code
          end

          it 'flags the root document as changed' do
            expect(person).to be_changed
          end

          it 'flags the first level embedded document as changed' do
            expect(address).to be_changed
          end

          it 'flags the lowest level embedded document as changed' do
            expect(code).to be_changed
          end

          context 'when saving the hierarchy' do

            before do
              person.save!
            end

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

            it 'saves the first embedded document' do
              expect(reloaded.addresses.first).to eq(address)
            end

            it 'saves the lowest level embedded document' do
              expect(reloaded.addresses.first.code).to eq(code)
            end

            context 'when embedding further' do

              let!(:deepest) do
                reloaded.addresses.first.code.build_deepest
              end

              before do
                reloaded.save!
              end

              it 'saves the deepest embedded document' do
                expect(reloaded.reload.addresses.first.code.deepest).to eq(deepest)
              end
            end
          end
        end
      end
    end
  end
end
