# Tests for time arithmetic: addition, subtraction, and duration operations.
#
# Cross-chronon arithmetic (e.g. date + months, date + years) uses calendar-field
# arithmetic: the duration's own chronon (e.g. month) is advanced by its magnitude, and the
# finer remainder (e.g. day-of-month) is preserved exactly, clamped to the shifted period's
# valid range if it would otherwise overflow (e.g. 2020-01-31 + months(1) -> 2020-02-29,
# not 2020-03-02).

# ---------------------------------------------------------------------------
# date + duration (same chronon: days)
# ---------------------------------------------------------------------------

test_that("date() + days() advances by the correct number of days", {
  d <- date(as.Date("2020-01-02"))

  expect_equal(format(d + days(1L)), "2020-01-03")
  expect_equal(format(d + days(7L)), "2020-01-09")
  expect_equal(format(d + days(30L)), "2020-02-01")
  expect_equal(format(d + days(0L)), "2020-01-02")
})

test_that("date() - days() retreats by the correct number of days", {
  d <- date(as.Date("2020-03-01"))

  expect_equal(format(d - days(1L)), "2020-02-29") # 2020 is a leap year
  expect_equal(format(d - days(29L)), "2020-02-01")
})

# ---------------------------------------------------------------------------
# date + duration (cross-chronon: days <-> months)
#
# The day-of-month is preserved exactly, clamped to the target month's length
# if it would otherwise overflow. Subtraction clamps against the earlier
# month's length in the same way.
# ---------------------------------------------------------------------------

test_that("date() + months(1L) advances to the same day next month", {
  expect_equal(format(date(as.Date("2020-01-02")) + months(1L)), "2020-02-02")
  expect_equal(format(date(as.Date("2019-12-15")) + months(1L)), "2020-01-15")
})

test_that("date() + months(n) advances by n calendar months, preserving day-of-month", {
  expect_equal(format(date(as.Date("2020-01-02")) + months(3L)), "2020-04-02")
  expect_equal(format(date(as.Date("2019-12-15")) + months(2L)), "2020-02-15")
})

test_that("date() + months() clamps an overflowing day-of-month to the target month's length", {
  # January 31 has no equivalent in February -> clamps to the last day
  expect_equal(format(date(as.Date("2020-01-31")) + months(1L)), "2020-02-29") # 2020 is a leap year
  expect_equal(format(date(as.Date("2021-01-31")) + months(1L)), "2021-02-28")
  # Clamping applies per intervening month, not just the destination
  expect_equal(format(date(as.Date("2020-01-31")) + months(2L)), "2020-03-31")
})

test_that("months() + date() is commutative", {
  expect_equal(format(months(1L) + date(as.Date("2020-01-02"))), "2020-02-02")
})

test_that("date() - months() retreats to the same day the given number of months earlier", {
  d <- date(as.Date("2020-03-15"))

  expect_equal(format(d - months(1L)), "2020-02-15")
  expect_equal(format(d - months(3L)), "2019-12-15")
})

test_that("date() - months() clamps an overflowing day-of-month to the earlier month's length", {
  # March 31 has no equivalent in February -> clamps to the last day
  expect_equal(format(date(as.Date("2020-03-31")) - months(1L)), "2020-02-29") # 2020 is a leap year
  expect_equal(format(date(as.Date("2021-03-31")) - months(1L)), "2021-02-28")
})

# ---------------------------------------------------------------------------
# date + duration (cross-chronon: days <-> years)
#
# days <-> years also has a direct (context-dependent) chronon_divmod() method - a flat
# day-of-year encoding, with no notion of "month" - but calendar-field arithmetic
# deliberately does *not* use it: it decomposes along the *longest* available path
# (day -> month -> year, not the shortcut edge) so that day-of-month is the component
# that gets clamped, exactly as it is for days <-> months. This is what keeps
# `+ years(1L)` and `+ months(12L)` in agreement.
# ---------------------------------------------------------------------------

test_that("date() + years(1L) advances to the same day next year", {
  expect_equal(format(date(as.Date("2020-01-02")) + years(1L)), "2021-01-02")
  expect_equal(format(date(as.Date("2021-01-02")) + years(1L)), "2022-01-02")
})

test_that("date() + years(1L) clamps a leap day to the last day of February in a common year", {
  expect_equal(format(date(as.Date("2024-02-29")) + years(1L)), "2025-02-28")
  # Agrees with the equivalent months() shift
  expect_equal(
    format(date(as.Date("2024-02-29")) + years(1L)),
    format(date(as.Date("2024-02-29")) + months(12L))
  )
})

test_that("date() - years(1L) retreats to the same day the given number of years earlier", {
  expect_equal(format(date(as.Date("2020-01-02")) - years(1L)), "2019-01-02")
  expect_equal(format(date(as.Date("2021-01-02")) - years(1L)), "2020-01-02")
})

