import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RELATIVE_UNITS, relativeTime, relativeFromMs } from "./format";
import en from "../i18n/locales/en/time.json";
import hu from "../i18n/locales/hu/time.json";

// i18n boots the whole locale glob (and touches localStorage/document) on import, so stub it.
// `language` is mutable because relativeFromMs reads it per call to pick the Intl locale — these
// cases assert both languages by flipping it. `t` echoes the key: the only string still coming
// from the locale files is justNow.
const i18nStub = vi.hoisted(() => ({ language: "en", t: (key: string) => key }));
vi.mock("../i18n", () => ({ default: i18nStub }));

const MIN = 60;
const HOUR = 3600;
const DAY = 86400;
const WEEK = 604800;
const MONTH = 2592000;
const YEAR = 31536000;

// [age in seconds, expected tier, expected en, expected hu]. Every tier is sampled at its exact
// opening second and inside it — an interior-only suite still passes if the range comparison
// flips to `<=`. The wording comes from Intl/CLDR rather than from us, so a failure here after a
// runtime upgrade means the platform's copy moved, not that tier selection broke; the tier column
// is what pins our own logic.
const CASES: [number, string, string, string][] = [
  [MIN - 1, "justNow", "time.justNow", "time.justNow"],
  [MIN, "minute", "1m ago", "1 perce"],
  [90, "minute", "1m ago", "1 perce"],
  [HOUR - 1, "minute", "59m ago", "59 perce"],
  [HOUR, "hour", "1h ago", "1 órája"],
  [2 * HOUR, "hour", "2h ago", "2 órája"],
  [DAY - 1, "hour", "23h ago", "23 órája"],
  [DAY, "day", "1d ago", "1 napja"],
  [25 * HOUR, "day", "1d ago", "1 napja"],
  [WEEK - 1, "day", "6d ago", "6 napja"],
  [WEEK, "week", "1w ago", "1 hete"],
  [8 * DAY, "week", "1w ago", "1 hete"],
  [MONTH - 1, "week", "4w ago", "4 hete"],
  [MONTH, "month", "1mo ago", "1 hónapja"],
  [40 * DAY, "month", "1mo ago", "1 hónapja"],
  [YEAR - 1, "month", "12mo ago", "12 hónapja"],
  [YEAR, "year", "1y ago", "1 éve"],
  [400 * DAY, "year", "1y ago", "1 éve"],
  [10 * YEAR, "year", "10y ago", "10 éve"],
];

// Fixed clock: relativeFromMs reads Date.now(), so the age under test has to be exact.
const NOW = Date.UTC(2026, 6, 20, 12, 0, 0);

describe("relativeFromMs", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(NOW);
    i18nStub.language = "en";
  });
  afterEach(() => {
    vi.useRealTimers();
  });

  for (const [secs, tier, expectedEn, expectedHu] of CASES) {
    it(`${secs}s is ${tier} — "${expectedEn}" / "${expectedHu}"`, () => {
      expect(relativeFromMs(NOW - secs * 1000)).toBe(expectedEn);
      i18nStub.language = "hu";
      expect(relativeFromMs(NOW - secs * 1000)).toBe(expectedHu);
    });
  }

  it("follows a language switch at runtime", () => {
    const twoHoursAgo = NOW - 2 * HOUR * 1000;
    expect(relativeFromMs(twoHoursAgo)).toBe("2h ago");
    i18nStub.language = "hu";
    expect(relativeFromMs(twoHoursAgo)).toBe("2 órája");
    i18nStub.language = "en";
    expect(relativeFromMs(twoHoursAgo)).toBe("2h ago");
  });

  it("clamps a future timestamp instead of counting up", () => {
    expect(relativeFromMs(NOW + 5 * DAY * 1000)).toBe("time.justNow");
  });

  it("renders nothing for a timestamp it cannot read", () => {
    expect(relativeTime("not-a-date")).toBe("");
    expect(relativeTime(null)).toBe("");
    expect(relativeFromMs(NaN)).toBe("");
  });

  // Derived from RELATIVE_UNITS, not from CASES, so adding a tier to the table without covering
  // it here fails rather than slipping through unnoticed.
  it("keeps the unit table sorted ascending", () => {
    const divisors = RELATIVE_UNITS.map(([secs]) => secs);
    expect(divisors, "row 0 doubles as the just-now threshold").toEqual(
      [...divisors].sort((a, b) => a - b),
    );
    expect(new Set(divisors).size, "duplicate divisors would make a tier unreachable").toBe(
      divisors.length,
    );
  });

  it("exercises every tier in the unit table", () => {
    const covered = new Set(CASES.map(([, tier]) => tier));
    for (const [, unit] of RELATIVE_UNITS) {
      expect(covered, `no case covers ${unit}`).toContain(unit);
    }
  });

  it("still has the one string Intl cannot produce, in both languages", () => {
    expect(en).toHaveProperty("justNow");
    expect(hu).toHaveProperty("justNow");
  });
});
