require 'test_helper'
require 'time_travel_helper'

class HasPaperTrailModelTest < ActiveSupport::TestCase

  context "A record with defined 'only' and 'ignore' attributes" do
    setup { @article = Article.create }
    should 'creation should change the number of versions' do assert_equal(1, PaperTrail::Version.count) end

    context 'which updates an ignored column' do
      setup { @article.update_attributes :title => 'My first title' }
      should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
    end

    context 'which updates an ignored column with truly Proc' do
      setup { @article.update_attributes :abstract => 'ignore abstract' }
      should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
    end

    context 'which updates an ignored column with falsy Proc' do
      setup { @article.update_attributes :abstract => 'do not ignore abstract!' }
      should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end
    end

    context 'which updates an ignored column, ignored with truly Proc and a selected column' do
      setup { @article.update_attributes :title => 'My first title',
                                         :content => 'Some text here.',
                                         :abstract => 'ignore abstract'
      }
      should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end

      should "show the new version in the model's `versions` association" do
        assert_equal(2, @article.versions.size)
      end

      should 'have stored only non-ignored attributes' do
        assert_equal ({'content' => [nil, 'Some text here.']}), @article.versions.last.changeset
      end
    end

    context 'which updates an ignored column, ignored with falsy Proc and a selected column' do
      setup { @article.update_attributes :title => 'My first title',
                                         :content => 'Some text here.',
                                         :abstract => 'do not ignore abstract'
      }
      should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end

      should "show the new version in the model's `versions` association" do
        assert_equal(2, @article.versions.size)
      end

      should 'have stored only non-ignored attributes' do
        assert_equal ({'content' => [nil, 'Some text here.'], 'abstract' => [nil, 'do not ignore abstract']}), @article.versions.last.changeset
      end
    end

    context 'which updates a selected column' do
      setup { @article.update_attributes :content => 'Some text here.' }
      should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end

      should "show the new version in the model's `versions` association" do
        assert_equal(2, @article.versions.size)
      end
    end

    context 'which updates a non-ignored and non-selected column' do
      setup { @article.update_attributes :abstract => 'Other abstract'}
      should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
    end

    context 'which updates a skipped column' do
      setup { @article.update_attributes :file_upload => 'Your data goes here' }
      should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
    end

    context 'which updates a skipped column and a selected column' do
      setup { @article.update_attributes :file_upload => 'Your data goes here', :content => 'Some text here.' }
      should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end

      should "show the new version in the model's `versions` association" do
        assert_equal(2, @article.versions.size)
      end

      should 'have stored only non-skipped attributes' do
        assert_equal ({'content' => [nil, 'Some text here.']}), @article.versions.last.changeset
      end

      context 'and when updated again' do
        setup do
          @article.update_attributes :file_upload => 'More data goes here', :content => 'More text here.'
          @old_article = @article.versions.last
        end

        should 'have removed the skipped attributes when saving the previous version' do
          assert_equal nil, PaperTrail.serializer.load(@old_article.object)['file_upload']
        end

        should 'have kept the non-skipped attributes in the previous version' do
          assert_equal 'Some text here.', PaperTrail.serializer.load(@old_article.object)['content']
        end
      end
    end

    context 'which gets destroyed' do
      setup { @article.destroy }
      should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end

      should "show the new version in the model's `versions` association" do
        assert_equal(2, @article.versions.size)
      end
    end
  end

  context "A record with defined 'ignore' attribute" do
    setup { @legacy_widget = LegacyWidget.create }

    context 'which updates an ignored column' do
      setup { @legacy_widget.update_attributes :version => 1 }
      should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
    end
  end

  context 'A record with defined "if" and "unless" attributes' do
    setup { @translation = Translation.new :headline => 'Headline' }

    context 'for non-US translations' do
      setup { @translation.save }
      should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end

      context 'after update' do
        setup { @translation.update_attributes :content => 'Content' }
        should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
      end

      context 'after destroy' do
        setup { @translation.destroy }
        should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
      end
    end

    context 'for US translations' do
      setup { @translation.language_code = "US" }

      context 'that are drafts' do
        setup do
          @translation.type = 'DRAFT'
          @translation.save
        end

        should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end

        context 'after update' do
          setup { @translation.update_attributes :content => 'Content' }
          should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
        end
      end

      context 'that are not drafts' do
        setup { @translation.save }

        should 'change the number of versions' do assert_equal(1, PaperTrail::Version.count) end

        context 'after update' do
          setup { @translation.update_attributes :content => 'Content' }
          should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end

          should "show the new version in the model's `versions` association" do
            assert_equal(2, @translation.versions.size)
          end
        end

        context 'after destroy' do
          setup { @translation.destroy }
          should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end

          should "show the new version in the model's `versions` association" do
            assert_equal(2, @translation.versions.size)
          end
        end
      end
    end
  end

  context 'A new record' do
    setup { @widget = Widget.new }

    should 'not have any previous versions' do
      assert_equal [], @widget.versions
    end

    should 'be live' do
      assert @widget.live?
    end

    context 'which is then created' do
      setup { @widget.update_attributes :name => 'Henry', :created_at => Time.now - 1.day }

      should 'have one previous version' do
        assert_equal 1, @widget.versions.length
      end

      should 'be nil in its previous version' do
        assert_nil @widget.versions.first.object
        assert_nil @widget.versions.first.reify
      end

      should 'record the correct event' do
        assert_match /create/i, @widget.versions.first.event
      end

      should 'be live' do
        assert @widget.live?
      end

      should "use the widget `updated_at` as the version's `created_at`" do
        assert_equal @widget.updated_at.to_i, @widget.versions.first.created_at.to_i
      end

      should 'have changes' do

        #TODO Postgres does not appear to pass back ActiveSupport::TimeWithZone,
        # so chosing the lowest common denominator to test.

        changes = {
          'name'       => [nil, 'Henry'],
          'created_at' => [nil, @widget.created_at.to_time.utc],
          'updated_at' => [nil, @widget.updated_at.to_time.utc],
          'id'         => [nil, @widget.id]
        }

        assert_equal Time, @widget.versions.last.changeset['updated_at'][1].class
        assert_equal changes, @widget.versions.last.changeset
      end

      context 'and then updated without any changes' do
        setup { @widget.touch }

        should 'not have a new version' do
          assert_equal 1, @widget.versions.length
        end
      end


      context 'and then updated with changes' do
        setup { @widget.update_attributes :name => 'Harry' }

        should 'have two previous versions' do
          assert_equal 2, @widget.versions.length
        end

        should 'be available in its previous version' do
          assert_equal 'Harry', @widget.name
          assert_not_nil @widget.versions.last.object
          widget = @widget.versions.last.reify
          assert_equal 'Henry', widget.name
          assert_equal 'Harry', @widget.name
        end

        should 'have the same ID in its previous version' do
          assert_equal @widget.id, @widget.versions.last.reify.id
        end

        should 'record the correct event' do
          assert_match /update/i, @widget.versions.last.event
        end

        should 'have versions that are not live' do
          assert @widget.versions.map(&:reify).compact.all? { |w| !w.live? }
        end

        should 'have stored changes' do
          # Behavior for ActiveRecord 4 is different than ActiveRecord 3;
          # AR4 includes the `updated_at` column in changes for updates, which is why we reject it from the right side of this assertion.
          assert_equal ({'name' => ['Henry', 'Harry']}), PaperTrail.serializer.load(@widget.versions.last.object_changes).reject { |k,v| k.to_sym == :updated_at }
          assert_equal ({'name' => ['Henry', 'Harry']}), @widget.versions.last.changeset.reject { |k,v| k.to_sym == :updated_at }
        end

        should 'return changes with indifferent access' do
          assert_equal ['Henry', 'Harry'], @widget.versions.last.changeset[:name]
          assert_equal ['Henry', 'Harry'], @widget.versions.last.changeset['name']
        end

        if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
          should 'not clobber the IdentityMap when reifying' do
            module ActiveRecord::IdentityMap
              class << self
                alias :__without :without
                def without(&block)
                  @unclobbered = true
                  __without(&block)
                end
              end
            end

            @widget.versions.last.reify
            assert ActiveRecord::IdentityMap.instance_variable_get("@unclobbered")
          end
        end

        context 'and has one associated object' do
          setup do
            @wotsit = @widget.create_wotsit :name => 'John'
          end

          should 'not copy the has_one association by default when reifying' do
            reified_widget = @widget.versions.last.reify
            assert_equal @wotsit, reified_widget.wotsit  # association hasn't been affected by reifying
            assert_equal @wotsit, @widget.wotsit(true)  # confirm that the association is correct
          end

          should 'copy the has_one association when reifying with :has_one => true' do
            reified_widget = @widget.versions.last.reify(:has_one => true)
            assert_nil reified_widget.wotsit  # wotsit wasn't there at the last version
            assert_equal @wotsit, @widget.wotsit(true)  # wotsit should still exist on live object
          end
        end


        context 'and has many associated objects' do
          setup do
            @f0 = @widget.fluxors.create :name => 'f-zero'
            @f1 = @widget.fluxors.create :name => 'f-one'
            @reified_widget = @widget.versions.last.reify
          end

          should 'copy the has_many associations when reifying' do
            assert_equal @widget.fluxors.length, @reified_widget.fluxors.length
            assert_same_elements @widget.fluxors, @reified_widget.fluxors

            assert_equal @widget.versions.length, @reified_widget.versions.length
            assert_same_elements @widget.versions, @reified_widget.versions
          end
        end

        context 'and has many associated polymorphic objects' do
          setup do
            @f0 = @widget.whatchamajiggers.create :name => 'f-zero'
            @f1 = @widget.whatchamajiggers.create :name => 'f-zero'
            @reified_widget = @widget.versions.last.reify
          end

          should 'copy the has_many associations when reifying' do
            assert_equal @widget.whatchamajiggers.length, @reified_widget.whatchamajiggers.length
            assert_same_elements @widget.whatchamajiggers, @reified_widget.whatchamajiggers

            assert_equal @widget.versions.length, @reified_widget.versions.length
            assert_same_elements @widget.versions, @reified_widget.versions
          end
        end

        context 'polymorphic objects by themselves' do
          setup do
            @widget = Whatchamajigger.new :name => 'f-zero'
          end

          should 'not fail with a nil pointer on the polymorphic association' do
            @widget.save!
          end
        end

        context 'and then destroyed' do
          setup do
            @fluxor = @widget.fluxors.create :name => 'flux'
            @widget.destroy
            @reified_widget = PaperTrail::Version.last.reify
          end

          should 'record the correct event' do
            assert_match /destroy/i, PaperTrail::Version.last.event
          end

          should 'have three previous versions' do
            assert_equal 3, PaperTrail::Version.with_item_keys('Widget', @widget.id).length
          end

          should 'be available in its previous version' do
            assert_equal @widget.id, @reified_widget.id
            assert_equal @widget.attributes, @reified_widget.attributes
          end

          should 'be re-creatable from its previous version' do
            assert @reified_widget.save
          end

          should 'restore its associations on its previous version' do
            @reified_widget.save
            assert_equal 1, @reified_widget.fluxors.length
          end

          should 'not have changes' do
            assert_equal Hash.new, @widget.versions.last.changeset
          end
        end
      end
    end
  end


  # Test the serialisation and deserialisation.
  # TODO: binary
  context "A record's papertrail" do
    setup do
      @date_time = DateTime.now.utc
      @time = Time.now
      @date = Date.new 2009, 5, 29
      @widget = Widget.create :name        => 'Warble',
                              :a_text      => 'The quick brown fox',
                              :an_integer  => 42,
                              :a_float     => 153.01,
                              :a_decimal   => 2.71828,
                              :a_datetime  => @date_time,
                              :a_time      => @time,
                              :a_date      => @date,
                              :a_boolean   => true

      @widget.update_attributes :name      => nil,
                              :a_text      => nil,
                              :an_integer  => nil,
                              :a_float     => nil,
                              :a_decimal   => nil,
                              :a_datetime  => nil,
                              :a_time      => nil,
                              :a_date      => nil,
                              :a_boolean   => false
      @previous = @widget.versions.last.reify
    end

    should 'handle strings' do
      assert_equal 'Warble', @previous.name
    end

    should 'handle text' do
      assert_equal 'The quick brown fox', @previous.a_text
    end

    should 'handle integers' do
      assert_equal 42, @previous.an_integer
    end

    should 'handle floats' do
      assert_in_delta 153.01, @previous.a_float, 0.001
    end

    should 'handle decimals' do
      assert_in_delta 2.7183, @previous.a_decimal, 0.0001
    end

    should 'handle datetimes' do
      assert_equal @date_time.to_time.utc.to_i, @previous.a_datetime.to_time.utc.to_i
    end

    should 'handle times' do
      assert_equal @time.utc.to_i, @previous.a_time.utc.to_i
    end

    should 'handle dates' do
      assert_equal @date, @previous.a_date
    end

    should 'handle booleans' do
      assert @previous.a_boolean
    end


    context "after a column is removed from the record's schema" do
      setup do
        change_schema
        Widget.connection.schema_cache.clear!
        Widget.reset_column_information
        assert_raise(NoMethodError) { Widget.new.sacrificial_column }
        @last = @widget.versions.last
      end

      teardown do
        restore_schema
      end

      should 'reify previous version' do
        assert_kind_of Widget, @last.reify
      end

      should 'restore all forward-compatible attributes' do
        assert_equal    'Warble',                    @last.reify.name
        assert_equal    'The quick brown fox',       @last.reify.a_text
        assert_equal    42,                          @last.reify.an_integer
        assert_in_delta 153.01,                      @last.reify.a_float,   0.001
        assert_in_delta 2.7183,                      @last.reify.a_decimal, 0.0001
        assert_equal    @date_time.to_time.utc.to_i, @last.reify.a_datetime.to_time.utc.to_i
        assert_equal    @time.utc.to_i,              @last.reify.a_time.utc.to_i
        assert_equal    @date,                       @last.reify.a_date
        assert          @last.reify.a_boolean
      end
    end
  end


  context 'A record' do
    setup { @widget = Widget.create :name => 'Zaphod' }

    context 'with PaperTrail globally disabled' do
      setup do
        PaperTrail.enabled = false
        @count = @widget.versions.length
      end

      teardown { PaperTrail.enabled = true }

      context 'when updated' do
        setup { @widget.update_attributes :name => 'Beeblebrox' }

        should 'not add to its trail' do
          assert_equal @count, @widget.versions.length
        end
      end
    end

    context 'with its paper trail turned off' do
      setup do
        Widget.paper_trail_off!
        @count = @widget.versions.length
      end

      teardown { Widget.paper_trail_on! }

      context 'when updated' do
        setup { @widget.update_attributes :name => 'Beeblebrox' }

        should 'not add to its trail' do
          assert_equal @count, @widget.versions.length
        end
      end

      context 'when destroyed "without versioning"' do
        should 'leave paper trail off after call' do
          @widget.without_versioning :destroy
          assert !Widget.paper_trail_enabled_for_model?
        end
      end

      context 'and then its paper trail turned on' do
        setup { Widget.paper_trail_on! }

        context 'when updated' do
          setup { @widget.update_attributes :name => 'Ford' }

          should 'add to its trail' do
            assert_equal @count + 1, @widget.versions.length
          end
        end

        context 'when updated "without versioning"' do
          setup do
            @widget.without_versioning do
              @widget.update_attributes :name => 'Ford'
            end
            # The model instance should yield itself for convenience purposes
            @widget.without_versioning { |w| w.update_attributes :name => 'Nixon' }
          end

          should 'not create new version' do
            assert_equal @count, @widget.versions.length
          end

          should 'enable paper trail after call' do
            assert Widget.paper_trail_enabled_for_model?
          end
        end

        context 'when receiving a method name as an argument' do
          setup { @widget.without_versioning(:touch_with_version) }

          should 'not create new version' do
            assert_equal @count, @widget.versions.length
          end

          should 'enable paper trail after call' do
            assert Widget.paper_trail_enabled_for_model?
          end
        end
      end
    end
  end


  context 'A papertrail with somebody making changes' do
    setup do
      @widget = Widget.new :name => 'Fidget'
    end

    context 'when a record is created' do
      setup do
        PaperTrail.whodunnit = 'Alice'
        @widget.save
        @version = @widget.versions.last  # only 1 version
      end

      should 'track who made the change' do
        assert_equal 'Alice', @version.whodunnit
        assert_nil   @version.paper_trail_originator
        assert_equal 'Alice', @version.terminator
        assert_equal 'Alice', @widget.paper_trail_originator
      end

      context 'when a record is updated' do
        setup do
          PaperTrail.whodunnit = 'Bob'
          @widget.update_attributes :name => 'Rivet'
          @version = @widget.versions.last
        end

        should 'track who made the change' do
          assert_equal 'Bob',   @version.whodunnit
          assert_equal 'Alice', @version.paper_trail_originator
          assert_equal 'Bob',   @version.terminator
          assert_equal 'Bob',   @widget.paper_trail_originator
        end

        context 'when a record is destroyed' do
          setup do
            PaperTrail.whodunnit = 'Charlie'
            @widget.destroy
            @version = PaperTrail::Version.last
          end

          should 'track who made the change' do
            assert_equal 'Charlie', @version.whodunnit
            assert_equal 'Bob',     @version.paper_trail_originator
            assert_equal 'Charlie', @version.terminator
            assert_equal 'Charlie', @widget.paper_trail_originator
          end
        end
      end
    end
  end


  context 'Timestamps' do
    setup do
      @wotsit = Wotsit.create! :name => 'wotsit'
    end

    should 'record timestamps' do
      @wotsit.update_attributes! :name => 'changed'
      assert_not_nil @wotsit.versions.last.reify.created_at
      assert_not_nil @wotsit.versions.last.reify.updated_at
    end

    should 'not generate warning' do
      # Tests that it doesn't try to write created_on as an attribute just because a created_on
      # method exists.
      warnings = capture(:stderr) {  # Deprecation warning in Rails 3.2
        assert_nothing_raised {  # ActiveModel::MissingAttributeError in Rails 4
          @wotsit.update_attributes! :name => 'changed'
        }
      }
      assert_equal '', warnings
    end

  end


  context 'A subclass' do
    setup do
      @foo = FooWidget.create
      @foo.update_attributes! :name => 'Foo'
    end

    should 'reify with the correct type' do
      # For some reason this test appears to be broken on AR4 in the test env. Executing it manually in the Rails console seems to work.. not sure what the issues is here.
      assert_kind_of FooWidget, @foo.versions.last.reify if ActiveRecord::VERSION::MAJOR < 4
      assert_equal @foo.versions.first, PaperTrail::Version.last.previous
      assert_nil PaperTrail::Version.last.next
    end

    should 'should return the correct originator' do
      PaperTrail.whodunnit = 'Ben'
      @foo.update_attribute(:name, 'Geoffrey')
      assert_equal PaperTrail.whodunnit, @foo.paper_trail_originator
    end

    context 'when destroyed' do
      setup { @foo.destroy }

      should 'reify with the correct type' do
        assert_kind_of FooWidget, @foo.versions.last.reify
        assert_equal @foo.versions[1], PaperTrail::Version.last.previous
        assert_nil PaperTrail::Version.last.next
      end
    end
  end


  context 'An item with versions' do
    setup do
      @widget = Widget.create :name => 'Widget'
      @widget.update_attributes :name => 'Fidget'
      @widget.update_attributes :name => 'Digit'
    end

    context 'which were created over time' do
      setup do
        @created       = 2.days.ago
        @first_update  = 1.day.ago
        @second_update = 1.hour.ago
        @widget.versions[0].update_attributes :created_at => @created
        @widget.versions[1].update_attributes :created_at => @first_update
        @widget.versions[2].update_attributes :created_at => @second_update
        @widget.update_attribute :updated_at, @second_update
      end

      should 'return nil for version_at before it was created' do
        assert_nil @widget.version_at(@created - 1)
      end

      should 'return how it looked when created for version_at its creation' do
        assert_equal 'Widget', @widget.version_at(@created).name
      end

      should "return how it looked when created for version_at just before its first update" do
        assert_equal 'Widget', @widget.version_at(@first_update - 1).name
      end

      should "return how it looked when first updated for version_at its first update" do
        assert_equal 'Fidget', @widget.version_at(@first_update).name
      end

      should 'return how it looked when first updated for version_at just before its second update' do
        assert_equal 'Fidget', @widget.version_at(@second_update - 1).name
      end

      should 'return how it looked when subsequently updated for version_at its second update' do
        assert_equal 'Digit', @widget.version_at(@second_update).name
      end

      should 'return the current object for version_at after latest update' do
        assert_equal 'Digit', @widget.version_at(1.day.from_now).name
      end

      context 'passing in a string representation of a timestamp' do
        should 'still return a widget when appropriate' do
          # need to add 1 second onto the timestamps before casting to a string, since casting a Time to a string drops the microseconds
          assert_equal 'Widget', @widget.version_at((@created + 1.second).to_s).name
          assert_equal 'Fidget', @widget.version_at((@first_update + 1.second).to_s).name
          assert_equal 'Digit', @widget.version_at((@second_update + 1.second).to_s).name
        end
      end
    end

    context '.versions_between' do
      setup do
        @created       = 30.days.ago
        @first_update  = 15.days.ago
        @second_update = 1.day.ago
        @widget.versions[0].update_attributes :created_at => @created
        @widget.versions[1].update_attributes :created_at => @first_update
        @widget.versions[2].update_attributes :created_at => @second_update
        @widget.update_attribute :updated_at, @second_update
      end

      should 'return versions in the time period' do
        assert_equal ['Fidget'], @widget.versions_between(20.days.ago, 10.days.ago).map(&:name)
        assert_equal ['Widget', 'Fidget'], @widget.versions_between(45.days.ago, 10.days.ago).map(&:name)
        assert_equal ['Fidget', 'Digit', 'Digit'], @widget.versions_between(16.days.ago, 1.minute.ago).map(&:name)
        assert_equal [], @widget.versions_between(60.days.ago, 45.days.ago).map(&:name)
      end
    end

    context 'on the first version' do
      setup { @version = @widget.versions.first }

      should 'have a nil previous version' do
        assert_nil @version.previous
      end

      should 'return the next version' do
        assert_equal @widget.versions[1], @version.next
      end

      should 'return the correct index' do
        assert_equal 0, @version.index
      end
    end

    context 'on the last version' do
      setup { @version = @widget.versions.last }

      should 'return the previous version' do
        assert_equal @widget.versions[@widget.versions.length - 2], @version.previous
      end

      should 'have a nil next version' do
        assert_nil @version.next
      end

      should 'return the correct index' do
        assert_equal @widget.versions.length - 1, @version.index
      end
    end
  end


  context 'An item' do
    setup do
      @initial_title = 'Foobar'
      @article = Article.new :title => @initial_title
    end

    context 'which is created' do
      setup { @article.save }

      should 'store fixed meta data' do
        assert_equal 42, @article.versions.last.answer
      end

      should 'store dynamic meta data which is independent of the item' do
        assert_equal '31 + 11 = 42', @article.versions.last.question
      end

      should 'store dynamic meta data which depends on the item' do
        assert_equal @article.id, @article.versions.last.article_id
      end

      should 'store dynamic meta data based on a method of the item' do
        assert_equal @article.action_data_provider_method, @article.versions.last.action
      end

      should 'store dynamic meta data based on an attribute of the item at creation' do
        assert_equal @initial_title, @article.versions.last.title
      end


      context 'and updated' do
        setup do
          @article.update_attributes! :content => 'Better text.', :title => 'Rhubarb'
        end

        should 'store fixed meta data' do
          assert_equal 42, @article.versions.last.answer
        end

        should 'store dynamic meta data which is independent of the item' do
          assert_equal '31 + 11 = 42', @article.versions.last.question
        end

        should 'store dynamic meta data which depends on the item' do
          assert_equal @article.id, @article.versions.last.article_id
        end

        should 'store dynamic meta data based on an attribute of the item prior to the update' do
          assert_equal @initial_title, @article.versions.last.title
        end
      end


      context 'and destroyed' do
        setup { @article.destroy }

        should 'store fixed meta data' do
          assert_equal 42, @article.versions.last.answer
        end

        should 'store dynamic meta data which is independent of the item' do
          assert_equal '31 + 11 = 42', @article.versions.last.question
        end

        should 'store dynamic meta data which depends on the item' do
          assert_equal @article.id, @article.versions.last.article_id
        end

        should 'store dynamic meta data based on an attribute of the item prior to the destruction' do
          assert_equal @initial_title, @article.versions.last.title
        end
      end
    end
  end

  context 'A reified item' do
    setup do
      widget = Widget.create :name => 'Bob'
      %w( Tom Dick Jane ).each { |name| widget.update_attributes :name => name }
      @version = widget.versions.last
      @widget = @version.reify
    end

    should 'know which version it came from' do
      assert_equal @version, @widget.version
    end

    should 'return its previous self' do
      assert_equal @widget.versions[-2].reify, @widget.previous_version
    end
  end


  context 'A non-reified item' do
    setup { @widget = Widget.new }

    should 'not have a previous version' do
      assert_nil @widget.previous_version
    end

    should 'not have a next version' do
      assert_nil @widget.next_version
    end

    context 'with versions' do
      setup do
        @widget.save
        %w( Tom Dick Jane ).each { |name| @widget.update_attributes :name => name }
      end

      should 'have a previous version' do
        assert_equal @widget.versions.last.reify.name, @widget.previous_version.name
      end

      should 'not have a next version' do
        assert_nil @widget.next_version
      end
    end
  end

  context 'A reified item' do
    setup do
      @widget = Widget.create :name => 'Bob'
      %w(Tom Dick Jane).each { |name| @widget.update_attributes :name => name }
      @second_widget = @widget.versions[1].reify  # first widget is `nil`
      @last_widget   = @widget.versions.last.reify
    end

    should 'have a previous version' do
      assert_nil @second_widget.previous_version # `create` events return `nil` for `reify`
      assert_equal @widget.versions[-2].reify.name, @last_widget.previous_version.name
    end

    should 'have a next version' do
      assert_equal @widget.versions[2].reify.name, @second_widget.next_version.name
      assert_equal @last_widget.next_version.name, @widget.name
    end
  end

  context ":has_many :through" do
    setup do
      @book = Book.create :title => 'War and Peace'
      @dostoyevsky  = Person.create :name => 'Dostoyevsky'
      @solzhenitsyn = Person.create :name => 'Solzhenitsyn'
    end

    should 'store version on source <<' do
      count = PaperTrail::Version.count
      @book.authors << @dostoyevsky
      assert_equal 1, PaperTrail::Version.count - count
      assert_equal PaperTrail::Version.last, @book.authorships.first.versions.first
    end

    should 'store version on source create' do
      count = PaperTrail::Version.count
      @book.authors.create :name => 'Tolstoy'
      assert_equal 2, PaperTrail::Version.count - count
      assert_same_elements [Person.last, Authorship.last], [PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
    end

    should 'store version on join destroy' do
      @book.authors << @dostoyevsky
      count = PaperTrail::Version.count
      @book.authorships(true).last.destroy
      assert_equal 1, PaperTrail::Version.count - count
      assert_equal @book, PaperTrail::Version.last.reify.book
      assert_equal @dostoyevsky, PaperTrail::Version.last.reify.person
    end

    should 'store version on join clear' do
      @book.authors << @dostoyevsky
      count = PaperTrail::Version.count
      @book.authorships(true).destroy_all
      assert_equal 1, PaperTrail::Version.count - count
      assert_equal @book, PaperTrail::Version.last.reify.book
      assert_equal @dostoyevsky, PaperTrail::Version.last.reify.person
    end
  end

  # `serialized_attributes` is deprecated in ActiveRecord 5.0
  if ::ActiveRecord::VERSION::MAJOR < 5
    context 'When an attribute has a custom serializer' do
      setup do
        PaperTrail.config.serialized_attributes = true
        @person = Person.new(:time_zone => "Samoa")
      end

      teardown { PaperTrail.config.serialized_attributes = false }

      should "be an instance of ActiveSupport::TimeZone" do
        assert_equal ActiveSupport::TimeZone, @person.time_zone.class
      end

      context 'when the model is saved' do
        setup do
          @changes_before_save = @person.changes.dup
          @person.save!
        end

        # Test for serialization:
        should 'version.object_changes should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
          assert @person.versions.last.object_changes.length < 105, "object_changes length was #{@person.versions.last.object_changes.length}"
        end
        # It should store the serialized value.
        should 'version.object_changes attribute should have stored the value returned by the attribute serializer' do
          as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object_changes)]
          assert_equal [nil, 'Samoa'], as_stored_in_version[:time_zone]
          serialized_value = Person::TimeZoneSerializer.dump(@person.time_zone)
          assert_equal serialized_value, as_stored_in_version[:time_zone].last
        end

        # Tests for unserialization:
        should 'version.changeset should convert the attribute value back to its original, unserialized value' do
          unserialized_value = Person::TimeZoneSerializer.load(@person.time_zone)
          assert_equal unserialized_value, @person.versions.last.changeset[:time_zone].last
        end
        should "record.changes (before save) returns the original, unserialized values" do
          assert_equal [NilClass, ActiveSupport::TimeZone], @changes_before_save[:time_zone].map(&:class)
        end
        should 'version.changeset should be the same as record.changes was before the save' do
          assert_equal @changes_before_save, @person.versions.last.changeset.delete_if { |key, val| key.to_sym == :id }
          assert_equal [NilClass, ActiveSupport::TimeZone], @person.versions.last.changeset[:time_zone].map(&:class)
        end

        context 'when that attribute is updated' do
          setup do
            @attribute_value_before_change = @person.time_zone
            @person.assign_attributes({ :time_zone => 'Pacific Time (US & Canada)' })
            @changes_before_save = @person.changes.dup
            @person.save!
          end

          # Tests for serialization:
          # Before the serialized attributes fix, the object/object_changes value that was stored was ridiculously long (58723).
          should 'version.object should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
            assert @person.versions.last.object.length < 105, "object length was #{@person.versions.last.object.length}"
          end
          # Need an additional clause to detect what version of ActiveRecord is being used for this test because AR4 injects the `updated_at` column into the changeset for updates to models
          should 'version.object_changes should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
            assert @person.versions.last.object_changes.length < (ActiveRecord::VERSION::MAJOR < 4 ? 105 : 118), "object_changes length was #{@person.versions.last.object_changes.length}"
          end
          # But now it stores the short, serialized value.
          should 'version.object attribute should have stored the value returned by the attribute serializer' do
            as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object)]
            assert_equal 'Samoa', as_stored_in_version[:time_zone]
            serialized_value = Person::TimeZoneSerializer.dump(@attribute_value_before_change)
            assert_equal serialized_value, as_stored_in_version[:time_zone]
          end
          should 'version.object_changes attribute should have stored the value returned by the attribute serializer' do
            as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object_changes)]
            assert_equal ['Samoa', 'Pacific Time (US & Canada)'], as_stored_in_version[:time_zone]
            serialized_value = Person::TimeZoneSerializer.dump(@person.time_zone)
            assert_equal serialized_value, as_stored_in_version[:time_zone].last
          end

          # Tests for unserialization:
          should 'version.reify should convert the attribute value back to its original, unserialized value' do
            unserialized_value = Person::TimeZoneSerializer.load(@attribute_value_before_change)
            assert_equal unserialized_value, @person.versions.last.reify.time_zone
          end
          should 'version.changeset should convert the attribute value back to its original, unserialized value' do
            unserialized_value = Person::TimeZoneSerializer.load(@person.time_zone)
            assert_equal unserialized_value, @person.versions.last.changeset[:time_zone].last
          end
          should "record.changes (before save) returns the original, unserialized values" do
            assert_equal [ActiveSupport::TimeZone, ActiveSupport::TimeZone], @changes_before_save[:time_zone].map(&:class)
          end
          should 'version.changeset should be the same as record.changes was before the save' do
            assert_equal @changes_before_save, @person.versions.last.changeset
            assert_equal [ActiveSupport::TimeZone, ActiveSupport::TimeZone], @person.versions.last.changeset[:time_zone].map(&:class)
          end

        end
      end
    end
  end


  context 'A new model instance which uses a custom PaperTrail::Version class' do
    setup { @post = Post.new }

    context 'which is then saved' do
      setup { @post.save }
      should 'change the number of post versions' do assert_equal 1, PostVersion.count end
      should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
    end
  end

  context 'An existing model instance which uses a custom PaperTrail::Version class' do
    setup { @post = Post.create }
    should 'have one post version' do assert_equal(1, PostVersion.count) end

    context 'on the first version' do
      setup { @version = @post.versions.first }

      should 'have the correct index' do
        assert_equal 0, @version.index
      end
    end

    should 'have versions of the custom class' do
      assert_equal "PostVersion", @post.versions.first.class.name
    end

    context 'which is modified' do
      setup { @post.update_attributes({ :content => "Some new content" }) }
      should 'change the number of post versions' do assert_equal(2, PostVersion.count) end
      should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
      should "not have stored changes when object_changes column doesn't exist" do
        assert_nil @post.versions.last.changeset
      end
    end
  end


  context 'An overwritten default accessor' do
    setup do
      @song = Song.create :length => 4
      @song.update_attributes :length => 5
    end

    should 'return "overwritten" value on live instance' do
      assert_equal 5, @song.length
    end
    should 'return "overwritten" value on reified instance' do
      assert_equal 4, @song.versions.last.reify.length
    end

    context 'Has a virtual attribute injected into the ActiveModel::Dirty changes' do
      setup do
        @song.name = 'Good Vibrations'
        @song.save
        @song.name = 'Yellow Submarine'
      end

      should 'return persist the changes on the live instance properly' do
        assert_equal 'Yellow Submarine', @song.name
      end
      should 'return "overwritten" virtual attribute on the reified instance' do
        assert_equal 'Good Vibrations', @song.versions.last.reify.name
      end
    end
  end


  context 'An unsaved record' do
    setup do
      @widget = Widget.new
      @widget.destroy
    end
    should 'not have a version created on destroy' do
      assert @widget.versions.empty?
    end
  end

  context 'A model with a custom association' do
    setup do
      @doc = Document.create
      @doc.update_attributes :name => 'Doc 1'
    end

    should 'not respond to versions method' do
      assert !@doc.respond_to?(:versions)
    end

    should 'create a new version record' do
      assert_equal 2, @doc.paper_trail_versions.length
    end

    should 'respond to `next_version` as normal' do
      assert_equal @doc.paper_trail_versions.last.reify.next_version.name, @doc.name
    end

    should 'respond to `previous_version` as normal' do
      @doc.update_attributes :name => 'Doc 2'
      assert_equal 3, @doc.paper_trail_versions.length
      assert_equal 'Doc 1', @doc.previous_version.name
    end
  end

  context 'The `on` option' do
    context 'on create' do
      setup do
        Fluxor.instance_eval <<-END
          has_paper_trail :on => [:create]
        END
        @fluxor = Fluxor.create
        @fluxor.update_attributes :name => 'blah'
        @fluxor.destroy
      end
      should 'only have a version for the create event' do
        assert_equal 1, @fluxor.versions.length
        assert_equal 'create', @fluxor.versions.last.event
      end
    end
    context 'on update' do
      setup do
        Fluxor.reset_callbacks :create
        Fluxor.reset_callbacks :update
        Fluxor.reset_callbacks :destroy
        Fluxor.instance_eval <<-END
          has_paper_trail :on => [:update]
        END
        @fluxor = Fluxor.create
        @fluxor.update_attributes :name => 'blah'
        @fluxor.destroy
      end
      should 'only have a version for the update event' do
        assert_equal 1, @fluxor.versions.length
        assert_equal 'update', @fluxor.versions.last.event
      end
    end
    context 'on destroy' do
      setup do
        Fluxor.reset_callbacks :create
        Fluxor.reset_callbacks :update
        Fluxor.reset_callbacks :destroy
        Fluxor.instance_eval <<-END
          has_paper_trail :on => [:destroy]
        END
        @fluxor = Fluxor.create
        @fluxor.update_attributes :name => 'blah'
        @fluxor.destroy
      end
      should 'only have a version for the destroy event' do
        assert_equal 1, @fluxor.versions.length
        assert_equal 'destroy', @fluxor.versions.last.event
      end
    end
    context 'on []' do
      setup do
        Fluxor.reset_callbacks :create
        Fluxor.reset_callbacks :update
        Fluxor.reset_callbacks :destroy
        Fluxor.instance_eval <<-END
          has_paper_trail :on => []
        END
        @fluxor = Fluxor.create
        @fluxor.update_attributes :name => 'blah'
      end

      teardown do
        @fluxor.destroy
      end

      should 'not have any versions' do
        assert_equal 0, @fluxor.versions.length
      end

      should 'still respond to touch_with_version' do
        @fluxor.touch_with_version
        assert_equal 1, @fluxor.versions.length
      end
    end
    context 'allows a symbol to be passed' do
      setup do
        Fluxor.reset_callbacks :create
        Fluxor.reset_callbacks :update
        Fluxor.reset_callbacks :destroy
        Fluxor.instance_eval <<-END
          has_paper_trail :on => :create
        END
        @fluxor = Fluxor.create
        @fluxor.update_attributes :name => 'blah'
        @fluxor.destroy
      end
      should 'only have a version for hte create event' do
        assert_equal 1, @fluxor.versions.length
        assert_equal 'create', @fluxor.versions.last.event
      end
    end
  end

  context 'A model with column version and custom version_method' do
    setup do
      @legacy_widget = LegacyWidget.create(:name => "foo", :version => 2)
    end

    should 'set version on create' do
      assert_equal 2, @legacy_widget.version
    end

    should 'allow version updates' do
      @legacy_widget.update_attributes :version => 3
      assert_equal 3, @legacy_widget.version
    end

    should 'create a new version record' do
      assert_equal 1, @legacy_widget.versions.size
    end
  end

  context 'A reified item with a column -version- and custom version_method' do
    setup do
      widget = LegacyWidget.create(:name => "foo", :version => 2)
      %w( bar baz ).each { |name| widget.update_attributes :name => name }
      @version = widget.versions.last
      @widget = @version.reify
    end

    should 'know which version it came from' do
      assert_equal @version, @widget.custom_version
    end

    should 'return its previous self' do
      assert_equal @widget.versions[-2].reify, @widget.previous_version
    end
  end

  context 'custom events' do
    context 'on create' do
      setup do
        Fluxor.reset_callbacks :create
        Fluxor.reset_callbacks :update
        Fluxor.reset_callbacks :destroy
        Fluxor.instance_eval <<-END
          has_paper_trail :on => [:create]
        END
        @fluxor = Fluxor.new.tap { |model| model.paper_trail_event = 'created' }
        @fluxor.update_attributes :name => 'blah'
        @fluxor.destroy
      end
      should 'only have a version for the created event' do
        assert_equal 1, @fluxor.versions.length
        assert_equal 'created', @fluxor.versions.last.event
      end
    end
    context 'on update' do
      setup do
        Fluxor.reset_callbacks :create
        Fluxor.reset_callbacks :update
        Fluxor.reset_callbacks :destroy
        Fluxor.instance_eval <<-END
          has_paper_trail :on => [:update]
        END
        @fluxor = Fluxor.create.tap { |model| model.paper_trail_event = 'name_updated' }
        @fluxor.update_attributes :name => 'blah'
        @fluxor.destroy
      end
      should 'only have a version for the name_updated event' do
        assert_equal 1, @fluxor.versions.length
        assert_equal 'name_updated', @fluxor.versions.last.event
      end
    end
    context 'on destroy' do
      setup do
        Fluxor.reset_callbacks :create
        Fluxor.reset_callbacks :update
        Fluxor.reset_callbacks :destroy
        Fluxor.instance_eval <<-END
          has_paper_trail :on => [:destroy]
        END
        @fluxor = Fluxor.create.tap { |model| model.paper_trail_event = 'destroyed' }
        @fluxor.update_attributes :name => 'blah'
        @fluxor.destroy
      end
      should 'only have a version for the destroy event' do
        assert_equal 1, @fluxor.versions.length
        assert_equal 'destroyed', @fluxor.versions.last.event
      end
    end
  end

  context '`PaperTrail::Config.version_limit` set' do
    setup do
      PaperTrail.config.version_limit = 2
      @widget = Widget.create! :name => 'Henry'
      6.times { @widget.update_attribute(:name, Faker::Lorem.word) }
    end

    teardown { PaperTrail.config.version_limit = nil }

    should "limit the number of versions to 3 (2 plus the created at event)" do
      assert_equal 'create', @widget.versions.first.event
      assert_equal 3, @widget.versions.size
    end
  end
