require "spec_helper"

# not testing proxy_respond_to? hack / 2 methods / deprecation of `version`
# also, an additional 6 around `after_touch` for Versions before 6.
# Increased to 17/10 to get to green CI as a new baseline, August 2024.
uncovered = (ActiveRecord::VERSION::MAJOR < 6) ? 17 : 10
SingleCov.covered! uncovered: uncovered

class ConditionalPrivateCompany < ::ActiveRecord::Base
  self.table_name = "companies"

  audited if: :foo?

  private def foo?
    true
  end
end

class ConditionalCompany < ::ActiveRecord::Base
  self.table_name = "companies"

  audited if: :public?

  def public?
  end
end

class ExclusiveCompany < ::ActiveRecord::Base
  self.table_name = "companies"
  audited if: proc { false }
end

class ExclusionaryCompany < ::ActiveRecord::Base
  self.table_name = "companies"

  audited unless: :non_profit?

  def non_profit?
  end
end

class ExclusionaryCompany2 < ::ActiveRecord::Base
  self.table_name = "companies"
  audited unless: proc { |c| c.exclusive? }

  def exclusive?
    true
  end
end

class InclusiveCompany < ::ActiveRecord::Base
  self.table_name = "companies"
  audited if: proc { true }
end

class InclusiveCompany2 < ::ActiveRecord::Base
  self.table_name = "companies"
  audited unless: proc { false }
end

class Secret < ::ActiveRecord::Base
  audited
end

class Secret2 < ::ActiveRecord::Base
  audited
  self.non_audited_columns = ["delta", "top_secret", "created_at"]
end

