# frozen_string_literal: true

require_relative "abstract_unit"
require "active_support/core_ext/date_time"
require "active_support/core_ext/numeric/time"
require "active_support/core_ext/string/conversions"
require_relative "time_zone_test_helpers"

class TimeTravelTest < ActiveSupport::TestCase
  include TimeZoneTestHelpers

  class TimeSubclass < ::Time; end
  class DateSubclass < ::Date; end
  class DateTimeSubclass < ::DateTime; end

  class TravelClass
    include ActiveSupport::Testing::TimeHelpers

    def travel_to_no_block(date)
      travel_to(date)
    end

    def travel_to_block(date)
      travel_to(date) { }
    end
  end

  def test_time_helper_travel
    Time.stub(:now, Time.now) do
      expected_time = Time.now + 1.day
      travel 1.day

      assert_equal expected_time.to_fs(:db), Time.now.to_fs(:db)
      assert_equal expected_time.to_date, Date.today
      assert_equal expected_time.to_datetime.to_fs(:db), DateTime.now.to_fs(:db)

      assert_equal expected_time.to_fs(:db), Time.new.to_fs(:db)
      if RUBY_VERSION >= "3.2"
        assert_not_equal expected_time.to_fs(:db), Time.new(precision: 3).to_fs(:db)
      end
    ensure
      travel_back
    end
  end

  def test_time_helper_travel_with_block
    Time.stub(:now, Time.now) do
      expected_time = Time.now + 1.day

      travel 1.day do
        assert_equal expected_time.to_fs(:db), Time.now.to_fs(:db)
        assert_equal expected_time.to_date, Date.today
        assert_equal expected_time.to_datetime.to_fs(:db), DateTime.now.to_fs(:db)

        assert_equal expected_time.to_fs(:db), Time.new.to_fs(:db)
        if RUBY_VERSION >= "3.2"
          assert_not_equal expected_time.to_fs(:db), Time.new(precision: 3).to_fs(:db)
          assert_equal Time.new("2000-12-31 23:59:59.567"), Time.new("2000-12-31 23:59:59.56789", precision: 3)
        end
      end

      assert_not_equal expected_time.to_fs(:db), Time.now.to_fs(:db)
      assert_not_equal expected_time.to_date, Date.today
      assert_not_equal expected_time.to_datetime.to_fs(:db), DateTime.now.to_fs(:db)
      if RUBY_VERSION >= "3.2"
        assert_equal Time.new("2000-12-31 23:59:59.567"), Time.new("2000-12-31 23:59:59.56789", precision: 3)
      end
    end
  end

  def test_time_helper_travel_to
    Time.stub(:now, Time.now) do
      expected_time = Time.new(2004, 11, 24, 1, 4, 44)
      travel_to expected_time

      assert_equal expected_time, Time.now
      assert_equal expected_time, Time.new
      assert_not_equal expected_time, Time.new(2004, 11, 25)
      if RUBY_VERSION >= "3.2"
        assert_not_equal expected_time, Time.new(precision: 3)
      end
      assert_equal Date.new(2004, 11, 24), Date.today
      assert_equal expected_time.to_datetime, DateTime.now
    ensure
      travel_back
    end
  end

  def test_time_helper_travel_to_with_block
    Time.stub(:now, Time.now) do
      expected_time = Time.new(2004, 11, 24, 1, 4, 44)

      travel_to expected_time do
        assert_equal expected_time, Time.now
        assert_equal expected_time, Time.new
        if RUBY_VERSION >= "3.2"
          assert_not_equal expected_time, Time.new(precision: 3)
        end
        assert_not_equal expected_time, Time.new(2004, 11, 25)
        assert_equal Date.new(2004, 11, 24), Date.today
        assert_equal expected_time.to_datetime, DateTime.now
      end

      assert_not_equal expected_time, Time.now
      assert_not_equal expected_time, Time.new
      assert_not_equal Date.new(2004, 11, 24), Date.today
      assert_not_equal expected_time.to_datetime, DateTime.now
    end
  end

  def test_time_helper_travel_to_with_time_zone
    with_env_tz "US/Eastern" do
      with_tz_default ActiveSupport::TimeZone["UTC"] do
        Time.stub(:now, Time.now) do
          expected_time = 5.minutes.ago

          travel_to 5.minutes.ago do
            assert_equal expected_time.to_fs(:db), Time.zone.now.to_fs(:db)
          end
        end
      end
    end
  end

  def test_time_helper_travel_to_with_different_system_and_application_time_zones
    with_env_tz "US/Eastern" do # system time zone: -05
      expected_time_in_2021 = Time.new(2021)

      with_tz_default ActiveSupport::TimeZone["Ekaterinburg"] do # application time zone: +05
        destination_time = Time.new(2023, 12, 1, 5, 6, 7, 5 * 3600)

        # All stubbed methods are expected to return values in system (-05) time zone
        expected_utc_offset = -5 * 3600
        expected_time = Time.new(2023, 11, 30, 19, 6, 7, expected_utc_offset)
        expected_datetime = DateTime.new(2023, 11, 30, 19, 6, 7, -Rational(5, 24))
        expected_date = Date.new(2023, 11, 30)

        travel_to destination_time do
          assert_equal expected_time, Time.now
          assert_equal expected_time.to_fs(:db), Time.now.to_fs(:db)
          assert_equal expected_utc_offset, Time.now.utc_offset

          assert_equal expected_datetime, DateTime.now
          assert_equal expected_datetime.to_fs(:db), DateTime.now.to_fs(:db)
          assert_equal expected_utc_offset, DateTime.now.utc_offset

          assert_equal expected_date, Date.today

          # Time.new with no args equals to Time.now
          assert_equal expected_time, Time.new
          assert_equal expected_time.to_fs(:db), Time.new.to_fs(:db)
          assert_equal expected_utc_offset, Time.new.utc_offset

          # Time.new with any args falls back to original Ruby implementation
          assert_equal expected_time_in_2021, Time.new(2021)
          assert_equal expected_time_in_2021.to_fs(:db), Time.new(2021).to_fs(:db)
          assert_equal expected_time_in_2021.utc_offset, Time.new(2021).utc_offset
        end
      end
    end
  end

  def test_time_helper_travel_to_with_string_for_time_zone
    with_env_tz "US/Eastern" do
      with_tz_default ActiveSupport::TimeZone["UTC"] do
        Time.stub(:now, Time.now) do
          expected_time = Time.new(2004, 11, 24, 1, 4, 44)

          travel_to "2004-11-24 01:04:44" do
            assert_equal expected_time.to_fs(:db), Time.zone.now.to_fs(:db)
          end
        end
      end
    end
  end

  def test_time_helper_travel_to_with_separate_class
    travel_object = TravelClass.new
    date1 = Date.new(2004, 11, 24)
    date2 = Date.new(2005, 11, 24)

    Time.stub(:now, now = Time.now) do
      travel_to(date1) do
        travel_object.travel_to_no_block(date2)
      end
      assert_equal now, Time.now

      travel_to(date1) do
        travel_object.travel_to_no_block(date2)
        assert_equal date2, Date.today
      end
      assert_equal now, Time.now

      travel_to(date1) do
        travel_object.travel_to_block(date2)
        assert_equal date1, Date.today
      end
      assert_equal now, Time.now
    end
  end

  def test_time_helper_travel_back
    Time.stub(:now, Time.now) do
      expected_time = Time.new(2004, 11, 24, 1, 4, 44)

      travel_to expected_time
      assert_equal expected_time, Time.now
      assert_equal expected_time, Time.new
      assert_equal Date.new(2004, 11, 24), Date.today
      assert_equal expected_time.to_datetime, DateTime.now
      travel_back

      assert_not_equal expected_time, Time.now
      assert_not_equal expected_time, Time.new
      assert_not_equal Date.new(2004, 11, 24), Date.today
      assert_not_equal expected_time.to_datetime, DateTime.now
    ensure
      travel_back
    end
  end

  def test_time_helper_travel_back_with_block
    Time.stub(:now, Time.now) do
      expected_time = Time.new(2004, 11, 24, 1, 4, 44)

      travel_to expected_time
      assert_equal expected_time, Time.now
      assert_equal expected_time, Time.new
      assert_equal Date.new(2004, 11, 24), Date.today
      assert_equal expected_time.to_datetime, DateTime.now

      travel_back do
        assert_not_equal expected_time, Time.now
        assert_not_equal expected_time, Time.new
        assert_not_equal Date.new(2004, 11, 24), Date.today
        assert_not_equal expected_time.to_datetime, DateTime.now
      end

      assert_equal expected_time, Time.now
      assert_equal expected_time, Time.new
      assert_equal Date.new(2004, 11, 24), Date.today
      assert_equal expected_time.to_datetime, DateTime.now
    ensure
      travel_back
    end
  end

  def test_time_helper_travel_to_with_nested_calls_with_blocks
    Time.stub(:now, Time.now) do
      outer_expected_time = Time.new(2004, 11, 24, 1, 4, 44)
      inner_expected_time = Time.new(2004, 10, 24, 1, 4, 44)
      travel_to outer_expected_time do
        e = assert_raises(RuntimeError) do
          travel_to(inner_expected_time) do
            # noop
          end
        end
        assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing\./, e.message)
      end
    end
  end

  def test_time_helper_travel_to_with_nested_calls
    Time.stub(:now, Time.now) do
      outer_expected_time = Time.new(2004, 11, 24, 1, 4, 44)
      inner_expected_time = Time.new(2004, 10, 24, 1, 4, 44)
      travel_to outer_expected_time do
        assert_nothing_raised do
          travel_to(inner_expected_time)

          assert_equal inner_expected_time, Time.now
        end
      end
    end
  end

  def test_time_helper_travel_to_with_subsequent_calls
    Time.stub(:now, Time.now) do
      initial_expected_time = Time.new(2004, 11, 24, 1, 4, 44)
      subsequent_expected_time = Time.new(2004, 10, 24, 1, 4, 44)
      assert_nothing_raised do
        travel_to initial_expected_time
        travel_to subsequent_expected_time

        assert_equal subsequent_expected_time, Time.now

        travel_back
      end
    ensure
      travel_back
    end
  end

  def test_time_helper_travel_to_with_usec
    Time.stub(:now, Time.now) do
      duration_usec = 0.1.seconds
      traveled_time = Time.new(2004, 11, 24, 1, 4, 44) + duration_usec
      expected_time = Time.new(2004, 11, 24, 1, 4, 44)

      assert_nothing_raised do
        travel_to traveled_time

        assert_equal expected_time, Time.now

        travel_back
      end
    ensure
      travel_back
    end
  end

  def test_time_helper_with_usec_true
    Time.stub(:now, Time.now) do
      duration_usec = 0.1.seconds
      expected_time = Time.new(2004, 11, 24, 1, 4, 44) + duration_usec

      assert_nothing_raised do
        travel_to expected_time, with_usec: true

        assert_equal expected_time.to_f, Time.now.to_f

        travel 0.5, with_usec: true

        assert_equal((expected_time + 0.5).to_f, Time.now.to_f)

        travel_back
      end
    ensure
      travel_back
    end
  end

  def test_time_helper_freeze_time_with_usec_true
    # repeatedly test in case Time.now happened to actually be 0 usec
    checks = 9.times.map do
      freeze_time(with_usec: true) do
        Time.now.usec != 0
      end
    end
    assert_predicate checks, :any?
  end

  def test_time_helper_travel_with_subsequent_block
    Time.stub(:now, Time.now) do
      outer_expected_time = Time.new(2004, 11, 24, 1, 4, 44)
      inner_expected_time = Time.new(2004, 10, 24, 1, 4, 44)
      travel_to outer_expected_time

      assert_equal outer_expected_time, Time.now

      assert_nothing_raised do
        travel_to(inner_expected_time) do
          assert_equal inner_expected_time, Time.now
        end
      end

      assert_equal outer_expected_time, Time.now
    ensure
      travel_back
    end
  end

  def test_travel_to_will_reset_the_usec_to_avoid_mysql_rounding
    Time.stub(:now, Time.now) do
      travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do
        assert_equal 50, Time.now.sec
        assert_equal 0, Time.now.usec
        assert_equal 50, DateTime.now.sec
        assert_equal 0, DateTime.now.usec
      end
    end
  end

  def test_time_helper_travel_with_time_subclass
    assert_equal TimeSubclass, TimeSubclass.now.class
    assert_equal DateSubclass, DateSubclass.today.class
    assert_equal DateTimeSubclass, DateTimeSubclass.now.class

    travel 1.day do
      assert_equal TimeSubclass, TimeSubclass.now.class
      assert_equal DateSubclass, DateSubclass.today.class
      assert_equal DateTimeSubclass, DateTimeSubclass.now.class
      assert_equal Time.now.to_s, TimeSubclass.now.to_s
      assert_equal Date.today.to_s, DateSubclass.today.to_s
      assert_equal DateTime.now.to_s, DateTimeSubclass.now.to_s
    end
  end

  def test_time_helper_freeze_time
    expected_time = Time.now
    freeze_time
    sleep(1)

    assert_equal expected_time.to_fs(:db), Time.now.to_fs(:db)
  ensure
    travel_back
  end

  def test_time_helper_freeze_time_with_block
    expected_time = Time.now

    freeze_time do
      sleep(1)

      assert_equal expected_time.to_fs(:db), Time.now.to_fs(:db)
    end

    assert_operator expected_time.to_fs(:db), :<, Time.now.to_fs(:db)
  end

  def test_time_helper_unfreeze_time
    assert_equal method(:travel_back), method(:unfreeze_time)
  end
end