end


class HasPaperTrailModelTransactionalTest < ActiveSupport::TestCase
  # These would have been done in test_helper.rb if using_mysql? is true
  unless using_mysql?
    self.use_transactional_fixtures = false
    setup { DatabaseCleaner.start }
  end

  teardown do
    Timecop.return
    # This would have been done in test_helper.rb if using_mysql? is true
    DatabaseCleaner.clean unless using_mysql?
  end

  context 'A model with a has_one association' do
    setup { @widget = Widget.create :name => 'widget_0' }

    context 'before the associated was created' do
      setup do
        @widget.update_attributes :name => 'widget_1'
        @wotsit = @widget.create_wotsit :name => 'wotsit_0'
      end

      context 'when reified' do
        setup { @widget_0 = @widget.versions.last.reify(:has_one => true) }

        should 'see the associated as it was at the time' do
          assert_nil @widget_0.wotsit
        end

        should 'not persist changes to the live association' do
          assert_equal @wotsit, @widget.wotsit(true)
        end
      end
    end

    context 'where the association is created between model versions' do
      setup do
        @wotsit = @widget.create_wotsit :name => 'wotsit_0'
        Timecop.travel 1.second.since
        @widget.update_attributes :name => 'widget_1'
      end

      context 'when reified' do
        setup { @widget_0 = @widget.versions.last.reify(:has_one => true) }

        should 'see the associated as it was at the time' do
          assert_equal 'wotsit_0', @widget_0.wotsit.name
        end

        should 'not persist changes to the live association' do
          assert_equal @wotsit, @widget.wotsit(true)
        end
      end

      context 'and then the associated is updated between model versions' do
        setup do
          @wotsit.update_attributes :name => 'wotsit_1'
          @wotsit.update_attributes :name => 'wotsit_2'
          Timecop.travel 1.second.since
          @widget.update_attributes :name => 'widget_2'
          @wotsit.update_attributes :name => 'wotsit_3'
        end

        context 'when reified' do
          setup { @widget_1 = @widget.versions.last.reify(:has_one => true) }

          should 'see the associated as it was at the time' do
            assert_equal 'wotsit_2', @widget_1.wotsit.name
          end

          should 'not persist changes to the live association' do
            assert_equal 'wotsit_3', @widget.wotsit(true).name
          end
        end

        context 'when reified opting out of has_one reification' do
          setup { @widget_1 = @widget.versions.last.reify(:has_one => false) }

          should 'see the associated as it is live' do
            assert_equal 'wotsit_3', @widget_1.wotsit.name
          end
        end
      end

      context 'and then the associated is destroyed' do
        setup do
          @wotsit.destroy
        end

        context 'when reify' do
          setup { @widget_1 = @widget.versions.last.reify(:has_one => true) }

          should 'see the associated as it was at the time' do
            assert_equal @wotsit, @widget_1.wotsit
          end

          should 'not persist changes to the live association' do
            assert_nil @widget.wotsit(true)
          end
        end

        context 'and then the model is updated' do
          setup do
            Timecop.travel 1.second.since
            @widget.update_attributes :name => 'widget_3'
          end

          context 'when reified' do
            setup { @widget_2 = @widget.versions.last.reify(:has_one => true) }

            should 'see the associated as it was at the time' do
              assert_nil @widget_2.wotsit
            end
          end
        end
      end
    end
  end

  context 'A model with a has_many association' do
    setup { @customer = Customer.create :name => 'customer_0' }

    context 'updated before the associated was created' do
      setup do
        @customer.update_attributes! :name => 'customer_1'
        @customer.orders.create! :order_date => Date.today
      end

      context 'when reified' do
        setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

        should 'see the associated as it was at the time' do
          assert_equal [], @customer_0.orders
        end

        should 'not persist changes to the live association' do
          assert_not_equal [], @customer.orders(true)
        end
      end

      context 'when reified with option mark_for_destruction' do
        setup { @customer_0 = @customer.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

        should 'mark the associated for destruction' do
          assert_equal [true], @customer_0.orders.map(&:marked_for_destruction?)
        end
      end
    end

    context 'where the association is created between model versions' do
      setup do
        @order = @customer.orders.create! :order_date => 'order_date_0'
        Timecop.travel 1.second.since
        @customer.update_attributes :name => 'customer_1'
      end

      context 'when reified' do
        setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

        should 'see the associated as it was at the time' do
          assert_equal ['order_date_0'], @customer_0.orders.map(&:order_date)
        end
      end

      context 'and then a nested has_many association is created' do
        setup do
          @order.line_items.create! :product => 'product_0'
        end

        context 'when reified' do
          setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

          should 'see the live version of the nested association' do
            assert_equal ['product_0'], @customer_0.orders.first.line_items.map(&:product)
          end
        end
      end

      context 'and then the associated is updated between model versions' do
        setup do
          @order.update_attributes :order_date => 'order_date_1'
          @order.update_attributes :order_date => 'order_date_2'
          Timecop.travel 1.second.since
          @customer.update_attributes :name => 'customer_2'
          @order.update_attributes :order_date => 'order_date_3'
        end

        context 'when reified' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal ['order_date_2'], @customer_1.orders.map(&:order_date)
          end

          should 'not persist changes to the live association' do
            assert_equal ['order_date_3'], @customer.orders(true).map(&:order_date)
          end
        end

        context 'when reified opting out of has_many reification' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => false) }

          should 'see the associated as it is live' do
            assert_equal ['order_date_3'], @customer_1.orders.map(&:order_date)
          end
        end

        context 'and then the associated is destroyed' do
          setup do
            @order.destroy
          end

          context 'when reified' do
            setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

            should 'see the associated as it was at the time' do
              assert_equal ['order_date_2'], @customer_1.orders.map(&:order_date)
            end

            should 'not persist changes to the live association' do
              assert_equal [], @customer.orders(true)
            end
          end
        end
      end

      context 'and then the associated is destroyed' do
        setup do
          @order.destroy
        end

        context 'when reified' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [@order.order_date], @customer_1.orders.map(&:order_date)
          end

          should 'not persist changes to the live association' do
            assert_equal [], @customer.orders(true)
          end
        end
      end

      context 'and then the associated is destroyed between model versions' do
        setup do
          @order.destroy
          Timecop.travel 1.second.since
          @customer.update_attributes :name => 'customer_2'
        end

        context 'when reified' do
          setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [], @customer_1.orders
          end
        end
      end

      context 'and then another association is added' do
        setup do
          @customer.orders.create! :order_date => 'order_date_1'
        end

        context 'when reified' do
          setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal ['order_date_0'], @customer_0.orders.map(&:order_date)
          end

          should 'not persist changes to the live association' do
            assert_equal ['order_date_0', 'order_date_1'], @customer.orders(true).map(&:order_date).sort
          end
        end

        context 'when reified with option mark_for_destruction' do
          setup { @customer_0 = @customer.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

          should 'mark the newly associated for destruction' do
            assert @customer_0.orders.detect { |o| o.order_date == 'order_date_1'}.marked_for_destruction?
          end
        end
      end
    end
  end

  context 'A model with a has_many through association' do
    setup { @book = Book.create :title => 'book_0' }

    context 'updated before the associated was created' do
      setup do
        @book.update_attributes! :title => 'book_1'
        @book.authors.create! :name => 'author_0'
      end

      context 'when reified' do
        setup { @book_0 = @book.versions.last.reify(:has_many => true) }

        should 'see the associated as it was at the time' do
          assert_equal [], @book_0.authors
        end

        should 'not persist changes to the live association' do
          assert_equal ['author_0'], @book.authors(true).map(&:name)
        end
      end

      context 'when reified with option mark_for_destruction' do
        setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

        should 'mark the associated for destruction' do
          assert_equal [true], @book_0.authors.map(&:marked_for_destruction?)
        end

        should 'mark the associated-through for destruction' do
          assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
        end
      end
    end

    context 'updated before it is associated with an existing one' do
      setup do
        person_existing = Person.create(:name => 'person_existing')
        Timecop.travel 1.second.since
        @book.update_attributes! :title => 'book_1'
        @book.authors << person_existing
      end

      context 'when reified' do
        setup { @book_0 = @book.versions.last.reify(:has_many => true) }

        should 'see the associated as it was at the time' do
          assert_equal [], @book_0.authors
        end
      end

      context 'when reified with option mark_for_destruction' do
        setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

        should 'not mark the associated for destruction' do
          assert_equal [false], @book_0.authors.map(&:marked_for_destruction?)
        end

        should 'mark the associated-through for destruction' do
          assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
        end
      end
    end

    context 'where the association is created between model versions' do
      setup do
        @author = @book.authors.create! :name => 'author_0'
        @person_existing = Person.create(:name => 'person_existing')
        Timecop.travel 1.second.since
        @book.update_attributes! :title => 'book_1'
      end

      context 'when reified' do
        setup { @book_0 = @book.versions.last.reify(:has_many => true) }

        should 'see the associated as it was at the time' do
          assert_equal ['author_0'], @book_0.authors.map(&:name)
        end
      end

      context 'and then the associated is updated between model versions' do
        setup do
          @author.update_attributes :name => 'author_1'
          @author.update_attributes :name => 'author_2'
          Timecop.travel 1.second.since
          @book.update_attributes :title => 'book_2'
          @author.update_attributes :name => 'author_3'
        end

        context 'when reified' do
          setup { @book_1 = @book.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal ['author_2'], @book_1.authors.map(&:name)
          end

          should 'not persist changes to the live association' do
            assert_equal ['author_3'], @book.authors(true).map(&:name)
          end
        end

        context 'when reified opting out of has_many reification' do
          setup { @book_1 = @book.versions.last.reify(:has_many => false) }

          should 'see the associated as it is live' do
            assert_equal ['author_3'], @book_1.authors.map(&:name)
          end
        end
      end

      context 'and then the associated is destroyed' do
        setup do
          @author.destroy
        end

        context 'when reified' do
          setup { @book_1 = @book.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [@author.name], @book_1.authors.map(&:name)
          end

          should 'not persist changes to the live association' do
            assert_equal [], @book.authors(true)
          end
        end
      end

      context 'and then the associated is destroyed between model versions' do
        setup do
          @author.destroy
          Timecop.travel 1.second.since
          @book.update_attributes :title => 'book_2'
        end

        context 'when reified' do
          setup { @book_1 = @book.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [], @book_1.authors
          end
        end
      end

      context 'and then the associated is dissociated between model versions' do
        setup do
          @book.authors = []
          Timecop.travel 1.second.since
          @book.update_attributes :title => 'book_2'
        end

        context 'when reified' do
          setup { @book_1 = @book.versions.last.reify(:has_many => true) }

          should 'see the associated as it was at the time' do
            assert_equal [], @book_1.authors
          end
        end
      end

      context 'and then another associated is created' do
        setup do
          @book.authors.create! :name => 'author_1'
        end

        context 'when reified' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true) }

          should 'only see the first associated' do
            assert_equal ['author_0'], @book_0.authors.map(&:name)
          end

          should 'not persist changes to the live association' do
            assert_equal ['author_0', 'author_1'], @book.authors(true).map(&:name)
          end
        end

        context 'when reified with option mark_for_destruction' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

          should 'mark the newly associated for destruction' do
            assert @book_0.authors.detect { |a| a.name == 'author_1' }.marked_for_destruction?
          end

          should 'mark the newly associated-through for destruction' do
            assert @book_0.authorships.detect { |as| as.person.name == 'author_1' }.marked_for_destruction?
          end
        end
      end

      context 'and then an existing one is associated' do
        setup do
          @book.authors << @person_existing
        end

        context 'when reified' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true) }

          should 'only see the first associated' do
            assert_equal ['author_0'], @book_0.authors.map(&:name)
          end

          should 'not persist changes to the live association' do
            assert_equal ['author_0', 'person_existing'], @book.authors(true).map(&:name).sort
          end
        end

        context 'when reified with option mark_for_destruction' do
          setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }

          should 'not mark the newly associated for destruction' do
            assert !@book_0.authors.detect { |a| a.name == 'person_existing' }.marked_for_destruction?
          end

          should 'mark the newly associated-through for destruction' do
            assert @book_0.authorships.detect { |as| as.person.name == 'person_existing' }.marked_for_destruction?
          end
        end
      end
    end

    context 'updated before the associated without paper_trail was created' do
      setup do
        @book.update_attributes! :title => 'book_1'
        @book.editors.create! :name => 'editor_0'
      end

      context 'when reified' do
        setup { @book_0 = @book.versions.last.reify(:has_many => true) }

        should 'see the live association' do
          assert_equal ['editor_0'], @book_0.editors.map(&:name)
        end
      end
    end
  end
end
