require "../spec_helper"
require "../../support/time"
require "spec/helpers/iterate"

CALENDAR_WEEK_TEST_DATA = [
  { {1981, 1, 1}, {1981, 1, 4} },
  { {1982, 1, 1}, {1981, 53, 5} },
  { {1983, 1, 1}, {1982, 52, 6} },
  { {1984, 1, 1}, {1983, 52, 7} },
  { {1985, 1, 1}, {1985, 1, 2} },
  { {1985, 4, 12}, {1985, 15, 5} },
  { {1986, 1, 1}, {1986, 1, 3} },
  { {1987, 1, 1}, {1987, 1, 4} },
  { {1988, 1, 1}, {1987, 53, 5} },
  { {1989, 1, 1}, {1988, 52, 7} },
  { {1990, 1, 1}, {1990, 1, 1} },
  { {1991, 1, 1}, {1991, 1, 2} },
  { {1992, 1, 1}, {1992, 1, 3} },
  { {1993, 1, 1}, {1992, 53, 5} },
  { {1994, 1, 1}, {1993, 52, 6} },
  { {1995, 1, 2}, {1995, 1, 1} },
  { {1996, 1, 1}, {1996, 1, 1} },
  { {1996, 1, 7}, {1996, 1, 7} },
  { {1996, 1, 8}, {1996, 2, 1} },
  { {1997, 1, 1}, {1997, 1, 3} },
  { {1998, 1, 1}, {1998, 1, 4} },
  { {1999, 1, 1}, {1998, 53, 5} },
  { {2000, 1, 1}, {1999, 52, 6} },
  { {2001, 1, 1}, {2001, 1, 1} },
  { {2002, 1, 1}, {2002, 1, 2} },
  { {2003, 1, 1}, {2003, 1, 3} },
  { {2004, 1, 1}, {2004, 1, 4} },
  { {2005, 1, 1}, {2004, 53, 6} },
  { {2006, 1, 1}, {2005, 52, 7} },
  { {2007, 1, 1}, {2007, 1, 1} },
  { {2008, 1, 1}, {2008, 1, 2} },
  { {2009, 1, 1}, {2009, 1, 4} },
  { {2010, 1, 1}, {2009, 53, 5} },
  { {2010, 1, 1}, {2009, 53, 5} },
  { {2011, 1, 1}, {2010, 52, 6} },
  { {2011, 1, 2}, {2010, 52, 7} },
  { {2011, 1, 3}, {2011, 1, 1} },
  { {2011, 1, 4}, {2011, 1, 2} },
  { {2011, 1, 5}, {2011, 1, 3} },
  { {2011, 1, 6}, {2011, 1, 4} },
  { {2011, 1, 7}, {2011, 1, 5} },
  { {2011, 1, 8}, {2011, 1, 6} },
  { {2011, 1, 9}, {2011, 1, 7} },
  { {2011, 1, 10}, {2011, 2, 1} },
  { {2011, 1, 11}, {2011, 2, 2} },
  { {2011, 6, 12}, {2011, 23, 7} },
  { {2011, 6, 13}, {2011, 24, 1} },
  { {2011, 12, 25}, {2011, 51, 7} },
  { {2011, 12, 26}, {2011, 52, 1} },
  { {2011, 12, 27}, {2011, 52, 2} },
  { {2011, 12, 28}, {2011, 52, 3} },
  { {2011, 12, 29}, {2011, 52, 4} },
  { {2011, 12, 30}, {2011, 52, 5} },
  { {2011, 12, 31}, {2011, 52, 6} },
  { {1995, 1, 1}, {1994, 52, 7} },
  { {2012, 1, 1}, {2011, 52, 7} },
  { {2012, 1, 2}, {2012, 1, 1} },
  { {2012, 1, 8}, {2012, 1, 7} },
  { {2012, 1, 9}, {2012, 2, 1} },
  { {2012, 12, 23}, {2012, 51, 7} },
  { {2012, 12, 24}, {2012, 52, 1} },
  { {2012, 12, 30}, {2012, 52, 7} },
  { {2012, 12, 31}, {2013, 1, 1} },
  { {2013, 1, 1}, {2013, 1, 2} },
  { {2013, 1, 6}, {2013, 1, 7} },
  { {2013, 1, 7}, {2013, 2, 1} },
  { {2013, 12, 22}, {2013, 51, 7} },
  { {2013, 12, 23}, {2013, 52, 1} },
  { {2013, 12, 29}, {2013, 52, 7} },
  { {2013, 12, 30}, {2014, 1, 1} },
  { {2014, 1, 1}, {2014, 1, 3} },
  { {2014, 1, 5}, {2014, 1, 7} },
  { {2014, 1, 6}, {2014, 2, 1} },
  { {2015, 1, 1}, {2015, 1, 4} },
  { {2016, 1, 1}, {2015, 53, 5} },
  { {2017, 1, 1}, {2016, 52, 7} },
  { {2018, 1, 1}, {2018, 1, 1} },
  { {2019, 1, 1}, {2019, 1, 2} },
  { {2020, 1, 1}, {2020, 1, 3} },
  { {2021, 1, 1}, {2020, 53, 5} },
  { {2022, 1, 1}, {2021, 52, 6} },
  { {2023, 1, 1}, {2022, 52, 7} },
  { {2024, 1, 1}, {2024, 1, 1} },
  { {2025, 1, 1}, {2025, 1, 3} },
  { {2026, 1, 1}, {2026, 1, 4} },
  { {2027, 1, 1}, {2026, 53, 5} },
  { {2028, 1, 1}, {2027, 52, 6} },
  { {2029, 1, 1}, {2029, 1, 1} },
  { {2030, 1, 1}, {2030, 1, 2} },
  { {2031, 1, 1}, {2031, 1, 3} },
  { {2032, 1, 1}, {2032, 1, 4} },
  { {2033, 1, 1}, {2032, 53, 6} },
  { {2034, 1, 1}, {2033, 52, 7} },
  { {2035, 1, 1}, {2035, 1, 1} },
  { {2036, 1, 1}, {2036, 1, 2} },
  { {2037, 1, 1}, {2037, 1, 4} },
  { {2038, 1, 1}, {2037, 53, 5} },
  { {2039, 1, 1}, {2038, 52, 6} },
  { {2040, 1, 1}, {2039, 52, 7} },
]

