import { describe, it, expect } from "vitest"; import { resolveRange, bucketKeys, bucketKey, previousPeriod, startOfDayInTz, addDays, observedDays, isValidTimeZone } from "@/lib/usage/time"; // 2026-09-11 22:30 in Montréal = 2026-09-12 02:30Z const NOW = new Date("2026-09-12T02:30:00Z"); const TZ = "America/Toronto"; describe("usage time ranges", () => { it("resolves `today` to local midnight → next local midnight, hourly buckets", () => { const r = resolveRange({ range: "today", tz: TZ }, NOW); expect(r.from?.toISOString()).toBe("2026-09-11T04:00:00.000Z"); expect(r.to.toISOString()).toBe("2026-09-12T04:00:00.000Z"); expect(r.bucket).toBe("hour"); expect(r.days).toBe(1); expect(bucketKeys(r.from!, r.to, "hour", TZ)).toHaveLength(24); expect(bucketKeys(r.from!, r.to, "hour", TZ)[0]).toBe("2026-09-11T00:00"); }); it("7d / 30d / 90d cover N whole local days ending today", () => { const r = resolveRange({ range: "7d", tz: TZ }, NOW); expect(bucketKey(r.from!, "day", TZ)).toBe("2026-09-05"); expect(bucketKeys(r.from!, r.to, "day", TZ)).toEqual(["2026-09-05", "2026-09-06", "2026-09-07", "2026-09-08", "2026-09-09", "2026-09-10", "2026-09-11"]); expect(resolveRange({ range: "90d", tz: TZ }, NOW).days).toBe(90); expect(bucketKeys(resolveRange({ range: "90d", tz: TZ }, NOW).from!, NOW, "day", TZ)).toHaveLength(90); }); it("custom ranges are inclusive of both dates, clamped to today and to one year", () => { const r = resolveRange({ range: "custom", from: "2026-08-01", to: "2026-08-03", tz: TZ }, NOW); expect(bucketKeys(r.from!, r.to, r.bucket, TZ)).toEqual(["2026-08-01", "2026-08-02", "2026-08-03"]); expect(r.days).toBe(3); const fut = resolveRange({ range: "custom", from: "2026-09-10", to: "2030-01-01", tz: TZ }, NOW); expect(fut.to.toISOString()).toBe("2026-09-12T04:00:00.000Z"); const huge = resolveRange({ range: "custom", from: "2020-01-01", to: "2026-09-11", tz: TZ }, NOW); expect(huge.days).toBeLessThanOrEqual(366); const swapped = resolveRange({ range: "custom", from: "2026-08-10", to: "2026-08-01", tz: TZ }, NOW); expect(swapped.from! < swapped.to).toBe(true); // two days or less → hourly expect(resolveRange({ range: "custom", from: "2026-09-10", to: "2026-09-11", tz: TZ }, NOW).bucket).toBe("hour"); }); it("falls back to UTC / 30d on bad input", () => { const r = resolveRange({ range: "nope", tz: "Mars/Olympus" }, NOW); expect(r.key).toBe("30d"); expect(r.tz).toBe("UTC"); expect(isValidTimeZone("Europe/Paris")).toBe(true); expect(isValidTimeZone("../etc")).toBe(false); expect(isValidTimeZone("UTC")).toBe(true); }); it("previous period has the same length and ends where the range starts", () => { const r = resolveRange({ range: "7d", tz: TZ }, NOW); const p = previousPeriod(r)!; expect(p.to.getTime()).toBe(r.from!.getTime()); expect(r.to.getTime() - r.from!.getTime()).toBe(p.to.getTime() - p.from.getTime()); expect(previousPeriod(resolveRange({ range: "all", tz: TZ }, NOW))).toBeNull(); }); it("handles DST when adding days", () => { // 2026-11-01 is the fall-back date in Toronto const oct31 = startOfDayInTz(new Date("2026-10-31T12:00:00Z"), TZ); const nov2 = addDays(oct31, 2, TZ); expect(bucketKey(nov2, "day", TZ)).toBe("2026-11-02"); expect((nov2.getTime() - oct31.getTime()) / 3_600_000).toBe(49); }); it("observed days is fractional for today and never below one hour", () => { const r = resolveRange({ range: "today", tz: TZ }, NOW); const d = observedDays(r, NOW); expect(d).toBeGreaterThan(0.9); expect(d).toBeLessThan(1); expect(observedDays(resolveRange({ range: "all", tz: TZ }, NOW), NOW, null)).toBeCloseTo(1 / 24, 5); }); });