# ---------------------------------------------------------------------------
# yearmonth + duration (same chronon: months) -- exact by definition
# ---------------------------------------------------------------------------

test_that("yearmonth() + months() advances by the correct number of months", {
  ym <- yearmonth(as.Date("2020-01-01"))

  expect_equal(format(ym + months(1L)), "2020 Feb")
  expect_equal(format(ym + months(12L)), "2021 Jan")
  expect_equal(format(ym + months(0L)), "2020 Jan")
})

test_that("yearmonth() - months() retreats by the correct number of months", {
  ym <- yearmonth(as.Date("2020-03-01"))

  expect_equal(format(ym - months(1L)), "2020 Feb")
  expect_equal(format(ym - months(3L)), "2019 Dec")
})

# ---------------------------------------------------------------------------
# yearmonth + duration (cross-chronon: months <-> years) -- exact (12 months/yr)
# ---------------------------------------------------------------------------

test_that("yearmonth() + years() advances by 12 months per year", {
  ym <- yearmonth(as.Date("2020-06-01"))

  expect_equal(format(ym + years(1L)), "2021 Jun")
  expect_equal(format(ym + years(2L)), "2022 Jun")
})

test_that("yearmonth() - years() retreats by 12 months per year", {
  ym <- yearmonth(as.Date("2023-03-01"))

  expect_equal(format(ym - years(1L)), "2022 Mar")
})

# ---------------------------------------------------------------------------
# time - time => duration
# ---------------------------------------------------------------------------

test_that("date() - date() returns the difference in days", {
  d1 <- date(as.Date("2020-02-01"))
  d2 <- date(as.Date("2020-01-01"))

  result <- d1 - d2
  expect_s7_class(result, class_mixtime)
  expect_equal(as.integer(result), 31L) # January has 31 days
})

test_that("date() - date() handles leap-year February correctly", {
  d1 <- date(as.Date("2020-03-01"))
  d2 <- date(as.Date("2020-02-01"))

  expect_equal(as.integer(d1 - d2), 29L) # 2020 is a leap year
})

test_that("date() - date() is zero for identical times", {
  d <- date(as.Date("2023-07-04"))
  expect_equal(as.integer(d - d), 0L)
})

test_that("yearmonth() - yearmonth() returns the difference in months", {
  ym1 <- yearmonth(as.Date("2021-04-01"))
  ym2 <- yearmonth(as.Date("2020-01-01"))

  expect_equal(as.integer(ym1 - ym2), 15L) # 12 + 3
})

test_that("yearmonth() - yearmonth() is negative when ym1 < ym2", {
  ym1 <- yearmonth(as.Date("2020-01-01"))
  ym2 <- yearmonth(as.Date("2020-04-01"))

  expect_equal(as.integer(ym1 - ym2), -3L)
})

# ---------------------------------------------------------------------------
# date +/- integer (direct numeric shift along the chronon)
# ---------------------------------------------------------------------------

test_that("date() + integer shifts by raw day units", {
  d <- date(as.Date("2020-01-01"))

  expect_equal(format(d + 1L), "2020-01-02")
  expect_equal(format(d + 31L), "2020-02-01")
})

test_that("yearmonth() + integer shifts by raw month units", {
  ym <- yearmonth(as.Date("2020-01-01"))

  expect_equal(format(ym + 1L), "2020 Feb")
  expect_equal(format(ym + 12L), "2021 Jan")
})

# ---------------------------------------------------------------------------
# Duration arithmetic (duration +/- duration, duration +/- integer)
# ---------------------------------------------------------------------------

test_that("duration + duration with the same chronon sums correctly", {
  expect_equal(as.integer(months(3L) + months(2L)), 5L)
  expect_equal(as.integer(days(10L) + days(5L)), 15L)
  expect_equal(as.integer(years(1L) + years(2L)), 3L)
})

test_that("duration - duration with the same chronon differences correctly", {
  expect_equal(as.integer(months(5L) - months(2L)), 3L)
  expect_equal(as.integer(days(10L) - days(3L)), 7L)
})

test_that("duration + integer shifts the duration magnitude", {
  expect_equal(as.integer(months(3L) + 2L), 5L)
  expect_equal(as.integer(days(7L) - 3L), 4L)
})

test_that("integer + duration shifts the duration magnitude", {
  expect_equal(as.integer(2L + months(3L)), 5L)
})

test_that("duration * numeric scales the duration magnitude", {
  expect_equal(as.integer(days(3L) * 3), 9L)
  expect_equal(as.integer(months(2L) * 6), 12L)
  expect_equal(as.integer(days(5L) * 0), 0L)
  expect_equal(as.integer(days(5L) * 1), 5L)
  expect_equal(as.integer(days(5L) * -1), -5L)
})

test_that("numeric * duration is commutative", {
  expect_equal(as.integer(3 * days(3L)), 9L)
  expect_equal(as.integer(0.5 * days(6L)), 3L)
})

