import { describe, expect, test } from "bun:test"

import {
  addDays,
  dayIn,
  isIsoDate,
  isWorkingDay,
  isoDayOfWeek,
  lastDayOfMonth,
  nextWorkingDay,
} from "@/lib/calendar"

const none = new Set<string>()

describe("dayIn", () => {
  /**
   * The case the previous system got wrong. Late evening in Bangkok is already
   * the next day there and still the day before in UTC, and dating a row by the
   * machine's clock put every instance one day early on a UTC host.
   */
  test("an instant late in the Bangkok evening is already the next day", () => {
    expect(dayIn(new Date("2026-08-02T17:30:00Z"))).toBe("2026-08-03")
  })

  test("an instant early in the Bangkok morning is still the same day", () => {
    expect(dayIn(new Date("2026-08-03T02:00:00Z"))).toBe("2026-08-03")
  })

  test("it is not the machine's day", () => {
    const instant = new Date("2026-08-02T20:00:00Z")
    expect(dayIn(instant)).toBe("2026-08-03")
    expect(dayIn(instant, "UTC")).toBe("2026-08-02")
  })
})

describe("isIsoDate", () => {
  test("accepts a real day", () => {
    expect(isIsoDate("2026-02-28")).toBe(true)
    expect(isIsoDate("2028-02-29")).toBe(true)
  })
  test("refuses a day that does not exist", () => {
    expect(isIsoDate("2026-02-30")).toBe(false)
    expect(isIsoDate("2026-13-01")).toBe(false)
    expect(isIsoDate("2026-04-31")).toBe(false)
  })
  test("refuses a shape that would sort wrong", () => {
    expect(isIsoDate("2026-8-1")).toBe(false)
  })
})

describe("lastDayOfMonth", () => {
  test("knows the short months", () => {
    expect(lastDayOfMonth(2026, 2)).toBe(28)
    expect(lastDayOfMonth(2028, 2)).toBe(29)
    expect(lastDayOfMonth(2026, 4)).toBe(30)
    expect(lastDayOfMonth(2026, 12)).toBe(31)
  })
})

describe("isoDayOfWeek and addDays", () => {
  test("Monday is 1 and Sunday is 7", () => {
    expect(isoDayOfWeek("2026-08-03")).toBe(1)
    expect(isoDayOfWeek("2026-08-09")).toBe(7)
  })
  test("crosses a month and a year without help", () => {
    expect(addDays("2026-08-31", 1)).toBe("2026-09-01")
    expect(addDays("2026-01-01", -1)).toBe("2025-12-31")
    expect(addDays("2028-02-28", 1)).toBe("2028-02-29")
  })
})

describe("isWorkingDay", () => {
  const saturday = "2026-08-08"
  const sunday = "2026-08-09"
  const monday = "2026-08-03"

  test("Sunday is nobody's working day", () => {
    expect(isWorkingDay(sunday, "mon_fri", none)).toBe(false)
    expect(isWorkingDay(sunday, "mon_sat", none)).toBe(false)
  })

  test("Saturday depends on the pattern, which is the point of having one", () => {
    expect(isWorkingDay(saturday, "mon_fri", none)).toBe(false)
    expect(isWorkingDay(saturday, "mon_sat", none)).toBe(true)
  })

  test("a holiday is not a working day for anybody", () => {
    expect(isWorkingDay(monday, "mon_sat", new Set([monday]))).toBe(false)
  })
})

describe("nextWorkingDay", () => {
  test("leaves a working day where it is", () => {
    expect(nextWorkingDay("2026-08-03", "mon_fri", none)).toBe("2026-08-03")
  })

  test("moves a Saturday to Monday for a five-day week, and not for a six-day one", () => {
    expect(nextWorkingDay("2026-08-08", "mon_fri", none)).toBe("2026-08-10")
    expect(nextWorkingDay("2026-08-08", "mon_sat", none)).toBe("2026-08-08")
  })

  test("steps over a holiday that is itself a Monday", () => {
    const holidays = new Set(["2026-08-10"])
    expect(nextWorkingDay("2026-08-08", "mon_fri", holidays)).toBe("2026-08-11")
  })

  test("gives up rather than looping when the calendar swallows a month", () => {
    const everything = new Set(
      Array.from({ length: 40 }, (_, i) => addDays("2026-08-01", i)),
    )
    expect(nextWorkingDay("2026-08-01", "mon_sat", everything)).toBeNull()
  })
})
