# frozen_string_literal: true

require "cases/helper"
require "support/ddl_helper"
require "models/developer"
require "models/computer"
require "models/owner"
require "models/pet"
require "models/toy"
require "models/car"
require "models/task"

class TimestampTest < ActiveRecord::TestCase
  fixtures :developers, :owners, :pets, :toys, :cars, :tasks

  def setup
    @developer = Developer.first
    @owner = Owner.first
    @developer.update_columns(updated_at: Time.now.prev_month)
    @previously_updated_at = @developer.updated_at
  end

  def test_saving_a_changed_record_updates_its_timestamp
    @developer.name = "Jack Bauer"
    @developer.save!

    assert_not_equal @previously_updated_at, @developer.updated_at
  end

  def test_saving_a_unchanged_record_doesnt_update_its_timestamp
    @developer.save!

    assert_equal @previously_updated_at, @developer.updated_at
  end

  def test_touching_a_record_updates_its_timestamp
    sleep 1.0 unless supports_datetime_with_precision? # Remove once MySQL 5.5 support is dropped.

    previous_salary = @developer.salary
    @developer.salary = previous_salary + 10000
    @developer.touch

    assert_not_equal @previously_updated_at, @developer.updated_at
    assert_equal previous_salary + 10000, @developer.salary
    assert_predicate @developer, :salary_changed?, "developer salary should have changed"
    assert_predicate @developer, :changed?, "developer should be marked as changed"
    assert_equal ["salary"], @developer.changed
    assert_predicate @developer, :saved_changes?
    assert_equal ["legacy_updated_at", "legacy_updated_on"], @developer.saved_changes.keys.sort

    @developer.reload
    assert_equal previous_salary, @developer.salary
  end

  def test_touching_a_record_with_default_scope_that_excludes_it_updates_its_timestamp
    sleep 1.0 unless supports_datetime_with_precision? # Remove once MySQL 5.5 support is dropped.

    developer = @developer.becomes(DeveloperCalledJamis)
    developer.touch

    assert_not_equal @previously_updated_at, developer.updated_at
    assert_not_predicate developer, :changed?
    assert_predicate developer, :saved_changes?
    assert_equal ["legacy_updated_at", "legacy_updated_on"], developer.saved_changes.keys.sort

    developer.reload
    assert_not_equal @previously_updated_at, developer.updated_at
  end

  def test_saving_when_record_timestamps_is_false_doesnt_update_its_timestamp
    Developer.record_timestamps = false
    @developer.name = "John Smith"
    @developer.save!

    assert_equal @previously_updated_at, @developer.updated_at
  ensure
    Developer.record_timestamps = true
  end

  def test_saving_when_instance_record_timestamps_is_false_doesnt_update_its_timestamp
    @developer.record_timestamps = false
    assert Developer.record_timestamps

    @developer.name = "John Smith"
    @developer.save!

    assert_equal @previously_updated_at, @developer.updated_at
  end

  def test_touching_updates_timestamp_with_given_time
    previously_updated_at = @developer.updated_at
    new_time = Time.utc(2015, 2, 16, 0, 0, 0)
    @developer.touch(time: new_time)

    assert_not_equal previously_updated_at, @developer.updated_at
    assert_equal new_time, @developer.updated_at
  end

  def test_touching_an_attribute_updates_timestamp
    previously_created_at = @developer.created_at
    travel(1.second) do
      @developer.touch(:created_at)
    end

    assert_not @developer.created_at_changed?, "created_at should not be changed"
    assert_not @developer.changed?, "record should not be changed"
    assert_not_equal previously_created_at, @developer.created_at
    assert_not_equal @previously_updated_at, @developer.updated_at
  end

  def test_touching_update_at_attribute_as_symbol_updates_timestamp
    travel(1.second) do
      @developer.touch(:updated_at)
    end

    assert_not @developer.updated_at_changed?
    assert_not @developer.changed?
    assert_not_equal @previously_updated_at, @developer.updated_at
  end

  def test_touching_an_attribute_updates_it
    task = Task.first
    previous_value = task.ending
    task.touch(:ending)

    now = Time.now.change(usec: 0)

    assert_not_equal previous_value, task.ending
    assert_in_delta now, task.ending, 1
  end

  def test_touching_an_attribute_updates_timestamp_with_given_time
    previously_updated_at = @developer.updated_at
    previously_created_at = @developer.created_at
    new_time = Time.utc(2015, 2, 16, 4, 54, 0)
    @developer.touch(:created_at, time: new_time)

    assert_not_equal previously_created_at, @developer.created_at
    assert_not_equal previously_updated_at, @developer.updated_at
    assert_equal new_time, @developer.created_at
    assert_equal new_time, @developer.updated_at
  end

  def test_touching_many_attributes_updates_them
    task = Task.first
    previous_starting = task.starting
    previous_ending = task.ending
    task.touch(:starting, :ending)

    now = Time.now.change(usec: 0)

    assert_not_equal previous_starting, task.starting
    assert_not_equal previous_ending, task.ending
    assert_in_delta now, task.starting, 1
    assert_in_delta now, task.ending, 1
  end

  def test_touching_a_record_without_timestamps_is_unexceptional
    assert_nothing_raised { Car.first.touch }
  end

  def test_touching_a_no_touching_object
    Developer.no_touching do
      assert_predicate @developer, :no_touching?
      assert_not_predicate @owner, :no_touching?
      @developer.touch
    end

    assert_not_predicate @developer, :no_touching?
    assert_not_predicate @owner, :no_touching?
    assert_equal @previously_updated_at, @developer.updated_at
  end

  def test_touching_related_objects
    @owner = Owner.first
    @previously_updated_at = @owner.updated_at

    Owner.no_touching do
      @owner.pets.first.touch
    end

    assert_equal @previously_updated_at, @owner.updated_at
  end

  def test_global_no_touching
    ActiveRecord::Base.no_touching do
      assert_predicate @developer, :no_touching?
      assert_predicate @owner, :no_touching?
      @developer.touch
    end

    assert_not_predicate @developer, :no_touching?
    assert_not_predicate @owner, :no_touching?
    assert_equal @previously_updated_at, @developer.updated_at
  end

  def test_no_touching_threadsafe
    Thread.new do
      Developer.no_touching do
        assert_predicate @developer, :no_touching?

        sleep(1)
      end
    end

    assert_not_predicate @developer, :no_touching?
  end

  def test_no_touching_with_callbacks
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "developers"

      attr_accessor :after_touch_called

      after_touch do |user|
        user.after_touch_called = true
      end
    end

    developer = klass.first

    klass.no_touching do
      developer.touch
      assert_not developer.after_touch_called
    end
  end

  def test_saving_an_unchanged_record_with_a_mutating_before_save_callback_updates_its_timestamp
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "developers"

      include Developer::TimestampAliases

      before_save :change_name

      private
        def change_name
          return if new_record?

          self.name = "Jack Bauer"
        end
    end

    @developer = klass.create!
    @previously_updated_at = @developer.updated_at
    @previous_name = @developer.name

    travel(1.second) do
      @developer.save!
    end

    assert_not_equal @previous_name, @developer.name
    assert_not_equal @previously_updated_at, @developer.updated_at
  end

  def test_saving_an_unchanged_record_with_a_mutating_before_update_callback_updates_its_timestamp
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "developers"

      include Developer::TimestampAliases

      before_update :change_name

      private
        def change_name
          return if new_record?

          self.name = "Jack Bauer"
        end
    end

    @developer = klass.create!
    @previously_updated_at = @developer.updated_at
    @previous_name = @developer.name

    travel(1.second) do
      @developer.save!
    end

    @developer.reload

    assert_not_equal @previous_name, @developer.name
    assert_not_equal @previously_updated_at, @developer.updated_at
  end

  def test_saving_an_unchanged_record_with_a_non_mutating_before_update_callback_does_not_update_its_timestamp
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "developers"

      include Developer::TimestampAliases

      before_update :change_name

      private
        def change_name; end
    end

    @developer = klass.create!
    @previously_updated_at = @developer.updated_at

    travel(1.second) do
      @developer.save!
    end

    @developer.reload

    assert_equal @previously_updated_at, @developer.updated_at
  end

  def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
    pet   = Pet.first
    owner = pet.owner
    previously_owner_updated_at = owner.updated_at

    travel(1.second) do
      pet.name = "Fluffy the Third"
      pet.save
    end

    assert_not_equal previously_owner_updated_at, pet.owner.updated_at
  end

  def test_destroying_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
    pet   = Pet.first
    owner = pet.owner
    previously_owner_updated_at = owner.updated_at

    travel(1.second) do
      pet.destroy
    end

    assert_not_equal previously_owner_updated_at, pet.owner.updated_at
  end

  def test_saving_a_new_record_belonging_to_invalid_parent_with_touch_should_not_raise_exception
    klass = Class.new(Owner) do
      def self.name; "Owner"; end
      validate { errors.add(:base, :invalid) }
    end

    pet = Pet.new(owner: klass.new)
    pet.save!

    assert_predicate pet.owner, :new_record?
  end

  def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
    klass = Class.new(ActiveRecord::Base) do
      def self.name; "Pet"; end
      belongs_to :owner, touch: :happy_at
    end

    pet   = klass.first
    owner = pet.owner
    previously_owner_happy_at = owner.happy_at

    pet.name = "Fluffy the Third"
    pet.save

    assert_not_equal previously_owner_happy_at, pet.owner.happy_at
  end

  def test_touching_a_record_with_a_belongs_to_that_uses_a_counter_cache_should_update_the_parent
    klass = Class.new(ActiveRecord::Base) do
      def self.name; "Pet"; end
      belongs_to :owner, counter_cache: :use_count, touch: true
    end

    pet = klass.first
    owner = pet.owner
    owner.update_columns(happy_at: 3.days.ago)
    previously_owner_updated_at = owner.updated_at

    travel(1.second) do
      pet.name = "I'm a parrot"
      pet.save
    end

    assert_not_equal previously_owner_updated_at, pet.owner.updated_at
  end

  def test_touching_a_record_touches_parent_record_and_grandparent_record
    klass = Class.new(ActiveRecord::Base) do
      def self.name; "Toy"; end
      belongs_to :pet, touch: true
    end

    toy = klass.first
    pet = toy.pet
    owner = pet.owner
    time = 3.days.ago

    owner.update_columns(updated_at: time)
    toy.touch
    owner.reload

    assert_not_equal time, owner.updated_at
  end

  def test_touching_a_record_touches_polymorphic_record
    klass = Class.new(ActiveRecord::Base) do
      def self.name; "Toy"; end
    end

    wheel_klass = Class.new(ActiveRecord::Base) do
      def self.name; "Wheel"; end
      belongs_to :wheelable, polymorphic: true, touch: true
    end

    toy = klass.first
    time = 3.days.ago
    toy.update_columns(updated_at: time)

    wheel = wheel_klass.new
    wheel.wheelable = toy
    wheel.save
    wheel.touch

    assert_not_equal time, toy.updated_at
  end

  def test_changing_parent_of_a_record_touches_both_new_and_old_parent_record
    klass = Class.new(ActiveRecord::Base) do
      def self.name; "Toy"; end
      belongs_to :pet, touch: true
    end

    toy1 = klass.find(1)
    old_pet = toy1.pet

    toy2 = klass.find(2)
    new_pet = toy2.pet
    time = 3.days.ago.at_beginning_of_hour

    old_pet.update_columns(updated_at: time)
    new_pet.update_columns(updated_at: time)

    toy1.pet = new_pet
    toy1.save!

    old_pet.reload
    new_pet.reload

    assert_not_equal time, new_pet.updated_at
    assert_not_equal time, old_pet.updated_at
  end

  def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_within_same_class
    car_class = Class.new(ActiveRecord::Base) do
      def self.name; "Car"; end
    end

    wheel_class = Class.new(ActiveRecord::Base) do
      def self.name; "Wheel"; end
      belongs_to :wheelable, polymorphic: true, touch: true
    end

    car1 = car_class.find(1)
    car2 = car_class.find(2)

    wheel = wheel_class.create!(wheelable: car1)

    time = 3.days.ago.at_beginning_of_hour

    car1.update_columns(updated_at: time)
    car2.update_columns(updated_at: time)

    wheel.wheelable = car2
    wheel.save!

    assert_not_equal time, car1.reload.updated_at
    assert_not_equal time, car2.reload.updated_at
  end

  def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_with_other_class
    car_class = Class.new(ActiveRecord::Base) do
      def self.name; "Car"; end
    end

    toy_class = Class.new(ActiveRecord::Base) do
      def self.name; "Toy"; end
    end

    wheel_class = Class.new(ActiveRecord::Base) do
      def self.name; "Wheel"; end
      belongs_to :wheelable, polymorphic: true, touch: true
    end

    car = car_class.find(1)
    toy = toy_class.find(3)

    wheel = wheel_class.create!(wheelable: car)

    time = 3.days.ago.at_beginning_of_hour

    car.update_columns(updated_at: time)
    toy.update_columns(updated_at: time)

    wheel.wheelable = toy
    wheel.save!

    assert_not_equal time, car.reload.updated_at
    assert_not_equal time, toy.reload.updated_at
  end

  def test_clearing_association_touches_the_old_record
    klass = Class.new(ActiveRecord::Base) do
      def self.name; "Toy"; end
      belongs_to :pet, touch: true
    end

    toy = klass.find(1)
    pet = toy.pet
    time = 3.days.ago.at_beginning_of_hour

    pet.update_columns(updated_at: time)

    toy.pet = nil
    toy.save!

    pet.reload

    assert_not_equal time, pet.updated_at
  end

  def test_timestamp_column_values_are_present_in_create_callbacks
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "people"

      before_create do
        self.born_at = created_at
      end
    end

    person = klass.create first_name: "David"
    assert_not_nil person.born_at
  end

  def test_timestamp_column_values_are_present_in_update_callbacks
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "people"

      before_update do
        self.born_at = created_at
      end
    end

    person = klass.create first_name: "David"
    person.update first_name: "John"
    assert_not_nil person.born_at
  end

  def test_timestamp_column_values_are_present_in_save_callbacks
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "people"

      before_create do
        self.born_at = created_at
      end
    end

    person = klass.create first_name: "David"
    assert_not_nil person.born_at
  end

  def test_timestamp_attributes_for_create_in_model
    toy = Toy.first
    assert_equal ["created_at"], toy.send(:timestamp_attributes_for_create_in_model)
  end

  def test_timestamp_attributes_for_update_in_model
    toy = Toy.first
    assert_equal ["updated_at"], toy.send(:timestamp_attributes_for_update_in_model)
  end

  def test_all_timestamp_attributes_in_model
    toy = Toy.first
    assert_equal ["created_at", "updated_at"], toy.send(:all_timestamp_attributes_in_model)
  end
end

class TimestampsWithoutTransactionTest < ActiveRecord::TestCase
  include DdlHelper
  self.use_transactional_tests = false

  class TimestampAttributePost < ActiveRecord::Base
    attr_accessor :created_at, :updated_at
  end

  def test_do_not_write_timestamps_on_save_if_they_are_not_attributes
    with_example_table ActiveRecord::Base.lease_connection, "timestamp_attribute_posts", "id integer primary key" do
      post = TimestampAttributePost.new(id: 1)
      post.save! # should not try to assign and persist created_at, updated_at
      assert_nil post.created_at
      assert_nil post.updated_at
    end
  end

  def test_index_is_created_for_both_timestamps
    ActiveRecord::Base.lease_connection.create_table(:foos, force: true) do |t|
      t.timestamps null: true, index: true
    end

    indexes = ActiveRecord::Base.lease_connection.indexes("foos")
    assert_equal ["created_at", "updated_at"], indexes.flat_map(&:columns).sort
  ensure
    ActiveRecord::Base.lease_connection.drop_table(:foos)
  end
end