test_that("duration / numeric scales the duration magnitude", {
  expect_equal(as.integer(days(6L) / 2), 3L)
  expect_equal(as.integer(months(9L) / 3), 3L)
  expect_equal(as.double(days(5L) / 2), 2.5)
})

test_that("duration / duration returns a dimensionless ratio", {
  expect_equal(as.double(days(6L) / days(2L)), 3)
  expect_equal(as.double(days(6L) / days(6L)), 1)
  expect_equal(as.double(months(4L) / months(2L)), 2)
})

test_that("duration / duration converts to a common chronon before dividing (same calendar)", {
  # months and years share cal_gregorian; years(1L) == 12 months
  expect_equal(as.double(years(2L) / months(1L)), 24)
  expect_equal(as.double(months(6L) / years(1L)), 0.5)
})

test_that("duration / duration converts to a common chronon before dividing (cross-calendar)", {
  # weeks (cal_isoweek) and days (cal_gregorian) share a common day chronon
  expect_equal(as.double(weeks(1L) / days(1L)), 7)
  expect_equal(as.double(days(14L) / weeks(1L)), 2)
})

# ---------------------------------------------------------------------------
# Vectorised operations
# ---------------------------------------------------------------------------

test_that("vectorised date() + days() works element-wise", {
  d <- date(as.Date("2020-01-01") + 0:2)

  result <- d + days(1L)
  expect_equal(format(result), c("2020-01-02", "2020-01-03", "2020-01-04"))
})

test_that("vectorised date() + months() works element-wise", {
  d <- date(as.Date(c("2020-01-15", "2020-02-15", "2020-03-15")))

  # Jan=31, Feb=29 (2020 leap), Mar=31 -> each advances by its own month's length
  result <- d + months(1L)
  expect_equal(format(result), c("2020-02-15", "2020-03-15", "2020-04-15"))
})

test_that("vectorised date() - date() returns element-wise differences in days", {
  d1 <- date(as.Date("2020-12-31") + 0:2)
  d2 <- date(as.Date("2020-12-31"))

  result <- d1 - d2
  expect_equal(as.integer(result), 0:2)
})

# ---------------------------------------------------------------------------
# NA propagation
# ---------------------------------------------------------------------------

test_that("NA propagates through date() + months()", {
  d <- date(as.Date(c("2020-01-01", NA, "2020-03-01")))

  result <- d + months(1L)
  expect_equal(is.na(result), c(FALSE, TRUE, FALSE))
})

test_that("NA propagates through date() - date()", {
  d1 <- date(as.Date(c("2020-02-01", NA)))
  d2 <- date(as.Date("2020-01-01"))

  result <- d1 - d2
  expect_equal(is.na(result), c(FALSE, TRUE))
})

# ---------------------------------------------------------------------------
# Unsupported operations error gracefully
# ---------------------------------------------------------------------------

test_that("multiplying two time points errors", {
  d <- date(as.Date("2020-01-01"))
  expect_error(d * d)
})

test_that("adding two time points errors", {
  d <- date(as.Date("2020-01-01"))
  expect_error(d + d)
})

# ---------------------------------------------------------------------------
# Timezone-aware time - date subtraction (issue #97)
# ---------------------------------------------------------------------------

test_that("datetime() - date() uses the date's timezone for its day boundary", {
  # 2026-06-16 09:30:00 AEST = 2026-06-15 23:30:00 UTC = 2026-06-16 01:30:00 CEST
  t <- datetime("2026-06-16 09:30:00", tz = "Australia/Melbourne")

  # Melbourne date: 2026-06-16 AEST; time-of-day in Melbourne = 9h30m = 34200s
  result_mel <- t - date(t, tz = "Australia/Melbourne", discrete = TRUE)
  expect_equal(as.double(result_mel), 34200)

  # Berlin date: 2026-06-16 CEST; time-of-day in Berlin = 1h30m = 5400s
  result_ber <- t - date(t, tz = "Europe/Berlin", discrete = TRUE)
  expect_equal(as.double(result_ber), 5400)

  # UTC date: 2026-06-15 UTC; time-of-day in UTC = 23h30m = 84600s
  result_utc <- t - date(t, tz = "UTC", discrete = TRUE)
  expect_equal(as.double(result_utc), 84600)
})

test_that("datetime() with a timezone can be shifted by durations with a regular cardinality", {
  d <- datetime("2026-06-03 02:30:00", tz = "Australia/Melbourne")

  expect_equal(format(d + hours(1L)), "2026-06-03 03:30:00 AEST")
  expect_equal(format(d + days(1L)), "2026-06-04 02:30:00 AEST")
  expect_equal(format(d - hours(1L)), "2026-06-03 01:30:00 AEST")
  expect_equal(format(hours(1L) + d), "2026-06-03 03:30:00 AEST")
})