describe Audited::Auditor do
  describe "configuration" do
    it "should include instance methods" do
      expect(Models::ActiveRecord::User.new).to be_a_kind_of(Audited::Auditor::AuditedInstanceMethods)
    end

    it "should include class methods" do
      expect(Models::ActiveRecord::User).to be_a_kind_of(Audited::Auditor::AuditedClassMethods)
    end

    ["created_at", "updated_at", "created_on", "updated_on", "lock_version", "id", "password"].each do |column|
      it "should not audit #{column}" do
        expect(Models::ActiveRecord::User.non_audited_columns).to include(column)
      end
    end

    context "should be configurable which conditions are audited" do
      subject { ConditionalCompany.new.send(:auditing_enabled) }

      context "when condition method is private" do
        subject { ConditionalPrivateCompany.new.send(:auditing_enabled) }

        it { is_expected.to be_truthy }
      end

      context "when passing a method name" do
        context "when conditions are true" do
          before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(true) }
          it { is_expected.to be_truthy }
        end

        context "when conditions are false" do
          before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(false) }
          it { is_expected.to be_falsey }
        end
      end

      context "when passing a Proc" do
        context "when conditions are true" do
          subject { InclusiveCompany.new.send(:auditing_enabled) }

          it { is_expected.to be_truthy }
        end

        context "when conditions are false" do
          subject { ExclusiveCompany.new.send(:auditing_enabled) }
          it { is_expected.to be_falsey }
        end
      end
    end

    context "should be configurable which conditions aren't audited" do
      context "when using a method name" do
        subject { ExclusionaryCompany.new.send(:auditing_enabled) }

        context "when conditions are true" do
          before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(true) }
          it { is_expected.to be_falsey }
        end

        context "when conditions are false" do
          before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(false) }
          it { is_expected.to be_truthy }
        end
      end

      context "when using a proc" do
        context "when conditions are true" do
          subject { ExclusionaryCompany2.new.send(:auditing_enabled) }
          it { is_expected.to be_falsey }
        end

        context "when conditions are false" do
          subject { InclusiveCompany2.new.send(:auditing_enabled) }
          it { is_expected.to be_truthy }
        end
      end
    end

    it "should be configurable which attributes are not audited via ignored_attributes" do
      Audited.ignored_attributes = ["delta", "top_secret", "created_at", "updated_at"]

      expect(Secret.non_audited_columns).to include("delta", "top_secret", "created_at")
    end

    it "should be configurable which attributes are not audited via non_audited_columns=" do
      expect(Secret2.non_audited_columns).to include("delta", "top_secret", "created_at")
    end

    it "should not save non-audited columns" do
      previous = Models::ActiveRecord::User.non_audited_columns
      begin
        Models::ActiveRecord::User.non_audited_columns += [:favourite_device]

        expect(create_user.audits.first.audited_changes.keys.any? { |col| ["favourite_device", "created_at", "updated_at", "password"].include?(col) }).to eq(false)
      ensure
        Models::ActiveRecord::User.non_audited_columns = previous
      end
    end

    it "should not save other columns than specified in 'only' option" do
      user = Models::ActiveRecord::UserOnlyPassword.create
      user.instance_eval do
        def non_column_attr
          @non_column_attr
        end

        def non_column_attr=(val)
          attribute_will_change!("non_column_attr")
          @non_column_attr = val
        end
      end

      user.password = "password"
      user.non_column_attr = "some value"
      user.save!
      expect(user.audits.last.audited_changes.keys).to eq(%w[password])
    end

    it "should save attributes not specified in 'except' option" do
      user = Models::ActiveRecord::User.create
      user.instance_eval do
        def non_column_attr
          @non_column_attr
        end

        def non_column_attr=(val)
          attribute_will_change!("non_column_attr")
          @non_column_attr = val
        end
      end

      user.password = "password"
      user.non_column_attr = "some value"
      user.save!
      expect(user.audits.last.audited_changes.keys).to eq(%w[non_column_attr])
    end

    it "should redact columns specified in 'redacted' option" do
      redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
      user = Models::ActiveRecord::UserRedactedPassword.create(password: "password")
      user.save!
      expect(user.audits.last.audited_changes["password"]).to eq(redacted)
      user.password = "new_password"
      user.save!
      expect(user.audits.last.audited_changes["password"]).to eq([redacted, redacted])
    end

    it "should redact columns specified in 'redacted' option when there are multiple specified" do
      redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
      user =
        Models::ActiveRecord::UserMultipleRedactedAttributes.create(
          password: "password"
        )
      user.save!
      expect(user.audits.last.audited_changes["password"]).to eq(redacted)
      # Saving '[REDACTED]' value for 'ssn' even if value wasn't set explicitly when record was created
      expect(user.audits.last.audited_changes["ssn"]).to eq(redacted)

      user.password = "new_password"
      user.ssn = 987654321
      user.save!
      expect(user.audits.last.audited_changes["password"]).to eq([redacted, redacted])
      expect(user.audits.last.audited_changes["ssn"]).to eq([redacted, redacted])

      # If we haven't changed any attrs from 'redacted' list, audit should not contain these keys
      user.name = "new name"
      user.save!
      expect(user.audits.last.audited_changes).to have_key("name")
      expect(user.audits.last.audited_changes).not_to have_key("password")
      expect(user.audits.last.audited_changes).not_to have_key("ssn")
    end

    it "should redact columns in 'redacted' column with custom option" do
      user = Models::ActiveRecord::UserRedactedPasswordCustomRedaction.create(password: "password")
      user.save!
      expect(user.audits.last.audited_changes["password"]).to eq(["My", "Custom", "Value", 7])
    end

    context "when ignored_default_callbacks is set" do
      before { Audited.ignored_default_callbacks = [:create] }
      after { Audited.ignored_default_callbacks = [] }

      it "should remove create callback" do
        class DefaultCallback < ::ActiveRecord::Base
          audited
        end

        expect(DefaultCallback.audited_options[:on]).to eq([:update, :touch, :destroy])
      end

      it "should keep create callback if specified" do
        class CallbacksSpecified < ::ActiveRecord::Base
          audited on: [:create, :update, :destroy]
        end

        expect(CallbacksSpecified.audited_options[:on]).to eq([:create, :update, :destroy])
      end
    end

    if ::ActiveRecord::VERSION::MAJOR >= 7
      it "should filter encrypted attributes" do
        user = Models::ActiveRecord::UserWithEncryptedPassword.create(password: "password")
        user.save
        expect(user.audits.last.audited_changes["password"]).to eq("[FILTERED]")
      end
    end

    if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
      describe "'json' and 'jsonb' audited_changes column type" do
        let(:migrations_path) { SPEC_ROOT.join("support/active_record/postgres") }

        after do
          run_migrations(:down, migrations_path)
        end

        it "should work if column type is 'json'" do
          run_migrations(:up, migrations_path, 1)
          Audited::Audit.reset_column_information
          expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("json")

          user = Models::ActiveRecord::User.create
          user.name = "new name"
          user.save!
          expect(user.audits.last.audited_changes).to eq({"name" => [nil, "new name"]})
        end

        it "should work if column type is 'jsonb'" do
          run_migrations(:up, migrations_path, 2)
          Audited::Audit.reset_column_information
          expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("jsonb")

          user = Models::ActiveRecord::User.create
          user.name = "new name"
          user.save!
          expect(user.audits.last.audited_changes).to eq({"name" => [nil, "new name"]})
        end
      end
    end
  end

  describe :new do
    it "should allow mass assignment of all unprotected attributes" do
      yesterday = 1.day.ago

      u = Models::ActiveRecord::NoAttributeProtectionUser.new(name: "name",
        username: "username",
        password: "password",
        activated: true,
        suspended_at: yesterday,
        logins: 2)

      expect(u.name).to eq("name")
      expect(u.username).to eq("username")
      expect(u.password).to eq("password")
      expect(u.activated).to eq(true)
      expect(u.suspended_at.to_i).to eq(yesterday.to_i)
      expect(u.logins).to eq(2)
    end
  end

  describe "on create" do
    let(:user) { create_user status: :reliable, audit_comment: "Create" }

    it "should change the audit count" do
      expect {
        user
      }.to change(Audited::Audit, :count).by(1)
    end

    it "should create associated audit" do
      expect(user.audits.count).to eq(1)
    end

    it "should set the action to create" do
      expect(user.audits.first.action).to eq("create")
      expect(Audited::Audit.creates.order(:id).last).to eq(user.audits.first)
      expect(user.audits.creates.count).to eq(1)
      expect(user.audits.updates.count).to eq(0)
      expect(user.audits.destroys.count).to eq(0)
    end

    it "should store all the audited attributes" do
      expect(user.audits.first.audited_changes).to eq(user.audited_attributes)
    end

    it "should store enum value" do
      expect(user.audits.first.audited_changes["status"]).to eq(1)
    end

    context "when store_synthesized_enums is set to true" do
      before { Audited.store_synthesized_enums = true }
      after { Audited.store_synthesized_enums = false }

      it "should store enum value as Rails synthesized value" do
        expect(user.audits.first.audited_changes["status"]).to eq("reliable")
      end
    end

    it "should store comment" do
      expect(user.audits.first.comment).to eq("Create")
    end

    it "should not audit an attribute which is excepted if specified on create or destroy" do
      on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: "Bart")
      expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any? { |col| ["name"].include? col }).to eq(false)
    end

    it "should not save an audit if only specified on update/destroy" do
      expect {
        Models::ActiveRecord::OnUpdateDestroy.create!(name: "Bart")
      }.to_not change(Audited::Audit, :count)
    end

    it "should save readonly columns" do
      expect {
        Models::ActiveRecord::UserWithReadOnlyAttrs.create!(name: "Bart")
      }.to change(Audited::Audit, :count)
    end
  end

  describe "on update" do
    before do
      @user = create_user(name: "Brandon", status: :active, audit_comment: "Update")
    end

    it "should save an audit" do
      expect {
        @user.update_attribute(:name, "Someone")
      }.to change(Audited::Audit, :count).by(1)
      expect {
        @user.update_attribute(:name, "Someone else")
      }.to change(Audited::Audit, :count).by(1)
    end

    it "should set the action to 'update'" do
      @user.update! name: "Changed"
      expect(@user.audits.last.action).to eq("update")
      expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
      expect(@user.audits.updates.last).to eq(@user.audits.last)
    end

    it "should store the changed attributes" do
      @user.update! name: "Changed"
      expect(@user.audits.last.audited_changes).to eq({"name" => ["Brandon", "Changed"]})
    end

    it "should store changed enum values" do
      @user.update! status: 1
      expect(@user.audits.last.audited_changes["status"]).to eq([0, 1])
    end

    it "should store audit comment" do
      expect(@user.audits.last.comment).to eq("Update")
    end

    it "should not save an audit if only specified on create/destroy" do
      on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create(name: "Bart")
      expect {
        on_create_destroy.update! name: "Changed"
      }.to_not change(Audited::Audit, :count)
    end

    it "should not save an audit if the value doesn't change after type casting" do
      @user.update! logins: 0, activated: true
      expect { @user.update_attribute :logins, "0" }.to_not change(Audited::Audit, :count)
      expect { @user.update_attribute :activated, 1 }.to_not change(Audited::Audit, :count)
      expect { @user.update_attribute :activated, "1" }.to_not change(Audited::Audit, :count)
    end

    context "with readonly attributes" do
      before do
        @user = create_user_with_readonly_attrs(status: "active")
      end

      it "should not save readonly columns" do
        expect { @user.update! status: "banned" }.to_not change(Audited::Audit, :count)
      end
    end

    describe "with no dirty changes" do
      it "does not create an audit if the record is not changed" do
        expect {
          @user.save!
        }.to_not change(Audited::Audit, :count)
      end

      it "creates an audit when an audit comment is present" do
        expect {
          @user.audit_comment = "Comment"
          @user.save!
        }.to change(Audited::Audit, :count)
      end
    end
  end

  if ::ActiveRecord::VERSION::MAJOR >= 6
    describe "on touch" do
      before do
        @user = create_user(name: "Brandon", status: :active)
      end

      it "should save an audit" do
        expect { @user.touch(:suspended_at) }.to change(Audited::Audit, :count).by(1)
      end

      it "should set the action to 'update'" do
        @user.touch(:suspended_at)
        expect(@user.audits.last.action).to eq("update")
        expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
        expect(@user.audits.updates.last).to eq(@user.audits.last)
      end

      it "should store the changed attributes" do
        @user.touch(:suspended_at)
        expect(@user.audits.last.audited_changes["suspended_at"][0]).to be_nil
        expect(Time.parse(@user.audits.last.audited_changes["suspended_at"][1].to_s)).to be_within(2.seconds).of(Time.current)
      end

      it "should store audit comment" do
        @user.audit_comment = "Here exists a touch comment"
        @user.touch(:suspended_at)
        expect(@user.audits.last.action).to eq("update")
        expect(@user.audits.last.comment).to eq("Here exists a touch comment")
      end

      it "should not save an audit if only specified on create/destroy" do
        on_create_destroy = Models::ActiveRecord::OnCreateDestroyUser.create(name: "Bart")
        expect {
          on_create_destroy.touch(:suspended_at)
        }.to_not change(Audited::Audit, :count)
      end

      it "should store an audit if touch is the only audit" do
        on_touch = Models::ActiveRecord::OnTouchOnly.create(name: "Bart")
        expect {
          on_touch.update(name: "NotBart")
        }.to_not change(Audited::Audit, :count)
        expect {
          on_touch.touch(:suspended_at)
        }.to change(on_touch.audits, :count).from(0).to(1)

        @user.audits.destroy_all
        expect(@user.audits).to be_empty
        expect {
          @user.touch(:suspended_at)
        }.to change(@user.audits, :count).from(0).to(1)
      end

      context "don't double audit" do
        let(:user) { Models::ActiveRecord::Owner.create(name: "OwnerUser", suspended_at: 1.month.ago, companies_attributes: [{name: "OwnedCompany"}]) }
        let(:company) { user.companies.first }

        it "should only create 1 (create) audit for object" do
          expect(user.audits.count).to eq(1)
          expect(user.audits.first.action).to eq("create")
        end

        it "should only create 1 (create) audit for nested resource" do
          expect(company.audits.count).to eq(1)
          expect(company.audits.first.action).to eq("create")
        end

        context "after creating" do
          it "updating / touching nested resource shouldn't save touch audit on parent object" do
            expect { company.touch(:type) }.not_to change(user.audits, :count)
            expect { company.update(type: "test") }.not_to change(user.audits, :count)
          end

          it "updating / touching parent object shouldn't save previous data" do
            expect { user.touch(:suspended_at) }.to change(user.audits, :count).from(1).to(2)
            expect(user.audits.last.action).to eq("update")
            expect(user.audits.last.audited_changes.keys).to eq(%w[suspended_at])
          end

          it "updating nested resource through parent while changing an enum on parent shouldn't double audit" do
            user.status = :reliable
            user.companies_attributes = [{name: "test"}]
            expect { user.save }.to change(user.audits, :count).from(1).to(2)
            expect(user.audits.last.action).to eq("update")
            expect(user.audits.last.audited_changes.keys).to eq(%w[status])
          end
        end

        context "after updating" do
          it "changing nested resource shouldn't audit owner" do
            expect { user.update(username: "test") }.to change(user.audits, :count).from(1).to(2)
            expect { company.update(type: "test") }.not_to change(user.audits, :count)

            expect { user.touch(:suspended_at) }.to change(user.audits, :count).from(2).to(3)
            expect { company.update(type: "another_test") }.not_to change(user.audits, :count)
          end
        end
      end
    end
  end

  describe "on destroy" do
    before do
      @user = create_user(status: :active)
    end

    it "should save an audit" do
      expect {
        @user.destroy
      }.to change(Audited::Audit, :count)

      expect(@user.audits.size).to eq(2)
    end

    it "should set the action to 'destroy'" do
      @user.destroy

      expect(@user.audits.last.action).to eq("destroy")
      expect(Audited::Audit.destroys.order(:id).last).to eq(@user.audits.last)
      expect(@user.audits.destroys.last).to eq(@user.audits.last)
    end

    it "should store all of the audited attributes" do
      @user.destroy

      expect(@user.audits.last.audited_changes).to eq(@user.audited_attributes)
    end

    it "should store enum value" do
      @user.destroy
      expect(@user.audits.last.audited_changes["status"]).to eq(0)
    end

    it "should be able to reconstruct a destroyed record without history" do
      @user.audits.delete_all
      @user.destroy

      revision = @user.audits.first.revision
      expect(revision.name).to eq(@user.name)
    end

    it "should not save an audit if only specified on create/update" do
      on_create_update = Models::ActiveRecord::OnCreateUpdate.create!(name: "Bart")

      expect {
        on_create_update.destroy
      }.to_not change(Audited::Audit, :count)
    end

    it "should audit dependent destructions" do
      owner = Models::ActiveRecord::Owner.create!
      company = owner.companies.create!

      expect {
        owner.destroy
      }.to change(Audited::Audit, :count)

      expect(company.audits.map { |a| a.action }).to eq(["create", "destroy"])
    end
  end

  describe "on destroy with unsaved object" do
    let(:user) { Models::ActiveRecord::User.new }

    it "should not audit on 'destroy'" do
      expect {
        user.destroy
      }.to_not raise_error

      expect(user.audits).to be_empty
    end
  end

  describe "associated with" do
    let(:owner) { Models::ActiveRecord::Owner.create(name: "Models::ActiveRecord::Owner") }
    let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: "The auditors", owner: owner) }

    it "should record the associated object on create" do
      expect(owned_company.audits.first.associated).to eq(owner)
    end

    it "should store the associated object on update" do
      owned_company.update_attribute(:name, "The Auditors")
      expect(owned_company.audits.last.associated).to eq(owner)
    end

    it "should store the associated object on destroy" do
      owned_company.destroy
      expect(owned_company.audits.last.associated).to eq(owner)
    end
  end

  describe "has associated audits" do
    let!(:owner) { Models::ActiveRecord::Owner.create!(name: "Models::ActiveRecord::Owner") }
    let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: "The auditors", owner: owner) }

    it "should list the associated audits" do
      expect(owner.associated_audits.length).to eq(1)
      expect(owner.associated_audits.first.auditable).to eq(owned_company)
    end
  end

  describe "max_audits" do
    it "should respect global setting" do
      stub_global_max_audits(10) do
        expect(Models::ActiveRecord::User.audited_options[:max_audits]).to eq(10)
      end
    end

    it "should respect per model setting" do
      stub_global_max_audits(10) do
        expect(Models::ActiveRecord::MaxAuditsUser.audited_options[:max_audits]).to eq(5)
      end
    end

    it "should delete old audits when keeped amount exceeded" do
      stub_global_max_audits(2) do
        user = create_versions(2)
        user.update(name: "John")
        expect(user.audits.pluck(:version)).to eq([2, 3])
      end
    end

    it "should not delete old audits when keeped amount not exceeded" do
      stub_global_max_audits(3) do
        user = create_versions(2)
        user.update(name: "John")
        expect(user.audits.pluck(:version)).to eq([1, 2, 3])
      end
    end

    it "should delete old extra audits after introducing limit" do
      stub_global_max_audits(nil) do
        user = Models::ActiveRecord::User.create!(name: "Brandon", username: "brandon")
        user.update!(name: "Foobar")
        user.update!(name: "Awesome", username: "keepers")
        user.update!(activated: true)

        Audited.max_audits = 3
        Models::ActiveRecord::User.send(:normalize_audited_options)
        user.update!(favourite_device: "Android Phone")
        audits = user.audits

        expect(audits.count).to eq(3)
        expect(audits[0].audited_changes).to include({"name" => ["Foobar", "Awesome"], "username" => ["brandon", "keepers"]})
        expect(audits[1].audited_changes).to eq({"activated" => [nil, true]})
        expect(audits[2].audited_changes).to eq({"favourite_device" => [nil, "Android Phone"]})
      end
    end

    it "should add comment line for combined audit" do
      stub_global_max_audits(2) do
        user = Models::ActiveRecord::User.create!(name: "Foobar 1")
        user.update(name: "Foobar 2", audit_comment: "First audit comment")
        user.update(name: "Foobar 3", audit_comment: "Second audit comment")
        expect(user.audits.first.comment).to match(/First audit comment.+is the result of multiple/m)
      end
    end

    def stub_global_max_audits(max_audits)
      previous_max_audits = Audited.max_audits
      previous_user_audited_options = Models::ActiveRecord::User.audited_options.dup
      begin
        Audited.max_audits = max_audits
        Models::ActiveRecord::User.send(:normalize_audited_options) # reloads audited_options
        yield
      ensure
        Audited.max_audits = previous_max_audits
        Models::ActiveRecord::User.audited_options = previous_user_audited_options
      end
    end
  end

  describe "revisions" do
    let(:user) { create_versions }

    it "should return an Array of Users" do
      expect(user.revisions).to be_a_kind_of(Array)
      user.revisions.each { |version| expect(version).to be_a_kind_of Models::ActiveRecord::User }
    end

    it "should have one revision for a new record" do
      expect(create_user.revisions.size).to eq(1)
    end

    it "should have one revision for each audit" do
      expect(user.audits.size).to eql(user.revisions.size)
    end

    it "should set the attributes for each revision" do
      u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
      u.update! name: "Foobar"
      u.update! name: "Awesome", username: "keepers"

      expect(u.revisions.size).to eql(3)

      expect(u.revisions[0].name).to eql("Brandon")
      expect(u.revisions[0].username).to eql("brandon")

      expect(u.revisions[1].name).to eql("Foobar")
      expect(u.revisions[1].username).to eql("brandon")

      expect(u.revisions[2].name).to eql("Awesome")
      expect(u.revisions[2].username).to eql("keepers")
    end

    it "access to only recent revisions" do
      u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
      u.update! name: "Foobar"
      u.update! name: "Awesome", username: "keepers"

      expect(u.revisions(2).size).to eq(2)

      expect(u.revisions(2)[0].name).to eq("Foobar")
      expect(u.revisions(2)[0].username).to eq("brandon")

      expect(u.revisions(2)[1].name).to eq("Awesome")
      expect(u.revisions(2)[1].username).to eq("keepers")
    end

    it "should be empty if no audits exist" do
      user.audits.delete_all
      expect(user.revisions).to be_empty
    end

    it "should ignore attributes that have been deleted" do
      user.audits.last.update! audited_changes: {old_attribute: "old value"}
      expect { user.revisions }.to_not raise_error
    end
  end

  describe "revisions" do
    let(:user) { create_versions(5) }

    it "should maintain identity" do
      expect(user.revision(1)).to eq(user)
    end

    it "should find the given revision" do
      revision = user.revision(3)
      expect(revision).to be_a_kind_of(Models::ActiveRecord::User)
      expect(revision.audit_version).to eq(3)
      expect(revision.name).to eq("Foobar 3")
    end

    it "should find the previous revision with :previous" do
      revision = user.revision(:previous)
      expect(revision.audit_version).to eq(4)
      # expect(revision).to eq(user.revision(4))
      expect(revision.attributes).to eq(user.revision(4).attributes)
    end

    it "should be able to get the previous revision repeatedly" do
      previous = user.revision(:previous)
      expect(previous.audit_version).to eq(4)
      expect(previous.revision(:previous).audit_version).to eq(3)
    end

    it "should be able to set protected attributes" do
      u = Models::ActiveRecord::User.create(name: "Brandon")
      u.update_attribute :logins, 1
      u.update_attribute :logins, 2

      expect(u.revision(3).logins).to eq(2)
      expect(u.revision(2).logins).to eq(1)
      expect(u.revision(1).logins).to eq(0)
    end

    it "should set attributes directly" do
      u = Models::ActiveRecord::User.create(name: "<Joe>")
      expect(u.revision(1).name).to eq("&lt;Joe&gt;")
    end

    it "should set the attributes for each revision" do
      u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
      u.update! name: "Foobar"
      u.update! name: "Awesome", username: "keepers"

      expect(u.revision(3).name).to eq("Awesome")
      expect(u.revision(3).username).to eq("keepers")

      expect(u.revision(2).name).to eq("Foobar")
      expect(u.revision(2).username).to eq("brandon")

      expect(u.revision(1).name).to eq("Brandon")
      expect(u.revision(1).username).to eq("brandon")
    end

    it "should correctly restore revision with enum" do
      u = Models::ActiveRecord::User.create(status: :active)
      u.update_attribute(:status, :reliable)
      u.update_attribute(:status, :banned)

      expect(u.revision(3)).to be_banned
      expect(u.revision(2)).to be_reliable
      expect(u.revision(1)).to be_active
    end

    it "should be able to get time for first revision" do
      suspended_at = Time.zone.now
      u = Models::ActiveRecord::User.create(suspended_at: suspended_at)
      expect(u.revision(1).suspended_at.to_s).to eq(suspended_at.to_s)
    end

    it "should not raise an error when no previous audits exist" do
      user.audits.destroy_all
      expect { user.revision(:previous) }.to_not raise_error
    end

    it "should mark revision's attributes as changed" do
      expect(user.revision(1).name_changed?).to eq(true)
    end

    it "should record new audit when saving revision" do
      expect {
        user.revision(1).save!
      }.to change(user.audits, :count).by(1)
    end

    it "should re-insert destroyed records" do
      user.destroy
      expect {
        user.revision(1).save!
      }.to change(Models::ActiveRecord::User, :count).by(1)
    end

    it "should return nil for values greater than the number of revisions" do
      expect(user.revision(user.revisions.count + 1)).to be_nil
    end

    it "should work with array attributes" do
      u = Models::ActiveRecord::User.create!(phone_numbers: ["+1 800-444-4444"])
      u.update!(phone_numbers: ["+1 804-222-1111", "+1 317 222-2222"])

      expect(u.revision(0).phone_numbers).to eq(["+1 804-222-1111", "+1 317 222-2222"])
      expect(u.revision(1).phone_numbers).to eq(["+1 800-444-4444"])
    end
  end

  describe "revision_at" do
    let(:user) { create_user }

    it "should find the latest revision before the given time" do
      audit = user.audits.first
      audit.created_at = 1.hour.ago
      audit.save!
      user.update! name: "updated"
      expect(user.revision_at(2.minutes.ago).audit_version).to eq(1)
    end

    it "should be nil if given a time before audits" do
      expect(user.revision_at(1.week.ago)).to be_nil
    end
  end

  describe "own_and_associated_audits" do
    it "should return audits for self and associated audits" do
      owner = Models::ActiveRecord::Owner.create!
      company = owner.companies.create!
      company.update!(name: "Collective Idea")

      other_owner = Models::ActiveRecord::Owner.create!
      other_owner.companies.create!

      expect(owner.own_and_associated_audits).to match_array(owner.audits + company.audits)
    end

    it "should return audits for STI classes" do
      # Where parent is STI
      sti_company = Models::ActiveRecord::Company::STICompany.create!
      sti_company.update!(name: "Collective Idea")
      expect(sti_company.own_and_associated_audits).to match_array(sti_company.audits)

      # Where associated is STI
      owner = Models::ActiveRecord::Owner.create!
      company = owner.companies.create! type: "Models::ActiveRecord::OwnedCompany::STICompany"
      company.update!(name: "Collective Idea")
      expect(owner.own_and_associated_audits).to match_array(owner.audits + company.audits)
    end

    it "should order audits by creation time" do
      owner = Models::ActiveRecord::Owner.create!
      first_audit = owner.audits.first
      first_audit.update_column(:created_at, 1.year.ago)

      company = owner.companies.create!
      second_audit = company.audits.first
      second_audit.update_column(:created_at, 1.month.ago)

      company.update!(name: "Collective Idea")
      third_audit = company.audits.last
      expect(owner.own_and_associated_audits.to_a).to eq([third_audit, second_audit, first_audit])
    end
  end

  describe "without auditing" do
    it "should not save an audit when calling #save_without_auditing" do
      expect {
        u = Models::ActiveRecord::User.new(name: "Brandon")
        expect(u.save_without_auditing).to eq(true)
      }.to_not change(Audited::Audit, :count)
    end

    it "should not save an audit inside of the #without_auditing block" do
      expect {
        Models::ActiveRecord::User.without_auditing { Models::ActiveRecord::User.create!(name: "Brandon") }
      }.to_not change(Audited::Audit, :count)
    end

    context "when global audits are disabled" do
      it "should re-enable class audits after #without_auditing block" do
        Audited.auditing_enabled = false
        Models::ActiveRecord::User.without_auditing {}
        Audited.auditing_enabled = true
        expect(Models::ActiveRecord::User.auditing_enabled).to eql(true)
      end
    end

    it "should reset auditing status even it raises an exception" do
      begin
        Models::ActiveRecord::User.without_auditing { raise }
      rescue
        nil
      end
      expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
    end

    it "should be thread safe using a #without_auditing block" do
      skip if Models::ActiveRecord::User.connection.class.name.include?("SQLite")

      t1 = Thread.new do
        expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
        Models::ActiveRecord::User.without_auditing do
          expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
          Models::ActiveRecord::User.create!(name: "Bart")
          sleep 1
          expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
        end
        expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
      end

      t2 = Thread.new do
        sleep 0.5
        expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
        Models::ActiveRecord::User.create!(name: "Lisa")
      end
      t1.join
      t2.join

      expect(Models::ActiveRecord::User.find_by_name("Bart").audits.count).to eq(0)
      expect(Models::ActiveRecord::User.find_by_name("Lisa").audits.count).to eq(1)
    end

    it "should not save an audit when auditing is globally disabled" do
      expect(Audited.auditing_enabled).to eq(true)
      Audited.auditing_enabled = false
      expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)

      user = create_user
      expect(user.audits.count).to eq(0)

      Audited.auditing_enabled = true
      expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)

      user.update!(name: "Test")
      expect(user.audits.count).to eq(1)
      Models::ActiveRecord::User.enable_auditing
    end
  end

  describe "with auditing" do
    it "should save an audit when calling #save_with_auditing" do
      expect {
        u = Models::ActiveRecord::User.new(name: "Brandon")
        Models::ActiveRecord::User.auditing_enabled = false
        expect(u.save_with_auditing).to eq(true)
        Models::ActiveRecord::User.auditing_enabled = true
      }.to change(Audited::Audit, :count).by(1)
    end

    it "should save an audit inside of the #with_auditing block" do
      expect {
        Models::ActiveRecord::User.auditing_enabled = false
        Models::ActiveRecord::User.with_auditing { Models::ActiveRecord::User.create!(name: "Brandon") }
        Models::ActiveRecord::User.auditing_enabled = true
      }.to change(Audited::Audit, :count).by(1)
    end

    context "when global audits are disabled" do
      it "should re-enable class audits after #with_auditing block" do
        Audited.auditing_enabled = false
        Models::ActiveRecord::User.with_auditing {}
        Audited.auditing_enabled = true
        expect(Models::ActiveRecord::User.auditing_enabled).to eql(true)
      end
    end

    it "should reset auditing status even it raises an exception" do
      Models::ActiveRecord::User.disable_auditing
      begin
        Models::ActiveRecord::User.with_auditing { raise }
      rescue
        nil
      end
      expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
      Models::ActiveRecord::User.enable_auditing
    end

    it "should be thread safe using a #with_auditing block" do
      skip if Models::ActiveRecord::User.connection.class.name.include?("SQLite")

      t1 = Thread.new do
        Models::ActiveRecord::User.disable_auditing
        expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
        Models::ActiveRecord::User.with_auditing do
          expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)

          Models::ActiveRecord::User.create!(name: "Shaggy")
          sleep 1
          expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
        end
        expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
        Models::ActiveRecord::User.enable_auditing
      end

      t2 = Thread.new do
        sleep 0.5
        Models::ActiveRecord::User.disable_auditing
        expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
        Models::ActiveRecord::User.create!(name: "Scooby")
        Models::ActiveRecord::User.enable_auditing
      end
      t1.join
      t2.join

      Models::ActiveRecord::User.enable_auditing
      expect(Models::ActiveRecord::User.find_by_name("Shaggy").audits.count).to eq(1)
      expect(Models::ActiveRecord::User.find_by_name("Scooby").audits.count).to eq(0)
    end
  end

  describe "comment required" do
    describe "on create" do
      it "should not validate when audit_comment is not supplied when initialized" do
        expect(Models::ActiveRecord::CommentRequiredUser.new(name: "Foo")).not_to be_valid
      end

      it "should not validate when audit_comment is not supplied trying to create" do
        expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo")).not_to be_valid
      end

      it "should validate when audit_comment is supplied" do
        expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo", audit_comment: "Create")).to be_valid
      end

      it "should validate when audit_comment is not supplied, and creating is not being audited" do
        expect(Models::ActiveRecord::OnUpdateCommentRequiredUser.create(name: "Foo")).to be_valid
        expect(Models::ActiveRecord::OnDestroyCommentRequiredUser.create(name: "Foo")).to be_valid
      end

      it "should validate when audit_comment is not supplied, and auditing is disabled" do
        Models::ActiveRecord::CommentRequiredUser.disable_auditing
        expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo")).to be_valid
        Models::ActiveRecord::CommentRequiredUser.enable_auditing
      end

      it "should validate when audit_comment is not supplied, and only excluded attributes changed" do
        expect(Models::ActiveRecord::CommentRequiredUser.new(password: "Foo")).to be_valid
      end
    end

    describe "on update" do
      let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create") }
      let(:on_create_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
      let(:on_destroy_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }

      it "should not validate when audit_comment is not supplied" do
        expect(user.update(name: "Test")).to eq(false)
      end

      it "should validate when audit_comment is not supplied, and updating is not being audited" do
        expect(on_create_user.update(name: "Test")).to eq(true)
        expect(on_destroy_user.update(name: "Test")).to eq(true)
      end

      it "should validate when audit_comment is supplied" do
        expect(user.update(name: "Test", audit_comment: "Update")).to eq(true)
      end

      it "should validate when audit_comment is not supplied, and auditing is disabled" do
        Models::ActiveRecord::CommentRequiredUser.disable_auditing
        expect(user.update(name: "Test")).to eq(true)
        Models::ActiveRecord::CommentRequiredUser.enable_auditing
      end

      it "should validate when audit_comment is not supplied, and only excluded attributes changed" do
        expect(user.update(password: "Test")).to eq(true)
      end
    end

    describe "on destroy" do
      let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create") }
      let(:on_create_user) { Models::ActiveRecord::OnCreateCommentRequiredUser.create!(audit_comment: "Create") }
      let(:on_update_user) { Models::ActiveRecord::OnUpdateCommentRequiredUser.create }

      it "should not validate when audit_comment is not supplied" do
        expect(user.destroy).to eq(false)
      end

      it "should validate when audit_comment is supplied" do
        user.audit_comment = "Destroy"
        expect(user.destroy).to eq(user)
      end

      it "should validate when audit_comment is not supplied, and destroying is not being audited" do
        expect(on_create_user.destroy).to eq(on_create_user)
        expect(on_update_user.destroy).to eq(on_update_user)
      end

      it "should validate when audit_comment is not supplied, and auditing is disabled" do
        Models::ActiveRecord::CommentRequiredUser.disable_auditing
        expect(user.destroy).to eq(user)
        Models::ActiveRecord::CommentRequiredUser.enable_auditing
      end
    end
  end

  describe "no update with comment only" do
    let(:user) { Models::ActiveRecord::NoUpdateWithCommentOnlyUser.create }

    it "does not create an audit when only an audit_comment is present" do
      user.audit_comment = "Comment"
      expect { user.save! }.to_not change(Audited::Audit, :count)
    end
  end

  describe "attr_protected and attr_accessible" do
    it "should not raise error when attr_accessible is set and protected is false" do
      expect {
        Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: "No fail!")
      }.to_not raise_error
    end

    it "should not rause an error when attr_accessible is declared before audited" do
      expect {
        Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: "No fail!")
      }.to_not raise_error
    end
  end

  describe "audit_as" do
    let(:user) { Models::ActiveRecord::User.create name: "Testing" }

    it "should record user objects" do
      Models::ActiveRecord::Company.audit_as(user) do
        company = Models::ActiveRecord::Company.create name: "The auditors"
        company.update! name: "The Auditors"

        company.audits.each do |audit|
          expect(audit.user).to eq(user)
        end
      end
    end

    it "should record usernames" do
      Models::ActiveRecord::Company.audit_as(user.name) do
        company = Models::ActiveRecord::Company.create name: "The auditors"
        company.update! name: "The Auditors"

        company.audits.each do |audit|
          expect(audit.user).to eq(user.name)
        end
      end
    end
  end

  describe "after_audit" do
    let(:user) { Models::ActiveRecord::UserWithAfterAudit.new }

    it "should invoke after_audit callback on create" do
      expect(user.bogus_attr).to be_nil
      expect(user.save).to eq(true)
      expect(user.bogus_attr).to eq("do something")
    end
  end

  describe "around_audit" do
    let(:user) { Models::ActiveRecord::UserWithAfterAudit.new }

    it "should invoke around_audit callback on create" do
      expect(user.around_attr).to be_nil
      expect(user.save).to eq(true)
      expect(user.around_attr).to eq(user.audits.last)
    end
  end

  describe "STI auditing" do
    it "should correctly disable auditing when using STI" do
      company = Models::ActiveRecord::Company::STICompany.create name: "The auditors"
      expect(company.type).to eq("Models::ActiveRecord::Company::STICompany")
      expect {
        Models::ActiveRecord::Company.auditing_enabled = false
        company.update! name: "STI auditors"
        Models::ActiveRecord::Company.auditing_enabled = true
      }.to_not change(Audited::Audit, :count)
    end
  end

  describe "call audit multiple times" do
    it "should update audit options" do
      user = Models::ActiveRecord::UserOnlyName.create
      user.update(password: "new password 1", name: "new name 1")
      expect(user.audits.last.audited_changes.keys).to eq(%w[name])

      user.class.class_eval do
        audited only: :password
      end

      user = Models::ActiveRecord::UserOnlyName.last
      user.update(password: "new password 2", name: "new name 2")
      expect(user.audits.last.audited_changes.keys).to eq(%w[password])
    end
  end
end