describe Time do
  describe ".local" do
    it "initializes" do
      t1 = Time.local 2002, 2, 25
      t1.date.should eq({2002, 2, 25})
      t1.year.should eq(2002)
      t1.month.should eq(2)
      t1.day.should eq(25)
      t1.hour.should eq(0)
      t1.minute.should eq(0)
      t1.second.should eq(0)
      t1.local?.should be_true

      t2 = Time.local 2002, 2, 25, 15, 25, 13, nanosecond: 8
      t2.date.should eq({2002, 2, 25})
      t2.year.should eq(2002)
      t2.month.should eq(2)
      t2.day.should eq(25)
      t2.hour.should eq(15)
      t2.minute.should eq(25)
      t2.second.should eq(13)
      t2.nanosecond.should eq(8)
      t2.local?.should be_true
    end

    it "initializes max value" do
      time = Time.local(9999, 12, 31, 23, 59, 59, nanosecond: 999_999_999)
      time.year.should eq(9999)
      time.month.should eq(12)
      time.day.should eq(31)
      time.hour.should eq(23)
      time.minute.should eq(59)
      time.second.should eq(59)
      time.nanosecond.should eq(999_999_999)

      time = Time.local(9999, 12, 31, 23, 59, 59, nanosecond: 999_999_999, location: Time::Location.posix_tz("Local", "EST5EDT,M3.2.0,M11.1.0"))
      time.year.should eq(9999)
      time.month.should eq(12)
      time.day.should eq(31)
      time.hour.should eq(23)
      time.minute.should eq(59)
      time.second.should eq(59)
      time.nanosecond.should eq(999_999_999)
    end

    it "fails with negative nanosecond" do
      expect_raises ArgumentError, "Invalid time" do
        Time.local(9999, 12, 31, 23, 59, 59, nanosecond: -1)
      end
    end

    it "fails with too big nanoseconds" do
      expect_raises ArgumentError, "Invalid time" do
        Time.local(9999, 12, 31, 23, 59, 59, nanosecond: 1_000_000_000)
      end
    end

    it "checks boundary at time min" do
      {-5 * 3600, -1, 0, 1, 5 * 3600}.each do |offset|
        seconds = -offset.to_i64
        location = Time::Location.fixed(offset)
        Time.new(seconds: seconds + 1, nanoseconds: 0, location: location)
        Time.new(seconds: seconds, nanoseconds: 0, location: location)
        expect_raises ArgumentError, "Invalid time" do
          Time.new(seconds: seconds - 1, nanoseconds: 0, location: location)
        end
      end
    end

    it "checks boundary at time max" do
      {-5 * 3600, -1, 0, 1, 5 * 3600}.each do |offset|
        seconds = Time::MAX_SECONDS - offset.to_i64
        location = Time::Location.fixed(offset)
        Time.new(seconds: seconds - 1, nanoseconds: 0, location: location)
        Time.new(seconds: seconds, nanoseconds: 0, location: location)
        expect_raises ArgumentError, "Invalid time" do
          Time.new(seconds: seconds + 1, nanoseconds: 0, location: location)
        end
      end
    end

    it "accepts midnight 24:00" do
      Time.utc(2020, 5, 21, 24, 0, 0).should eq Time.utc(2020, 5, 22, 0, 0, 0)

      expect_raises ArgumentError, "Invalid time" do
        Time.utc(2020, 5, 21, 24, 0, 0, nanosecond: 1)
      end

      expect_raises ArgumentError, "Invalid time" do
        Time.utc(2020, 5, 21, 24, 0, 1)
      end

      expect_raises ArgumentError, "Invalid time" do
        Time.utc(2020, 5, 21, 24, 1, 0)
      end
    end
  end

  it "UNIX_EPOCH" do
    Time::UNIX_EPOCH.should eq(Time.utc(1970, 1, 1))
  end

  it ".unix" do
    seconds = 1439404155
    time = Time.unix(seconds)
    time.should eq(Time.utc(2015, 8, 12, 18, 29, 15))
    time.to_unix.should eq(seconds)
    time.utc?.should be_true
  end

  it ".unix_ms" do
    milliseconds = 1439404155000
    time = Time.unix_ms(milliseconds)
    time.should eq(Time.utc(2015, 8, 12, 18, 29, 15))
    time.to_unix_ms.should eq(milliseconds)
    time.utc?.should be_true
  end

  describe ".unix_ns" do
    it "supports Int64 values" do
      nanoseconds = 1439404155001457425i64
      time = Time.unix_ns(nanoseconds)
      time.should eq(Time.utc(2015, 8, 12, 18, 29, 15, nanosecond: 1457425))
      time.to_unix_ns.should eq(nanoseconds)
      time.utc?.should be_true
    end

    it "supports maximum valid time" do
      nanoseconds = Int128.new("253402300799999999999")
      time = Time.unix_ns(nanoseconds)
      time.should eq(Time.utc(9999, 12, 31, 23, 59, 59, nanosecond: 999999999))
      time.to_unix_ns.should eq(nanoseconds)
      time.utc?.should be_true
    end

    it "supports minimum valid time" do
      nanoseconds = Int128.new("-62135596800000000000")
      time = Time.unix_ns(nanoseconds)
      time.should eq(Time.utc(1, 1, 1, 0, 0, 0, nanosecond: 0))
      time.to_unix_ns.should eq(nanoseconds)
      time.utc?.should be_true
    end
  end

  describe ".local without arguments" do
    it "current time is similar in different locations" do
      (Time.local - Time.utc).should be_close(0.seconds, 1.second)
      (Time.local - Time.local(Time::Location.fixed(1234))).should be_close(0.seconds, 1.second)
    end
  end

  describe ".monotonic" do
    it "returns always increasing monotonic clock" do
      clock = Time.monotonic
      Time.monotonic.should be >= clock
    end
  end

  describe ".measure" do
    it "measures elapsed time" do
      # NOTE: On some systems, the sleep may not always wait for 1ms and the fiber
      #       be resumed early. We thus merely test that the method returns a
      #       positive time span.
      elapsed = Time.measure { sleep 1.millisecond }
      elapsed.should be >= 0.seconds
    end
  end

  it "#clone" do
    time = Time.local
    time.clone.should eq(time)
  end

  describe "#shift" do
    it "adds hours, minutes, seconds" do
      t1 = Time.utc(2002, 2, 25, 15, 25, 13)
      t2 = t1 + Time::Span.new(hours: 3, minutes: 54, seconds: 1)

      t2.should eq Time.utc(2002, 2, 25, 19, 19, 14)
    end

    it "raises out of range min" do
      t1 = Time.utc(9980, 2, 25, 15, 25, 13)

      expect_raises ArgumentError do
        t1 + Time::Span.new(nanoseconds: Int64::MAX)
      end
    end

    it "raises out of range max" do
      t1 = Time.utc(1, 2, 25, 15, 25, 13)

      expect_raises ArgumentError do
        t1 + Time::Span.new(nanoseconds: Int64::MIN)
      end
    end

    it "checks boundary at time min" do
      {5 * 3600, 1, 0, -1, -5 * 3600}.each do |offset|
        location = Time::Location.fixed(offset)

        time = Time.local(1, 1, 1, location: location)
        time.shift(0, 1).should eq Time.local(1, 1, 1, nanosecond: 1, location: location)
        time.shift(0, 0).should eq time
        expect_raises(ArgumentError) do
          time.shift(0, -1)
        end
      end
    end

    it "checks boundary at time max" do
      {5 * 3600, 1, 0, -1, -5 * 3600}.each do |offset|
        location = Time::Location.fixed(offset)

        time = Time.local(9999, 12, 31, 23, 59, 59, nanosecond: 999_999_999, location: location)
        time.shift(0, -1).should eq Time.local(9999, 12, 31, 23, 59, 59, nanosecond: 999_999_998, location: location)
        time.shift(0, 0).should eq time
        expect_raises(ArgumentError) do
          time.shift(0, 1)
        end
      end
    end

    it "adds zero span" do
      time = Time.utc
      time.shift(0, 0).should eq time
    end

    describe "irregular calendaric unit ratios" do
      it "shifts by a week if one day is left out" do
        # The week from 2011-12-25 to 2012-01-01 for example lasted only 6 days in Samoa,
        # because it skipped 2011-12-28 due to changing time zone from -11:00 to +13:00.
        with_zoneinfo do
          samoa = Time::Location.load("Pacific/Apia")
          start = Time.local(2011, 12, 25, 0, 0, 0, location: samoa)

          plus_one_week = start.shift days: 7
          plus_one_week.should eq start + 6.days

          plus_one_year = start.shift years: 1
          plus_one_year.should eq start + 365.days # 2012 is a leap year so it should've been 366 days, but 2011-12-28 was skipped
        end
      end

      it "shifts by conceptual hour even if elapsed time is less" do
        # Venezuela switched from -4:30 to -4:00 on 2016-05-01, the hour between 2:00 and 3:00 lasted only 30 minutes
        with_zoneinfo do
          venezuela = Time::Location.load("America/Caracas")
          start = Time.local(2016, 5, 1, 2, 0, 0, location: venezuela)
          plus_one_hour = start.shift hours: 1
          plus_one_hour.should eq start + 30.minutes
        end
      end
    end

    describe "adds days" do
      it "simple" do
        time = Time.utc(2002, 2, 25, 15, 25, 13)
        time = time + 3.days

        time.should eq Time.utc(2002, 2, 28, 15, 25, 13)

        time = time + 1.9.days
        time.should eq Time.utc(2002, 3, 2, 13, 1, 13)

        time = time + 0.2.days
        time.should eq Time.utc(2002, 3, 2, 17, 49, 13)
      end

      it "over dst" do
        with_zoneinfo do
          location = Time::Location.load("Europe/Berlin")
          reference = Time.local(2017, 10, 28, 13, 37, location: location)
          next_day = reference.shift days: 1

          next_day.should eq reference + 25.hours
        end
      end

      it "out of range max" do
        time = Time.utc(2002, 2, 25, 15, 25, 13)
        expect_raises ArgumentError do
          time + 10000000.days
        end
      end

      it "out of range max (shift days)" do
        time = Time.utc(2002, 2, 25, 15, 25, 13)
        expect_raises OverflowError do
          time.shift days: 10000000
        end
      end

      it "out of range min" do
        time = Time.utc(2002, 2, 25, 15, 25, 13)
        expect_raises ArgumentError do
          time - 10000000.days
        end
      end

      it "out of range min (shift days)" do
        time = Time.utc(2002, 2, 25, 15, 25, 13)
        expect_raises OverflowError do
          time.shift days: -10000000
        end
      end
    end

    it "adds months" do
      t = Time.utc 2014, 10, 30, 21, 18, 13

      t2 = t.shift months: 1
      t2.should eq Time.utc(2014, 11, 30, 21, 18, 13)

      t2 = t.shift months: -1
      t2.should eq Time.utc(2014, 9, 30, 21, 18, 13)

      t = Time.utc 2014, 10, 31, 21, 18, 13
      t2 = t.shift months: 1
      t2.should eq Time.utc(2014, 11, 30, 21, 18, 13)

      t = Time.utc 2014, 10, 31, 21, 18, 13
      t2 = t.shift months: -1
      t2.should eq Time.utc(2014, 9, 30, 21, 18, 13)

      t = Time.utc 2014, 10, 31, 21, 18, 13
      t2 = t.shift months: 6
      t2.should eq Time.utc(2015, 4, 30, 21, 18, 13)
    end

    it "adds years" do
      t = Time.utc 2014, 10, 30, 21, 18, 13
      t2 = t.shift years: 1
      t2.should eq Time.utc(2015, 10, 30, 21, 18, 13)

      t = Time.utc 2014, 10, 30, 21, 18, 13
      t2 = t.shift years: -2
      t2.should eq Time.utc(2012, 10, 30, 21, 18, 13)
    end

    it "adds hours" do
      time = Time.utc(2002, 2, 25, 15, 25, 13)

      time = time + 10.hours
      time.should eq Time.utc(2002, 2, 26, 1, 25, 13)

      time = time - 3.7.hours
      time.should eq Time.utc(2002, 2, 25, 21, 43, 13)

      time = time + 3.732.hours
      time.should eq Time.utc(2002, 2, 26, 1, 27, 8, nanosecond: 200_000_000)
    end

    it "adds nanoseconds" do
      t1 = Time.utc(2002, 2, 25, 15, 25, 13)
      t1 = t1.shift nanoseconds: 10_000_000_000_000_000

      t1.should eq Time.utc(2002, 6, 21, 9, 11, 53)

      t1 = t1.shift nanoseconds: -190_000_000_000_000_000
      t1.should eq Time.utc(1996, 6, 13, 7, 25, 13)

      t1 = t1.shift nanoseconds: 15_623_000
      t1.should eq Time.utc(1996, 6, 13, 7, 25, 13, nanosecond: 15_623_000)
    end

    it "preserves location when adding" do
      time = Time.utc
      time.utc?.should be_true

      (time + 5.minutes).utc?.should be_true

      location = Time::Location.fixed(1234)
      time = Time.local(location)
      (time + 5.minutes).location.should eq location
    end

    it "covers date boundaries with zone offset (#8741)" do
      zone = Time::Location.fixed(7 * 3600)

      Time.local(2020, 2, 5, 0, 13, location: zone).shift(months: 3).should eq Time.local(2020, 5, 5, 0, 13, location: zone)
    end

    it "covers date boundaries with zone offset (#10869)" do
      location = Time::Location.fixed(2 * 3600)
      Time.local(2021, 7, 1, location: location).shift(months: 1).should eq Time.local(2021, 8, 1, location: location)
    end
  end

  it "#time_of_day" do
    t = Time.utc 2014, 10, 30, 21, 18, 13
    t.time_of_day.should eq(Time::Span.new(hours: 21, minutes: 18, seconds: 13))
  end

  describe "#day_of_week" do
    it "gets day of week" do
      t = Time.utc 2014, 10, 30, 21, 18, 13
      t.day_of_week.should eq(Time::DayOfWeek::Thursday)
    end

    CALENDAR_WEEK_TEST_DATA.each do |date, week_date|
      it "#{date.join('-')} is #{week_date[2]}" do
        Time.utc(*date).day_of_week.should eq Time::DayOfWeek.from_value(week_date[2])
      end
    end
  end

  it "answers day name predicates" do
    7.times do |i|
      time = Time.utc(2015, 2, 15 + i)
      time.sunday?.should eq(i == 0)
      time.monday?.should eq(i == 1)
      time.tuesday?.should eq(i == 2)
      time.wednesday?.should eq(i == 3)
      time.thursday?.should eq(i == 4)
      time.friday?.should eq(i == 5)
      time.saturday?.should eq(i == 6)
    end
  end

  describe "#calendar_week" do
    CALENDAR_WEEK_TEST_DATA.each do |date, week_date|
      it "#{date.join('-')} to #{week_date[0]}-#{week_date[1]}" do
        Time.utc(*date).calendar_week.should eq({week_date[0], week_date[1]})
      end
    end
  end

  it "#day_of_year" do
    t = Time.utc 2014, 10, 30, 21, 18, 13
    t.day_of_year.should eq(303)
  end

  describe "#<=>" do
    it "compares" do
      t1 = Time.utc 2014, 10, 30, 21, 18, 13
      t2 = Time.utc 2014, 10, 30, 21, 18, 14

      (t1 <=> t2).should eq(-1)
      (t1 == t2).should be_false
      (t1 < t2).should be_true
    end

    it "compares different locations" do
      time = Time.local(Time::Location.fixed(1234))
      (time.to_utc <=> time).should eq(0)
    end
  end

  describe "#step" do
    days = (1..24).map { |d| Time.utc(2020, 12, d) }.to_a
    it_iterates "advent", days, Time.utc(2020, 12, 1).step(to: Time.utc(2020, 12, 24), by: 1.day)
  end

  describe "#to_unix" do
    it "gets unix seconds" do
      t1 = Time.utc 2014, 10, 30, 21, 18, 13, nanosecond: 0
      t1.to_unix.should eq(1414703893)
      t1.to_unix_f.should be_close(1414703893, 1e-01)
    end

    it "gets unix seconds at GMT" do
      t1 = Time.local(Time::Location.fixed(1234))
      t1.to_unix.should eq(t1.to_utc.to_unix)
      t1.to_unix_f.should be_close(t1.to_utc.to_unix_f, 1e-01)
    end
  end

  it "#year" do
    Time.utc(2008, 12, 31).year.should eq 2008
    Time.utc(2000, 12, 31).year.should eq 2000
    Time.utc(1900, 12, 31).year.should eq 1900
    Time.utc(1800, 12, 31).year.should eq 1800
    Time.utc(1700, 12, 31).year.should eq 1700
    Time.utc(1600, 12, 31).year.should eq 1600
    Time.utc(400, 12, 31).year.should eq 400
    Time.utc(100, 12, 31).year.should eq 100
    Time.utc(4, 12, 31).year.should eq 4
    Time.utc(1, 1, 1).year.should eq 1
  end

  describe "#to_s" do
    it "prints string" do
      with_zoneinfo do
        time = Time.local(2017, 11, 25, 22, 6, 17, location: Time::Location::UTC)
        time.to_s.should eq "2017-11-25 22:06:17 UTC"

        time = Time.local(2017, 11, 25, 22, 6, 17, location: Time::Location.fixed(-7200))
        time.to_s.should eq "2017-11-25 22:06:17 -02:00"

        time = Time.local(2017, 11, 25, 22, 6, 17, location: Time::Location.fixed(-7259))
        time.to_s.should eq "2017-11-25 22:06:17 -02:00:59"

        location = Time::Location.load("Europe/Berlin")
        time = Time.local(2017, 11, 25, 22, 6, 17, location: location)
        time.to_s.should eq "2017-11-25 22:06:17 +01:00"
      end
    end

    it "prints date-time fields" do
      Time.utc(2014, 1, 30, 21, 18, 13).to_s.should eq("2014-01-30 21:18:13 UTC")
      Time.utc(2014, 10, 1, 21, 18, 13).to_s.should eq("2014-10-01 21:18:13 UTC")
      Time.utc(2014, 10, 30, 1, 18, 13).to_s.should eq("2014-10-30 01:18:13 UTC")
      Time.utc(2014, 10, 30, 21, 1, 13).to_s.should eq("2014-10-30 21:01:13 UTC")
      Time.utc(2014, 10, 30, 21, 18, 1).to_s.should eq("2014-10-30 21:18:01 UTC")
    end

    it "omits nanoseconds" do
      Time.utc(2014, 10, 30, 21, 18, 13).to_s.should eq("2014-10-30 21:18:13 UTC")
      Time.utc(2014, 10, 30, 21, 18, 13, nanosecond: 12345).to_s.should eq("2014-10-30 21:18:13 UTC")
    end

    it "prints offset for location" do
      with_zoneinfo do
        location = Time::Location.load("Europe/Berlin")
        Time.local(2014, 10, 30, 21, 18, 13, location: location).to_s.should eq("2014-10-30 21:18:13 +01:00")
        Time.local(2014, 10, 30, 21, 18, 13, nanosecond: 123_456, location: location).to_s.should eq("2014-10-30 21:18:13 +01:00")

        Time.local(2014, 10, 10, 21, 18, 13, location: location).to_s.should eq("2014-10-10 21:18:13 +02:00")
        Time.local(2014, 10, 10, 21, 18, 13, nanosecond: 123_456, location: location).to_s.should eq("2014-10-10 21:18:13 +02:00")
      end
    end

    it "prints offset for fixed location" do
      location = Time::Location.fixed(3601)
      Time.local(2014, 1, 2, 3, 4, 5, location: location).to_s.should eq "2014-01-02 03:04:05 +01:00:01"
      Time.local(2014, 1, 2, 3, 4, 5, nanosecond: 123_456_789, location: location).to_s.should eq "2014-01-02 03:04:05 +01:00:01"

      t = Time.local 2014, 10, 30, 21, 18, 13, location: Time::Location.fixed(-9000)
      t.to_s.should eq("2014-10-30 21:18:13 -02:30")
    end

    it "prints local time" do
      # Simulates loading non-fixed offset local time from /etc/localtime
      old_local = Time::Location.local
      begin
        location = Time::Location.new "Local", [Time::Location::Zone.new("STZ", 3600, false), Time::Location::Zone.new("DTZ", -3600, false)], [] of Time::Location::ZoneTransition
        Time::Location.local = location

        Time.local(2014, 10, 30, 21, 18, 13).to_s.should eq("2014-10-30 21:18:13 +01:00")
      ensure
        Time::Location.local = old_local
      end
    end
  end

  it "#inspect" do
    Time.utc(2014, 1, 2, 3, 4, 5).inspect.should eq "2014-01-02 03:04:05Z"
    Time.utc(2014, 1, 2, 3, 4, 5, nanosecond: 123_456_789).inspect.should eq "2014-01-02 03:04:05.123456789Z"

    with_zoneinfo do
      location = Time::Location.load("Europe/Berlin")
      Time.local(2014, 1, 2, 3, 4, 5, location: location).inspect.should eq "2014-01-02 03:04:05+01:00[Europe/Berlin]"
      Time.local(2014, 1, 2, 3, 4, 5, nanosecond: 123_456_789, location: location).inspect.should eq "2014-01-02 03:04:05.123456789+01:00[Europe/Berlin]"
    end

    location = Time::Location.fixed(3601)
    Time.local(2014, 1, 2, 3, 4, 5, location: location).inspect.should eq "2014-01-02 03:04:05+01:00:01"
    Time.local(2014, 1, 2, 3, 4, 5, nanosecond: 123_456_789, location: location).inspect.should eq "2014-01-02 03:04:05.123456789+01:00:01"
  end

  it "at methods" do
    t1 = Time.utc 2014, 11, 25, 10, 11, 12, nanosecond: 13
    t2 = Time.utc 2014, 6, 25, 10, 11, 12, nanosecond: 13

    t1.at_beginning_of_year.should eq Time.utc(2014, 1, 1)

    1.upto(3) do |i|
      Time.utc(2014, i, 10).at_beginning_of_quarter.should eq Time.utc(2014, 1, 1)
      Time.utc(2014, i, 10).at_end_of_quarter.should eq Time.utc(2014, 3, 31, 23, 59, 59, nanosecond: 999_999_999)
    end
    4.upto(6) do |i|
      Time.utc(2014, i, 10).at_beginning_of_quarter.should eq Time.utc(2014, 4, 1)
      Time.utc(2014, i, 10).at_end_of_quarter.should eq Time.utc(2014, 6, 30, 23, 59, 59, nanosecond: 999_999_999)
    end
    7.upto(9) do |i|
      Time.utc(2014, i, 10).at_beginning_of_quarter.should eq Time.utc(2014, 7, 1)
      Time.utc(2014, i, 10).at_end_of_quarter.should eq Time.utc(2014, 9, 30, 23, 59, 59, nanosecond: 999_999_999)
    end
    10.upto(12) do |i|
      Time.utc(2014, i, 10).at_beginning_of_quarter.should eq Time.utc(2014, 10, 1)
      Time.utc(2014, i, 10).at_end_of_quarter.should eq Time.utc(2014, 12, 31, 23, 59, 59, nanosecond: 999_999_999)
    end

    t1.at_beginning_of_quarter.should eq Time.utc(2014, 10, 1)
    t1.at_beginning_of_month.should eq Time.utc(2014, 11, 1)

    3.upto(9) do |i|
      Time.utc(2014, 11, i).at_beginning_of_week.should eq Time.utc(2014, 11, 3)
    end

    sunday_day_of_week = Time::DayOfWeek::Sunday
    Time.utc(2014, 11, 1).at_beginning_of_week(sunday_day_of_week).should eq Time.utc(2014, 10, 26)
    2.upto(8) do |i|
      Time.utc(2014, 11, i).at_beginning_of_week(sunday_day_of_week).should eq Time.utc(2014, 11, 2)
    end
    Time.utc(2014, 11, 9).at_beginning_of_week(sunday_day_of_week).should eq Time.utc(2014, 11, 9)

    Time.utc(2014, 11, 1).at_beginning_of_week(:sunday).should eq Time.utc(2014, 10, 26)
    2.upto(8) do |i|
      Time.utc(2014, 11, i).at_beginning_of_week(:sunday).should eq Time.utc(2014, 11, 2)
    end
    Time.utc(2014, 11, 9).at_beginning_of_week(:sunday).should eq Time.utc(2014, 11, 9)

    Time.utc(2014, 11, 10).at_beginning_of_week(Time::DayOfWeek::Sunday).should eq Time.utc(2014, 11, 9)
    Time.utc(2014, 11, 10).at_beginning_of_week(Time::DayOfWeek::Monday).should eq Time.utc(2014, 11, 10)
    Time.utc(2014, 11, 10).at_beginning_of_week(Time::DayOfWeek::Tuesday).should eq Time.utc(2014, 11, 4)
    Time.utc(2014, 11, 10).at_beginning_of_week(Time::DayOfWeek::Wednesday).should eq Time.utc(2014, 11, 5)
    Time.utc(2014, 11, 10).at_beginning_of_week(Time::DayOfWeek::Thursday).should eq Time.utc(2014, 11, 6)
    Time.utc(2014, 11, 10).at_beginning_of_week(Time::DayOfWeek::Friday).should eq Time.utc(2014, 11, 7)
    Time.utc(2014, 11, 10).at_beginning_of_week(Time::DayOfWeek::Saturday).should eq Time.utc(2014, 11, 8)

    at_beginning_of_week_default = Time.local.at_beginning_of_week
    at_beginning_of_week_default.hour.should eq 0
    at_beginning_of_week_default.minute.should eq 0
    at_beginning_of_week_default.second.should eq 0

    at_beginning_of_week_sunday = Time.local.at_beginning_of_week(:sunday)
    at_beginning_of_week_sunday.hour.should eq 0
    at_beginning_of_week_sunday.minute.should eq 0
    at_beginning_of_week_sunday.second.should eq 0

    with_zoneinfo do
      # Observes time zone (https://github.com/crystal-lang/crystal/issues/16112)
      is = Time::Location.load("Asia/Jerusalem")
      Time.local(2025, 3, 27, 23, 34, location: is).at_end_of_week
        .should eq Time.local(2025, 3, 30, 23, 59, 59, nanosecond: 999_999_999, location: is)

      ny = Time::Location.load("America/New_York")
      Time.local(2024, 11, 3, 23, 34, 8, location: ny).at_beginning_of_week
        .should eq Time.local(2024, 10, 28, 0, location: ny)
    end

    t1.at_beginning_of_day.should eq Time.utc(2014, 11, 25)
    t1.at_beginning_of_hour.should eq Time.utc(2014, 11, 25, 10)
    t1.at_beginning_of_minute.should eq Time.utc(2014, 11, 25, 10, 11)
    t1.at_beginning_of_second.should eq Time.utc(2014, 11, 25, 10, 11, 12)

    t1.at_end_of_year.should eq Time.utc(2014, 12, 31, 23, 59, 59, nanosecond: 999_999_999)

    t1.at_end_of_quarter.should eq Time.utc(2014, 12, 31, 23, 59, 59, nanosecond: 999_999_999)
    t2.at_end_of_quarter.should eq Time.utc(2014, 6, 30, 23, 59, 59, nanosecond: 999_999_999)

    t1.at_end_of_month.should eq Time.utc(2014, 11, 30, 23, 59, 59, nanosecond: 999_999_999)
    t1.at_end_of_week.should eq Time.utc(2014, 11, 30, 23, 59, 59, nanosecond: 999_999_999)

    Time.utc(2014, 11, 2).at_end_of_week.should eq Time.utc(2014, 11, 2, 23, 59, 59, nanosecond: 999_999_999)
    3.upto(9) do |i|
      Time.utc(2014, 11, i).at_end_of_week.should eq Time.utc(2014, 11, 9, 23, 59, 59, nanosecond: 999_999_999)
    end

    t1.at_end_of_day.should eq Time.utc(2014, 11, 25, 23, 59, 59, nanosecond: 999_999_999)
    t1.at_end_of_hour.should eq Time.utc(2014, 11, 25, 10, 59, 59, nanosecond: 999_999_999)
    t1.at_end_of_minute.should eq Time.utc(2014, 11, 25, 10, 11, 59, nanosecond: 999_999_999)
    t1.at_end_of_second.should eq Time.utc(2014, 11, 25, 10, 11, 12, nanosecond: 999_999_999)

    t1.at_midday.should eq Time.utc(2014, 11, 25, 12)

    t1.at_beginning_of_semester.should eq Time.utc(2014, 7, 1)
    t2.at_beginning_of_semester.should eq Time.utc(2014, 1, 1)

    t1.at_end_of_semester.should eq Time.utc(2014, 12, 31, 23, 59, 59, nanosecond: 999_999_999)
    t2.at_end_of_semester.should eq Time.utc(2014, 6, 30, 23, 59, 59, nanosecond: 999_999_999)
  end

  it "does diff of utc vs local time" do
    local = Time.local(Time::Location.fixed(1234))
    utc = local.to_utc
    (utc - local).should eq(0.seconds)
    (local - utc).should eq(0.seconds)
  end

  describe "#in" do
    it "changes location" do
      location = Time::Location.fixed(3600)
      location2 = Time::Location.fixed(12345)
      time1 = Time.local(location)
      time1.location.should eq(location)

      time2 = time1.in(location2)
      time2.should eq(time1)
      time2.location.should eq(location2)
    end
  end

  describe "#to_local_in" do
    it "keeps wall clock" do
      location = Time::Location.fixed(3600)
      location2 = Time::Location.fixed(12345)
      time1 = Time.local(location)
      time1.location.should eq(location)

      time2 = time1.to_local_in(location2)
      time2.location.should eq(location2)
      time2.year.should eq time1.year
      time2.month.should eq time1.month
      time2.day.should eq time1.day
      time2.hour.should eq time1.hour
      time2.minute.should eq time1.minute
      time2.second.should eq time1.second
      time2.nanosecond.should eq time1.nanosecond
    end

    it "is the difference of offsets apart" do
      location = Time::Location.fixed(3600)
      location2 = Time::Location.fixed(12345)
      time1 = Time.local(location)
      time2 = time1.to_local_in(location2)

      (time2 - time1).should eq (time1.offset - time2.offset).seconds
    end
  end

  it "#to_s" do
    with_zoneinfo do
      time = Time.local(2017, 11, 25, 22, 6, 17, location: Time::Location::UTC)
      time.to_s.should eq "2017-11-25 22:06:17 UTC"

      time = Time.local(2017, 11, 25, 22, 6, 17, location: Time::Location.fixed(-7200))
      time.to_s.should eq "2017-11-25 22:06:17 -02:00"

      time = Time.local(2017, 11, 25, 22, 6, 17, location: Time::Location.fixed(-7259))
      time.to_s.should eq "2017-11-25 22:06:17 -02:00:59"

      location = Time::Location.load("Europe/Berlin")
      time = Time.local(2017, 11, 25, 22, 6, 17, location: location)
      time.to_s.should eq "2017-11-25 22:06:17 +01:00"
    end
  end

  describe ".days_in_month" do
    it "returns days for valid month and year" do
      Time.days_in_month(2016, 2).should eq(29)
      Time.days_in_month(1990, 4).should eq(30)
    end

    it "raises exception for invalid month" do
      expect_raises(ArgumentError, "Invalid month") do
        Time.days_in_month(2016, 13)
      end
    end

    it "raises exception for invalid year" do
      expect_raises(ArgumentError, "Invalid year") do
        Time.days_in_month(10000, 11)
      end
    end
  end

  it ".days_in_year" do
    Time.days_in_year(2005).should eq(365)
    Time.days_in_year(2004).should eq(366)
    Time.days_in_year(2000).should eq(366)
    Time.days_in_year(1990).should eq(365)
  end

  describe ".leap_year?" do
    it "knows that 400-year centuries are leap years" do
      {1600, 2000, 2400}.each do |year|
        Time.leap_year?(year).should be_true
      end
    end

    it "knows that 100-year centuries are normal years" do
      {1700, 1800, 1900, 2100, 2200, 2300}.each do |year|
        Time.leap_year?(year).should be_false
      end
    end

    it "knows that typical non-century leap years are divisible by 4" do
      {1968, 1972, 2004, 2020}.each do |year|
        Time.leap_year?(year).should be_true
      end
    end

    it "knows years *not* divisible by 4 are normal" do
      {1965, 1999, 2001, 2018, 2019, 2021, 2099, 2101}.each do |year|
        Time.leap_year?(year).should be_false
      end
    end
  end

  describe Time::DayOfWeek do
    it "#value" do
      Time::DayOfWeek::Monday.value.should eq 1
      Time::DayOfWeek::Tuesday.value.should eq 2
      Time::DayOfWeek::Wednesday.value.should eq 3
      Time::DayOfWeek::Thursday.value.should eq 4
      Time::DayOfWeek::Friday.value.should eq 5
      Time::DayOfWeek::Saturday.value.should eq 6
      Time::DayOfWeek::Sunday.value.should eq 7
    end

    it ".from_value" do
      Time::DayOfWeek.from_value(1).should eq Time::DayOfWeek::Monday
      Time::DayOfWeek.from_value(2).should eq Time::DayOfWeek::Tuesday
      Time::DayOfWeek.from_value(3).should eq Time::DayOfWeek::Wednesday
      Time::DayOfWeek.from_value(4).should eq Time::DayOfWeek::Thursday
      Time::DayOfWeek.from_value(5).should eq Time::DayOfWeek::Friday
      Time::DayOfWeek.from_value(6).should eq Time::DayOfWeek::Saturday
      Time::DayOfWeek.from_value(7).should eq Time::DayOfWeek::Sunday

      # Special case: Identify 0 as Sunday
      Time::DayOfWeek.from_value(0).should eq Time::DayOfWeek::Sunday

      expect_raises(Exception, "Unknown enum Time::DayOfWeek value: 8") do
        Time::DayOfWeek.from_value(8)
      end
    end

    it ".new does not identify 0 as Sunday" do
      Time::DayOfWeek.new(0).should_not eq Time::DayOfWeek::Sunday
    end
  end

  describe ".week_date" do
    describe "verify test data" do
      with_zoneinfo do
        location = Time::Location.load("Europe/Berlin")

        CALENDAR_WEEK_TEST_DATA.each do |date, week_date|
          it "W#{week_date.join('-')} eq #{date.join('-')}" do
            Time.week_date(*week_date, location: Time::Location::UTC).should eq(Time.utc(*date))
            Time.week_date(week_date[0], week_date[1], Time::DayOfWeek.from_value(week_date[2]), location: Time::Location::UTC).should eq(Time.utc(*date))
            Time.week_date(*week_date).should eq(Time.local(*date))
            Time.week_date(*week_date, location: location).should eq(Time.local(*date, location: location))
          end
        end
      end
    end

    it "accepts time arguments" do
      with_zoneinfo do
        location = Time::Location.load("Europe/Berlin")
        Time.week_date(*CALENDAR_WEEK_TEST_DATA[0][1], 11, 57, 32, nanosecond: 123_567, location: location).should eq(
          Time.local(*CALENDAR_WEEK_TEST_DATA[0][0], 11, 57, 32, nanosecond: 123_567, location: location))

        location = Time::Location.load("America/Argentina/Buenos_Aires")
        Time.week_date(*CALENDAR_WEEK_TEST_DATA[0][1], 11, 57, 32, nanosecond: 123_567, location: location).should eq(
          Time.local(*CALENDAR_WEEK_TEST_DATA[0][0], 11, 57, 32, nanosecond: 123_567, location: location))
      end
    end
  end

  describe ".month_week_date" do
    it "works with DayOfWeek" do
      Time.month_week_date(2025, 4, 1, Time::DayOfWeek::Tuesday, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 1))
      Time.month_week_date(2025, 4, 1, Time::DayOfWeek::Sunday, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 6))
      Time.month_week_date(2025, 4, 1, Time::DayOfWeek::Monday, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 7))
      Time.month_week_date(2025, 4, 2, Time::DayOfWeek::Wednesday, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 9))
      Time.month_week_date(2025, 4, 5, Time::DayOfWeek::Thursday, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 24))
      Time.month_week_date(2025, 2, 5, Time::DayOfWeek::Saturday, location: Time::Location::UTC).should eq(Time.utc(2025, 2, 22))
    end

    it "works with integer day of week" do
      Time.month_week_date(2025, 4, 1, 2, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 1))
      Time.month_week_date(2025, 4, 1, 7, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 6))
      Time.month_week_date(2025, 4, 1, 0, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 6))
      Time.month_week_date(2025, 4, 1, 1, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 7))
      Time.month_week_date(2025, 4, 2, 3, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 9))
      Time.month_week_date(2025, 4, 5, 4, location: Time::Location::UTC).should eq(Time.utc(2025, 4, 24))
      Time.month_week_date(2025, 2, 5, 6, location: Time::Location::UTC).should eq(Time.utc(2025, 2, 22))
    end

    it "accepts time arguments" do
      with_zoneinfo do
        location = Time::Location.load("Europe/Berlin")
        Time.month_week_date(2025, 3, 3, 7, 11, 57, 32, nanosecond: 123_567, location: location).should eq(
          Time.local(2025, 3, 16, 11, 57, 32, nanosecond: 123_567, location: location))

        location = Time::Location.load("America/Argentina/Buenos_Aires")
        Time.month_week_date(2025, 3, 3, 7, 11, 57, 32, nanosecond: 123_567, location: location).should eq(
          Time.local(2025, 3, 16, 11, 57, 32, nanosecond: 123_567, location: location))
      end
    end

    it "raises on invalid week or day of week" do
      expect_raises(Exception) { Time.month_week_date(2025, 1, 0, Time::DayOfWeek::Monday) }
      expect_raises(Exception) { Time.month_week_date(2025, 1, 6, Time::DayOfWeek::Monday) }
      expect_raises(Exception) { Time.month_week_date(2025, 1, 1, -1) }
      expect_raises(Exception) { Time.month_week_date(2025, 1, 1, 8) }
    end
  end

  typeof(Time.local.year)
  typeof(1.minute.from_now.year)
  typeof(1.minute.ago.year)
  typeof(1.month.from_now.year)
  typeof(1.month.ago.year)
  typeof(Time.local.to_utc)
  typeof(Time.local.to_local)
end
