# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec

Code.require_file("../test_helper.exs", __DIR__)
Code.require_file("holocene.exs", __DIR__)
Code.require_file("fakes.exs", __DIR__)

defmodule DateTest do
  use ExUnit.Case, async: true
  doctest Date

  test "sigil_D" do
    assert ~D[2000-01-01] ==
             %Date{calendar: Calendar.ISO, year: 2000, month: 1, day: 1}

    assert ~D[20001-01-01 Calendar.Holocene] ==
             %Date{calendar: Calendar.Holocene, year: 20001, month: 1, day: 1}

    assert_raise ArgumentError,
                 ~s/cannot parse "2000-50-50" as Date for Calendar.ISO, reason: :invalid_date/,
                 fn -> Code.eval_string("~D[2000-50-50]") end

    assert_raise ArgumentError,
                 ~s/cannot parse "2000-04-15 notalias" as Date for Calendar.ISO, reason: :invalid_format/,
                 fn -> Code.eval_string("~D[2000-04-15 notalias]") end

    assert_raise ArgumentError,
                 ~s/cannot parse "20010415" as Date for Calendar.ISO, reason: :invalid_format/,
                 fn -> Code.eval_string(~s{~D[20010415]}) end

    assert_raise ArgumentError,
                 ~s/cannot parse "20001-50-50" as Date for Calendar.Holocene, reason: :invalid_date/,
                 fn -> Code.eval_string("~D[20001-50-50 Calendar.Holocene]") end

    assert_raise UndefinedFunctionError, fn ->
      Code.eval_string("~D[2000-01-01 UnknownCalendar]")
    end
  end

  test "to_string/1" do
    date = ~D[2000-01-01]
    assert to_string(date) == "2000-01-01"
    assert Date.to_string(date) == "2000-01-01"
    assert Date.to_string(Map.from_struct(date)) == "2000-01-01"

    assert to_string(%{date | calendar: FakeCalendar}) == "1/1/2000"
    assert Date.to_string(%{date | calendar: FakeCalendar}) == "1/1/2000"

    date2 = Date.new!(5_874_897, 12, 31)
    assert to_string(date2) == "5874897-12-31"
    assert Date.to_string(date2) == "5874897-12-31"
    assert Date.to_string(Map.from_struct(date2)) == "5874897-12-31"

    assert to_string(%{date2 | calendar: FakeCalendar}) == "31/12/5874897"
    assert Date.to_string(%{date2 | calendar: FakeCalendar}) == "31/12/5874897"
  end

  test "inspect/1" do
    assert inspect(~D[2000-01-01]) == "~D[2000-01-01]"
    assert inspect(~D[-0100-12-31]) == "~D[-0100-12-31]"

    date = %{~D[2000-01-01] | calendar: FakeCalendar}
    assert inspect(date) == "~D[1/1/2000 FakeCalendar]"

    assert inspect(Date.new!(99999, 12, 31)) == "Date.new!(99999, 12, 31)"
    assert inspect(Date.new!(-99999, 1, 1)) == "Date.new!(-99999, 1, 1)"

    date2 = %{Date.new!(99999, 12, 31) | calendar: FakeCalendar}

    assert inspect(%{date2 | calendar: FakeCalendar}) ==
             "~D[31/12/99999 FakeCalendar]"
  end

  test "compare/2" do
    date1 = ~D[-0001-12-30]
    date2 = ~D[-0001-12-31]
    date3 = ~D[0001-01-01]
    date4 = Date.new!(5_874_897, 12, 31)
    date5 = Date.new!(-4713, 1, 1)

    assert Date.compare(date1, date1) == :eq
    assert Date.compare(date1, date2) == :lt
    assert Date.compare(date2, date1) == :gt
    assert Date.compare(date3, date3) == :eq
    assert Date.compare(date2, date3) == :lt
    assert Date.compare(date3, date2) == :gt
    assert Date.compare(date4, date1) == :gt
    assert Date.compare(date1, date4) == :lt
    assert Date.compare(date4, date4) == :eq
    assert Date.compare(date4, date5) == :gt
    assert Date.compare(date5, date4) == :lt
    assert Date.compare(date5, date5) == :eq

    assert_raise ArgumentError,
                 ~r/cannot compare .*\n\n.* their calendars have incompatible day rollover moments/,
                 fn -> Date.compare(date1, %{date2 | calendar: FakeCalendar}) end
  end

  test "before?/2 and after?/2" do
    date1 = ~D[2022-11-01]
    date2 = ~D[2022-11-02]
    date3 = Date.new!(5_874_897, 12, 31)
    date4 = Date.new!(-4713, 1, 1)

    assert Date.before?(date1, date2)
    assert Date.before?(date1, date3)
    assert Date.before?(date4, date1)
    assert not Date.before?(date2, date1)
    assert not Date.before?(date3, date1)
    assert not Date.before?(date1, date4)

    assert Date.after?(date2, date1)
    assert Date.after?(date3, date2)
    assert Date.after?(date2, date4)
    assert not Date.after?(date1, date2)
    assert not Date.after?(date2, date3)
    assert not Date.after?(date4, date2)
  end

  test "compare/2 across calendars" do
    date1 = ~D[2000-01-01]
    date2 = Calendar.Holocene.date(12000, 01, 01)
    assert Date.compare(date1, date2) == :eq

    date2 = Calendar.Holocene.date(12001, 01, 01)
    assert Date.compare(date1, date2) == :lt
    assert Date.compare(date2, date1) == :gt
  end

  test "day_of_week/1" do
    assert Date.day_of_week(Calendar.Holocene.date(2016, 10, 31)) == 1
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 01)) == 2
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 02)) == 3
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 03)) == 4
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 04)) == 5
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 05)) == 6
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 06)) == 7
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 07)) == 1

    assert Date.day_of_week(Calendar.Holocene.date(2016, 10, 30), :sunday) == 1
    assert Date.day_of_week(Calendar.Holocene.date(2016, 10, 31), :sunday) == 2
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 01), :sunday) == 3
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 02), :sunday) == 4
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 03), :sunday) == 5
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 04), :sunday) == 6
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 05), :sunday) == 7
    assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 06), :sunday) == 1
  end

  test "beginning_of_week" do
    assert Date.beginning_of_week(Calendar.Holocene.date(2020, 07, 11)) ==
             Calendar.Holocene.date(2020, 07, 06)

    assert Date.beginning_of_week(Calendar.Holocene.date(2020, 07, 06)) ==
             Calendar.Holocene.date(2020, 07, 06)

    assert Date.beginning_of_week(Calendar.Holocene.date(2020, 07, 11), :sunday) ==
             Calendar.Holocene.date(2020, 07, 05)

    assert Date.beginning_of_week(Calendar.Holocene.date(2020, 07, 11), :saturday) ==
             Calendar.Holocene.date(2020, 07, 11)
  end

  test "end_of_week" do
    assert Date.end_of_week(Calendar.Holocene.date(2020, 07, 11)) ==
             Calendar.Holocene.date(2020, 07, 12)

    assert Date.end_of_week(Calendar.Holocene.date(2020, 07, 05)) ==
             Calendar.Holocene.date(2020, 07, 05)

    assert Date.end_of_week(Calendar.Holocene.date(2020, 07, 05), :sunday) ==
             Calendar.Holocene.date(2020, 07, 11)

    assert Date.end_of_week(Calendar.Holocene.date(2020, 07, 05), :saturday) ==
             Calendar.Holocene.date(2020, 07, 10)
  end

  test "convert/2" do
    assert Date.convert(~D[2000-01-01], Calendar.Holocene) ==
             {:ok, Calendar.Holocene.date(12000, 01, 01)}

    assert ~D[2000-01-01]
           |> Date.convert!(Calendar.Holocene)
           |> Date.convert!(Calendar.ISO) == ~D[2000-01-01]

    assert Date.convert(~N[2000-01-01 00:00:00], Calendar.Holocene) ==
             {:ok, Calendar.Holocene.date(12000, 01, 01)}

    assert Date.convert(~D[2016-02-03], FakeCalendar) == {:error, :incompatible_calendars}

    assert_raise ArgumentError,
                 "cannot convert ~D[2016-02-03] to target calendar FakeCalendar, reason: :incompatible_calendars",
                 fn -> Date.convert!(~D[2016-02-03], FakeCalendar) end
  end

  test "add/2" do
    assert Date.add(~D[0000-01-01], 3_652_424) == ~D[9999-12-31]
    assert Date.add(~D[0000-01-01], 3_652_425) == Date.new!(10000, 1, 1)
    assert Date.add(~D[0000-01-01], -1) == ~D[-0001-12-31]
    assert Date.add(~D[0000-01-01], -365) == ~D[-0001-01-01]
    assert Date.add(~D[0000-01-01], -366) == ~D[-0002-12-31]
    assert Date.add(~D[0000-01-01], -(365 * 4)) == ~D[-0004-01-02]
    assert Date.add(~D[0000-01-01], -(365 * 5)) == ~D[-0005-01-02]
    assert Date.add(~D[0000-01-01], -(365 * 100)) == ~D[-0100-01-25]
    assert Date.add(~D[0000-01-01], -3_652_059) == ~D[-9999-01-01]
    assert Date.add(~D[0000-01-01], -3_652_060) == Date.new!(-10000, 12, 31)
    assert Date.add(Date.new!(5_874_897, 12, 31), 1) == Date.new!(5_874_898, 1, 1)
  end

  test "diff/2" do
    assert Date.diff(~D[2000-01-31], ~D[2000-01-01]) == 30
    assert Date.diff(~D[2000-01-01], ~D[2000-01-31]) == -30

    assert Date.diff(~D[0000-01-01], ~D[-0001-01-01]) == 365
    assert Date.diff(~D[-0003-01-01], ~D[-0004-01-01]) == 366

    assert Date.diff(Date.new!(5_874_898, 1, 1), Date.new!(5_874_897, 1, 1)) == 365
    assert Date.diff(Date.new!(5_874_905, 1, 1), Date.new!(5_874_904, 1, 1)) == 366

    date1 = ~D[2000-01-01]
    date2 = Calendar.Holocene.date(12000, 01, 14)
    assert Date.diff(date1, date2) == -13
    assert Date.diff(date2, date1) == 13

    assert_raise ArgumentError,
                 ~r/cannot calculate the difference between .* because their calendars are not compatible/,
                 fn -> Date.diff(date1, %{date2 | calendar: FakeCalendar}) end
  end

  test "shift/2" do
    assert Date.shift(~D[2012-02-29], day: -1) == ~D[2012-02-28]
    assert Date.shift(~D[2012-02-29], month: -1) == ~D[2012-01-29]
    assert Date.shift(~D[2012-02-29], week: -9) == ~D[2011-12-28]
    assert Date.shift(~D[2012-02-29], month: 1) == ~D[2012-03-29]
    assert Date.shift(~D[2012-02-29], year: -1) == ~D[2011-02-28]
    assert Date.shift(~D[2012-02-29], year: 4) == ~D[2016-02-29]
    assert Date.shift(~D[0000-01-01], day: -1) == ~D[-0001-12-31]
    assert Date.shift(~D[0000-01-01], month: -1) == ~D[-0001-12-01]
    assert Date.shift(~D[0000-01-01], year: -1) == ~D[-0001-01-01]
    assert Date.shift(~D[0000-01-01], year: -1) == ~D[-0001-01-01]
    assert Date.shift(~D[2000-01-01], month: 12) == ~D[2001-01-01]
    assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == ~D[0004-02-03]

    assert Date.shift(Date.new!(5_874_904, 2, 29), day: -1) == Date.new!(5_874_904, 2, 28)
    assert Date.shift(Date.new!(5_874_904, 2, 29), month: -2) == Date.new!(5_874_903, 12, 29)
    assert Date.shift(Date.new!(5_874_904, 2, 29), week: -9) == Date.new!(5_874_903, 12, 28)
    assert Date.shift(Date.new!(5_874_904, 2, 29), month: 1) == Date.new!(5_874_904, 3, 29)
    assert Date.shift(Date.new!(5_874_904, 2, 29), year: -1) == Date.new!(5_874_903, 2, 28)
    assert Date.shift(Date.new!(5_874_904, 2, 29), year: -4) == Date.new!(5_874_900, 2, 28)
    assert Date.shift(Date.new!(5_874_904, 2, 29), year: 4) == Date.new!(5_874_908, 2, 29)

    assert Date.shift(Date.new!(5_874_904, 2, 29), day: 1, year: 4, month: 2) ==
             Date.new!(5_874_908, 4, 30)

    assert_raise ArgumentError,
                 "unsupported unit :second. Expected :year, :month, :week, :day",
                 fn -> Date.shift(~D[2012-02-29], second: 86400) end

    assert_raise ArgumentError,
                 "unknown unit :months. Expected :year, :month, :week, :day",
                 fn -> Date.shift(~D[2012-01-01], months: 12) end

    assert_raise ArgumentError,
                 "unsupported value nil for :day. Expected an integer",
                 fn -> Date.shift(~D[2012-02-29], year: 1, day: nil) end

    assert_raise ArgumentError,
                 "cannot shift date by time scale unit. Expected :year, :month, :week, :day",
                 fn -> Date.shift(~D[2012-02-29], %Duration{second: 86400}) end

    # Microsecond precision is ignored
    assert Date.shift(~D[2012-02-29], Duration.new!(microsecond: {0, 6})) == ~D[2012-02-29]

    assert_raise ArgumentError,
                 "cannot shift date by time scale unit. Expected :year, :month, :week, :day",
                 fn -> Date.shift(~D[2012-02-29], %Duration{microsecond: {100, 6}}) end

    # Implements calendar callback
    assert_raise RuntimeError, "shift_date/4 not implemented", fn ->
      date = Calendar.Holocene.date(10000, 01, 01)
      Date.shift(date, month: 1)
    end
  end

  test "utc_today/1" do
    date = Date.utc_today()
    assert date.year > 2020
    assert date.calendar == Calendar.ISO

    date = Date.utc_today(Calendar.ISO)
    assert date.year > 2020
    assert date.calendar == Calendar.ISO

    date = Date.utc_today(Calendar.Holocene)
    assert date.year > 12020
    assert date.calendar == Calendar.Holocene
  end
end
