# frozen_string_literal: true

require "spec_helper"
require "support/performance_helpers"

RSpec.describe(::PaperTrail, versioning: true) do
  describe "#changeset" do
    it "has expected values" do
      widget = Widget.create(name: "Henry")
      changeset = widget.versions.last.changeset
      expect(changeset["name"]).to eq([nil, "Henry"])
      expect(changeset["id"]).to eq([nil, widget.id])
      # When comparing timestamps, round off to the nearest second, because
      # mysql doesn't do fractional seconds.
      expect(changeset["created_at"][0]).to be_nil
      expect(changeset["created_at"][1].to_i).to eq(widget.created_at.to_i)
      expect(changeset["updated_at"][0]).to be_nil
      expect(changeset["updated_at"][1].to_i).to eq(widget.updated_at.to_i)
    end

    context "custom object_changes_adapter" do
      after do
        PaperTrail.config.object_changes_adapter = nil
      end

      it "calls the adapter's load_changeset method" do
        widget = Widget.create(name: "Henry")
        adapter = instance_spy("CustomObjectChangesAdapter")
        PaperTrail.config.object_changes_adapter = adapter
        allow(adapter).to(
          receive(:load_changeset).with(widget.versions.last).and_return(a: "b", c: "d")
        )
        changeset = widget.versions.last.changeset
        expect(changeset[:a]).to eq("b")
        expect(changeset[:c]).to eq("d")
        expect(adapter).to have_received(:load_changeset)
      end

      it "defaults to the original behavior" do
        adapter = Class.new.new
        PaperTrail.config.object_changes_adapter = adapter
        widget = Widget.create(name: "Henry")
        changeset = widget.versions.last.changeset
        expect(changeset[:name]).to eq([nil, "Henry"])
      end
    end
  end

  context "a new record" do
    it "not have any previous versions" do
      expect(Widget.new.versions).to(eq([]))
    end

    it "be live" do
      expect(Widget.new.paper_trail.live?).to(eq(true))
    end
  end

  context "a persisted record" do
    it "have one previous version" do
      widget = Widget.create(name: "Henry", created_at: (Time.current - 1.day))
      expect(widget.versions.length).to(eq(1))
    end

    it "be nil in its previous version" do
      widget = Widget.create(name: "Henry")
      expect(widget.versions.first.object).to(be_nil)
      expect(widget.versions.first.reify).to(be_nil)
    end

    it "record the correct event" do
      widget = Widget.create(name: "Henry")
      expect(widget.versions.first.event).to(match(/create/i))
    end

    it "be live" do
      widget = Widget.create(name: "Henry")
      expect(widget.paper_trail.live?).to(eq(true))
    end

    it "use the widget `updated_at` as the version's `created_at`" do
      widget = Widget.create(name: "Henry")
      expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
    end

    context "and then updated without any changes" do
      it "to have two previous versions" do
        widget = Widget.create(name: "Henry")
        widget.touch
        expect(widget.versions.length).to eq(2)
      end
    end

    context "and then updated with changes" do
      it "have three previous versions" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        expect(widget.versions.length).to(eq(2))
      end

      it "be available in its previous version" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        expect(widget.name).to(eq("Harry"))
        expect(widget.versions.last.object).not_to(be_nil)
        reified_widget = widget.versions.last.reify
        expect(reified_widget.name).to(eq("Henry"))
        expect(widget.name).to(eq("Harry"))
      end

      it "have the same ID in its previous version" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        expect(widget.versions.last.reify.id).to(eq(widget.id))
      end

      it "record the correct event" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        expect(widget.versions.last.event).to(match(/update/i))
      end

      it "have versions that are not live" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.versions.map(&:reify).compact.each do |v|
          expect(v.paper_trail).not_to be_live
        end
      end

      it "have stored changes" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        last_obj_changes = widget.versions.last.object_changes
        actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
          (k.to_sym == :updated_at)
        end
        expect(actual).to(eq("name" => %w[Henry Harry]))
        actual = widget.versions.last.changeset.reject { |k, _v| (k.to_sym == :updated_at) }
        expect(actual).to(eq("name" => %w[Henry Harry]))
      end

      it "return changes with indifferent access" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry]))
        expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry]))
      end
    end

    context "updated, and has one associated object" do
      it "not copy the has_one association by default when reifying" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        wotsit = widget.create_wotsit name: "John"
        reified_widget = widget.versions.last.reify
        expect(reified_widget.wotsit).to eq(wotsit)
        expect(widget.reload.wotsit).to eq(wotsit)
      end
    end

    context "updated, and has many associated objects" do
      it "copy the has_many associations when reifying" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.fluxors.create(name: "f-zero")
        widget.fluxors.create(name: "f-one")
        reified_widget = widget.versions.last.reify
        expect(reified_widget.fluxors.length).to(eq(widget.fluxors.length))
        expect(reified_widget.fluxors).to match_array(widget.fluxors)
        expect(reified_widget.versions.length).to(eq(widget.versions.length))
        expect(reified_widget.versions).to match_array(widget.versions)
      end
    end

    context "updated, and has many associated polymorphic objects" do
      it "copy the has_many associations when reifying" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.whatchamajiggers.create(name: "f-zero")
        widget.whatchamajiggers.create(name: "f-zero")
        reified_widget = widget.versions.last.reify
        expect(reified_widget.whatchamajiggers.length).to eq(widget.whatchamajiggers.length)
        expect(reified_widget.whatchamajiggers).to match_array(widget.whatchamajiggers)
        expect(reified_widget.versions.length).to(eq(widget.versions.length))
        expect(reified_widget.versions).to match_array(widget.versions)
      end
    end

    context "updated, polymorphic objects by themselves" do
      it "not fail with a nil pointer on the polymorphic association" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget = Whatchamajigger.new(name: "f-zero")
        widget.save!
      end
    end

    context "updated, and then destroyed" do
      it "record the correct event" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.destroy
        expect(PaperTrail::Version.last.event).to(match(/destroy/i))
      end

      it "have three previous versions" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.destroy
        expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
      end

      it "returns the expected attributes for the reified widget" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.destroy
        reified_widget = PaperTrail::Version.last.reify
        expect(reified_widget.id).to eq(widget.id)
        expected = widget.attributes
        actual = reified_widget.attributes
        expect(expected["id"]).to eq(actual["id"])
        expect(expected["name"]).to eq(actual["name"])
        expect(expected["a_text"]).to eq(actual["a_text"])
        expect(expected["an_integer"]).to eq(actual["an_integer"])
        expect(expected["a_float"]).to eq(actual["a_float"])
        expect(expected["a_decimal"]).to eq(actual["a_decimal"])
        expect(expected["a_datetime"]).to eq(actual["a_datetime"])
        expect(expected["a_time"]).to eq(actual["a_time"])
        expect(expected["a_date"]).to eq(actual["a_date"])
        expect(expected["a_boolean"]).to eq(actual["a_boolean"])
        expect(expected["type"]).to eq(actual["type"])

        # We are using `to_i` to truncate to the nearest second, but isn't
        # there still a chance of this failing intermittently if
        # ___ and ___ occured more than 0.5s apart?
        expect(expected["created_at"].to_i).to eq(actual["created_at"].to_i)
        expect(expected["updated_at"].to_i).to eq(actual["updated_at"].to_i)
      end

      it "be re-creatable from its previous version" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.destroy
        reified_widget = PaperTrail::Version.last.reify
        expect(reified_widget.save).to(be_truthy)
      end

      it "restore its associations on its previous version" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.fluxors.create(name: "flux")
        widget.destroy
        reified_widget = PaperTrail::Version.last.reify
        reified_widget.save
        expect(reified_widget.fluxors.length).to(eq(1))
      end

      it "have nil item for last version" do
        widget = Widget.create(name: "Henry")
        widget.update(name: "Harry")
        widget.destroy
        expect(widget.versions.last.item).to be_nil
      end

      it "has changes" do
        book = Book.create! title: "A"
        changes = YAML.load book.versions.last.attributes["object_changes"]
        expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])

        book.update! title: "B"
        changes = YAML.load book.versions.last.attributes["object_changes"]
        expect(changes).to eq("title" => %w[A B])

        book.destroy
        changes = YAML.load book.versions.last.attributes["object_changes"]
        expect(changes).to eq("id" => [book.id, nil], "title" => ["B", nil])
      end
    end
  end

  context "a record's papertrail" do
    let!(:d0) { Date.new(2009, 5, 29) }
    let!(:t0) { Time.current }
    let(:previous_widget) { widget.versions.last.reify }
    let(:widget) {
      Widget.create(
        name: "Warble",
        a_text: "The quick brown fox",
        an_integer: 42,
        a_float: 153.01,
        a_decimal: 2.71828,
        a_datetime: t0,
        a_time: t0,
        a_date: d0,
        a_boolean: true
      )
    }

    before do
      widget.update(
        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
      )
    end

    it "handle strings" do
      expect(previous_widget.name).to(eq("Warble"))
    end

    it "handle text" do
      expect(previous_widget.a_text).to(eq("The quick brown fox"))
    end

    it "handle integers" do
      expect(previous_widget.an_integer).to(eq(42))
    end

    it "handle floats" do
      assert_in_delta(153.01, previous_widget.a_float, 0.001)
    end

    it "handle decimals" do
      assert_in_delta(2.7183, previous_widget.a_decimal, 0.0001)
    end

    it "handle datetimes" do
      expect(previous_widget.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
    end

    it "handle times (time only, no date)" do
      format = ->(t) { t.utc.strftime "%H:%M:%S" }
      expect(format[previous_widget.a_time]).to eq(format[t0])
    end

    it "handle dates" do
      expect(previous_widget.a_date).to(eq(d0))
    end

    it "handle booleans" do
      expect(previous_widget.a_boolean).to(be_truthy)
    end

    context "after a column is removed from the record's schema" do
      let(:last_version) { widget.versions.last }

      it "reify previous version" do
        assert_kind_of(Widget, last_version.reify)
      end

      it "restore all forward-compatible attributes" do
        reified = last_version.reify
        expect(reified.name).to(eq("Warble"))
        expect(reified.a_text).to(eq("The quick brown fox"))
        expect(reified.an_integer).to(eq(42))
        assert_in_delta(153.01, reified.a_float, 0.001)
        assert_in_delta(2.7183, reified.a_decimal, 0.0001)
        expect(reified.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
        format = ->(t) { t.utc.strftime "%H:%M:%S" }
        expect(format[reified.a_time]).to eq(format[t0])
        expect(reified.a_date).to(eq(d0))
        expect(reified.a_boolean).to(be_truthy)
      end
    end
  end

  context "A record" do
    context "with PaperTrail globally disabled, when updated" do
      after { PaperTrail.enabled = true }

      it "not add to its trail" do
        widget = Widget.create(name: "Zaphod")
        PaperTrail.enabled = false
        count = widget.versions.length
        widget.update(name: "Beeblebrox")
        expect(widget.versions.length).to(eq(count))
      end
    end

    context "with its paper trail turned off, when updated" do
      after do
        PaperTrail.request.enable_model(Widget)
      end

      it "not add to its trail" do
        widget = Widget.create(name: "Zaphod")
        PaperTrail.request.disable_model(Widget)
        count = widget.versions.length
        widget.update(name: "Beeblebrox")
        expect(widget.versions.length).to(eq(count))
      end

      it "add to its trail" do
        widget = Widget.create(name: "Zaphod")
        PaperTrail.request.disable_model(Widget)
        count = widget.versions.length
        widget.update(name: "Beeblebrox")
        PaperTrail.request.enable_model(Widget)
        widget.update(name: "Ford")
        expect(widget.versions.length).to(eq((count + 1)))
      end
    end
  end

  context "A papertrail with somebody making changes" do
    context "when a record is created" do
      it "tracks who made the change" do
        widget = Widget.new(name: "Fidget")
        PaperTrail.request.whodunnit = "Alice"
        widget.save
        version = widget.versions.last
        expect(version.whodunnit).to(eq("Alice"))
        expect(version.paper_trail_originator).to(be_nil)
        expect(version.terminator).to(eq("Alice"))
        expect(widget.paper_trail.originator).to(eq("Alice"))
      end
    end

    context "when created, then updated" do
      it "tracks who made the change" do
        widget = Widget.new(name: "Fidget")
        PaperTrail.request.whodunnit = "Alice"
        widget.save
        PaperTrail.request.whodunnit = "Bob"
        widget.update(name: "Rivet")
        version = widget.versions.last
        expect(version.whodunnit).to(eq("Bob"))
        expect(version.paper_trail_originator).to(eq("Alice"))
        expect(version.terminator).to(eq("Bob"))
        expect(widget.paper_trail.originator).to(eq("Bob"))
      end
    end

    context "when created, updated, and destroyed" do
      it "tracks who made the change" do
        widget = Widget.new(name: "Fidget")
        PaperTrail.request.whodunnit = "Alice"
        widget.save
        PaperTrail.request.whodunnit = "Bob"
        widget.update(name: "Rivet")
        PaperTrail.request.whodunnit = "Charlie"
        widget.destroy
        version = PaperTrail::Version.last
        expect(version.whodunnit).to(eq("Charlie"))
        expect(version.paper_trail_originator).to(eq("Bob"))
        expect(version.terminator).to(eq("Charlie"))
        expect(widget.paper_trail.originator).to(eq("Charlie"))
      end
    end
  end

  it "update! records timestamps" do
    wotsit = Wotsit.create!(name: "wotsit")
    wotsit.update!(name: "changed")
    reified = wotsit.versions.last.reify
    expect(reified.created_at).not_to(be_nil)
    expect(reified.updated_at).not_to(be_nil)
  end

  it "update! does not raise error" do
    wotsit = Wotsit.create!(name: "name1")
    expect { wotsit.update!(name: "name2") }.not_to(raise_error)
  end

  context "A subclass" do
    let(:foo) { FooWidget.create }

    before do
      foo.update!(name: "Foo")
    end

    it "reify with the correct type" do
      expect(PaperTrail::Version.last.previous).to(eq(foo.versions.first))
      expect(PaperTrail::Version.last.next).to(be_nil)
    end

    it "returns the correct originator" do
      PaperTrail.request.whodunnit = "Ben"
      foo.update_attribute(:name, "Geoffrey")
      expect(foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
    end

    context "when destroyed" do
      before { foo.destroy }

      it "reify with the correct type" do
        assert_kind_of(FooWidget, foo.versions.last.reify)
        expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1]))
        expect(PaperTrail::Version.last.next).to(be_nil)
      end
    end
  end

  context "An item with versions" do
    context "which were created over time" do
      let(:widget) { Widget.create(name: "Widget") }
      let(:t0) { 2.days.ago }
      let(:t1) { 1.day.ago }
      let(:t2) { 1.hour.ago }

      before do
        widget.update(name: "Fidget")
        widget.update(name: "Digit")
        widget.versions[0].update(created_at: t0)
        widget.versions[1].update(created_at: t1)
        widget.versions[2].update(created_at: t2)
        widget.update_attribute(:updated_at, t2)
      end

      it "return nil for version_at before it was created" do
        expect(widget.paper_trail.version_at((t0 - 1))).to(be_nil)
      end

      it "return how it looked when created for version_at its creation" do
        expect(widget.paper_trail.version_at(t0).name).to(eq("Widget"))
      end

      it "return how it looked before its first update" do
        expect(widget.paper_trail.version_at((t1 - 1)).name).to(eq("Widget"))
      end

      it "return how it looked after its first update" do
        expect(widget.paper_trail.version_at(t1).name).to(eq("Fidget"))
      end

      it "return how it looked before its second update" do
        expect(widget.paper_trail.version_at((t2 - 1)).name).to(eq("Fidget"))
      end

      it "return how it looked after its second update" do
        expect(widget.paper_trail.version_at(t2).name).to(eq("Digit"))
      end

      it "return the current object for version_at after latest update" do
        expect(widget.paper_trail.version_at(1.day.from_now).name).to(eq("Digit"))
      end

      it "still return a widget when appropriate, when passing timestamp as string" do
        expect(
          widget.paper_trail.version_at((t0 + 1.second).to_s).name
        ).to(eq("Widget"))
        expect(
          widget.paper_trail.version_at((t1 + 1.second).to_s).name
        ).to(eq("Fidget"))
        expect(
          widget.paper_trail.version_at((t2 + 1.second).to_s).name
        ).to(eq("Digit"))
      end
    end

    describe ".versions_between" do
      it "return versions in the time period" do
        widget = Widget.create(name: "Widget")
        widget.update(name: "Fidget")
        widget.update(name: "Digit")
        widget.versions[0].update(created_at: 30.days.ago)
        widget.versions[1].update(created_at: 15.days.ago)
        widget.versions[2].update(created_at: 1.day.ago)
        widget.update_attribute(:updated_at, 1.day.ago)
        expect(
          widget.paper_trail.versions_between(20.days.ago, 10.days.ago).map(&:name)
        ).to(eq(["Fidget"]))
        expect(
          widget.paper_trail.versions_between(45.days.ago, 10.days.ago).map(&:name)
        ).to(eq(%w[Widget Fidget]))
        expect(
          widget.paper_trail.versions_between(16.days.ago, 1.minute.ago).map(&:name)
        ).to(eq(%w[Fidget Digit Digit]))
        expect(
          widget.paper_trail.versions_between(60.days.ago, 45.days.ago).map(&:name)
        ).to(eq([]))
      end
    end

    context "on the first version" do
      let(:widget) { Widget.create(name: "Widget") }
      let(:version) { widget.versions.last }

      before do
        widget = Widget.create(name: "Widget")
        widget.update(name: "Fidget")
        widget.update(name: "Digit")
      end

      it "have a nil previous version" do
        expect(version.previous).to(be_nil)
      end

      it "return the next version" do
        expect(version.next).to(eq(widget.versions[1]))
      end

      it "return the correct index" do
        expect(version.index).to(eq(0))
      end
    end

    context "on the last version" do
      let(:widget) { Widget.create(name: "Widget") }
      let(:version) { widget.versions.last }

      before do
        widget.update(name: "Fidget")
        widget.update(name: "Digit")
      end

      it "return the previous version" do
        expect(version.previous).to(eq(widget.versions[(widget.versions.length - 2)]))
      end

      it "have a nil next version" do
        expect(version.next).to(be_nil)
      end

      it "return the correct index" do
        expect(version.index).to(eq((widget.versions.length - 1)))
      end
    end
  end

  context "An item" do
    let(:article) { Article.new(title: initial_title) }
    let(:initial_title) { "Foobar" }

    context "which is created" do
      before { article.save }

      it "store fixed meta data" do
        expect(article.versions.last.answer).to(eq(42))
      end

      it "store dynamic meta data which is independent of the item" do
        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
      end

      it "store dynamic meta data which depends on the item" do
        expect(article.versions.last.article_id).to(eq(article.id))
      end

      it "store dynamic meta data based on a method of the item" do
        expect(article.versions.last.action).to(eq(article.action_data_provider_method))
      end

      it "store dynamic meta data based on an attribute of the item at creation" do
        expect(article.versions.last.title).to(eq(initial_title))
      end
    end

    context "created, then updated" do
      before do
        article.save
        article.update!(content: "Better text.", title: "Rhubarb")
      end

      it "store fixed meta data" do
        expect(article.versions.last.answer).to(eq(42))
      end

      it "store dynamic meta data which is independent of the item" do
        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
      end

      it "store dynamic meta data which depends on the item" do
        expect(article.versions.last.article_id).to(eq(article.id))
      end

      it "store dynamic meta data based on an attribute of the item prior to the update" do
        expect(article.versions.last.title).to(eq(initial_title))
      end
    end

    context "created, then destroyed" do
      before do
        article.save
        article.destroy
      end

      it "store fixed metadata" do
        expect(article.versions.last.answer).to(eq(42))
      end

      it "store dynamic metadata which is independent of the item" do
        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
      end

      it "store dynamic metadata which depends on the item" do
        expect(article.versions.last.article_id).to(eq(article.id))
      end

      it "store dynamic metadata based on attribute of item prior to destruction" do
        expect(article.versions.last.title).to(eq(initial_title))
      end
    end
  end

  context "A reified item" do
    it "know which version it came from, and return its previous self" do
      widget = Widget.create(name: "Bob")
      %w[Tom Dick Jane].each do |name|
        widget.update(name: name)
      end
      version = widget.versions.last
      widget = version.reify
      expect(widget.version).to(eq(version))
      expect(widget.paper_trail.previous_version).to(eq(widget.versions[-2].reify))
    end
  end

  describe "#next_version" do
    context "a reified item" do
      it "returns the object (not a Version) as it became next" do
        widget = Widget.create(name: "Bob")
        %w[Tom Dick Jane].each do |name|
          widget.update(name: name)
        end
        second_widget = widget.versions[1].reify
        last_widget = widget.versions.last.reify
        expect(second_widget.paper_trail.next_version.name).to(eq(widget.versions[2].reify.name))
        expect(widget.name).to(eq(last_widget.paper_trail.next_version.name))
      end
    end

    context "a non-reified item" do
      it "always returns nil because cannot ever have a next version" do
        widget = Widget.new
        expect(widget.paper_trail.next_version).to(be_nil)
        widget.save
        %w[Tom Dick Jane].each do |name|
          widget.update(name: name)
        end
        expect(widget.paper_trail.next_version).to(be_nil)
      end
    end
  end

  describe "#previous_version" do
    context "a reified item" do
      it "returns the object (not a Version) as it was most recently" do
        widget = Widget.create(name: "Bob")
        %w[Tom Dick Jane].each do |name|
          widget.update(name: name)
        end
        second_widget = widget.versions[1].reify
        last_widget = widget.versions.last.reify
        expect(second_widget.paper_trail.previous_version).to(be_nil)
        expect(last_widget.paper_trail.previous_version.name).to(eq(widget.versions[-2].reify.name))
      end
    end

    context "a non-reified item" do
      it "returns the object (not a Version) as it was most recently" do
        widget = Widget.new
        expect(widget.paper_trail.previous_version).to(be_nil)
        widget.save
        %w[Tom Dick Jane].each do |name|
          widget.update(name: name)
        end
        expect(widget.paper_trail.previous_version.name).to(eq(widget.versions.last.reify.name))
      end
    end
  end

  context ":has_many :through" do
    it "store version on source <<" do
      book = Book.create(title: "War and Peace")
      dostoyevsky = Person.create(name: "Dostoyevsky")
      Person.create(name: "Solzhenitsyn")
      count = PaperTrail::Version.count
      (book.authors << dostoyevsky)
      expect((PaperTrail::Version.count - count)).to(eq(1))
      expect(book.authorships.first.versions.first).to(eq(PaperTrail::Version.last))
    end

    it "store version on source create" do
      book = Book.create(title: "War and Peace")
      Person.create(name: "Dostoyevsky")
      Person.create(name: "Solzhenitsyn")
      count = PaperTrail::Version.count
      book.authors.create(name: "Tolstoy")
      expect((PaperTrail::Version.count - count)).to(eq(2))
      expect(
        [PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
      ).to match_array([Person.last, Authorship.last])
    end

    it "store version on join destroy" do
      book = Book.create(title: "War and Peace")
      dostoyevsky = Person.create(name: "Dostoyevsky")
      Person.create(name: "Solzhenitsyn")
      (book.authors << dostoyevsky)
      count = PaperTrail::Version.count
      book.authorships.reload.last.destroy
      expect((PaperTrail::Version.count - count)).to(eq(1))
      expect(PaperTrail::Version.last.reify.book).to(eq(book))
      expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
    end

    it "store version on join clear" do
      book = Book.create(title: "War and Peace")
      dostoyevsky = Person.create(name: "Dostoyevsky")
      Person.create(name: "Solzhenitsyn")
      book.authors << dostoyevsky
      count = PaperTrail::Version.count
      book.authorships.reload.destroy_all
      expect((PaperTrail::Version.count - count)).to(eq(1))
      expect(PaperTrail::Version.last.reify.book).to(eq(book))
      expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
    end
  end

  context "the default accessor, length=, is overwritten" do
    it "returns overwritten value on reified instance" do
      song = Song.create(length: 4)
      song.update(length: 5)
      expect(song.length).to(eq(5))
      expect(song.versions.last.reify.length).to(eq(4))
    end
  end

  context "song name is a virtual attribute (no such db column)" do
    it "returns overwritten virtual attribute on the reified instance" do
      song = Song.create(length: 4)
      song.update(length: 5)
      song.name = "Good Vibrations"
      song.save
      song.name = "Yellow Submarine"
      expect(song.name).to(eq("Yellow Submarine"))
      expect(song.versions.last.reify.name).to(eq("Good Vibrations"))
    end
  end

  context "An unsaved record" do
    it "not have a version created on destroy" do
      widget = Widget.new
      widget.destroy
      expect(widget.versions.empty?).to(eq(true))
    end
  end

  context "Memory allocation of" do
    let(:widget) do
      Widget.new(
        name: "Warble",
        a_text: "The quick brown fox",
        an_integer: 42,
        a_float: 153.01,
        a_decimal: 2.71828,
        a_boolean: true
      )
    end

    before do
      # Json fields for `object` & `object_changes` attributes is most efficient way
      # to do the things - this way we will save even more RAM, as well as will skip
      # the whole YAML serialization
      allow(PaperTrail::Version).to receive(:object_changes_col_is_json?).and_return(true)
      allow(PaperTrail::Version).to receive(:object_col_is_json?).and_return(true)

      # Force the loading of all lazy things like class definitions,
      # in order to get the pure benchmark
      version_building.call
    end

    describe "#build_version_on_create" do
      let(:version_building) do
        lambda do
          widget.paper_trail.send(
            :build_version_on_create,
            in_after_callback: false
          )
        end
      end

      it "is frugal enough" do
        # Some time ago there was 95kbs..
        # At the time of commit the test passes with assertion on 17kbs.
        # Lets assert 20kbs then, to avoid flaky fails.
        expect(&version_building).to allocate_less_than(20).kilobytes
      end
    end

    describe "#build_version_on_update" do
      let(:widget) do
        super().tap do |w|
          w.save!
          w.attributes = {
            name: "Dostoyevsky",
            a_text: "The slow yellow mouse",
            an_integer: 84,
            a_float: 306.02,
            a_decimal: 5.43656,
            a_boolean: false
          }
        end
      end
      let(:version_building) do
        lambda do
          widget.paper_trail.send(
            :build_version_on_update,
            force: false,
            in_after_callback: false,
            is_touch: false
          )
        end
      end

      it "is frugal enough" do
        # Some time ago there was 144kbs..
        # At the time of commit the test passes with assertion on 27kbs.
        # Lets assert 35kbs then, to avoid flaky fails.
        expect(&version_building).to allocate_less_than(35).kilobytes
      end
    end
  end
end
