// Unit tests for the date-range picker's pure helpers (t-paliad-248).
// Run with `bun test`.

import { test, expect, describe } from "bun:test";
import {
  horizonBounds,
  isValidHorizon,
  isValidISODate,
  validateCustomRange,
  parseURL,
  serializeURL,
  isDefault,
  ALL_HORIZONS,
  PAST_HORIZONS,
  NEXT_HORIZONS,
  type TimeHorizon,
  type TimeSpec,
} from "./date-range-picker-pure";

// Anchor the clock so day-arithmetic assertions don't drift with the
// wall clock. 2026-05-25 00:00 UTC matches the Go-side bounds test.
const NOW = new Date(Date.UTC(2026, 4, 25));
const DAY = (offsetDays: number): Date =>
  new Date(NOW.getTime() + offsetDays * 86_400_000);

describe("ALL_HORIZONS / PAST / NEXT registries", () => {
  test("registries sum to a known total without overlap", () => {
    // 6 past + 6 next + any + custom = 14 fan chips (custom is the
    // trailing entry in ALL_HORIZONS; `all` is intentionally absent —
    // surfaces don't render the legacy bidirectional-unbounded chip).
    expect(ALL_HORIZONS.length).toBe(14);
    expect(PAST_HORIZONS.length).toBe(6);
    expect(NEXT_HORIZONS.length).toBe(6);
    expect(new Set(ALL_HORIZONS).size).toBe(ALL_HORIZONS.length);
  });

  test("PAST_HORIZONS are all past_*", () => {
    for (const h of PAST_HORIZONS) {
      expect(h.startsWith("past_")).toBe(true);
    }
  });

  test("NEXT_HORIZONS are all next_*", () => {
    for (const h of NEXT_HORIZONS) {
      expect(h.startsWith("next_")).toBe(true);
    }
  });

  test("ALL_HORIZONS ends with custom and contains any in the middle", () => {
    expect(ALL_HORIZONS.at(-1)).toBe("custom");
    expect(ALL_HORIZONS).toContain("any");
  });
});

describe("horizonBounds", () => {
  test("future fan: bounds anchor at today, extend forward", () => {
    expect(horizonBounds("next_1d", NOW)).toEqual({ from: DAY(0),  to: DAY(1) });
    expect(horizonBounds("next_7d", NOW)).toEqual({ from: DAY(0),  to: DAY(7) });
    expect(horizonBounds("next_14d", NOW)).toEqual({ from: DAY(0), to: DAY(14) });
    expect(horizonBounds("next_30d", NOW)).toEqual({ from: DAY(0), to: DAY(30) });
    expect(horizonBounds("next_90d", NOW)).toEqual({ from: DAY(0), to: DAY(90) });
  });

  test("past fan: bounds extend back, upper bound is tomorrow (exclusive end-of-today)", () => {
    expect(horizonBounds("past_1d",  NOW)).toEqual({ from: DAY(-1),  to: DAY(1) });
    expect(horizonBounds("past_7d",  NOW)).toEqual({ from: DAY(-7),  to: DAY(1) });
    expect(horizonBounds("past_14d", NOW)).toEqual({ from: DAY(-14), to: DAY(1) });
    expect(horizonBounds("past_30d", NOW)).toEqual({ from: DAY(-30), to: DAY(1) });
    expect(horizonBounds("past_90d", NOW)).toEqual({ from: DAY(-90), to: DAY(1) });
  });

  test("next_all is one-sided: from=today, to undefined", () => {
    const b = horizonBounds("next_all", NOW);
    expect(b.from).toEqual(DAY(0));
    expect(b.to).toBeUndefined();
  });

  test("past_all is one-sided: from undefined, to=tomorrow", () => {
    const b = horizonBounds("past_all", NOW);
    expect(b.from).toBeUndefined();
    expect(b.to).toEqual(DAY(1));
  });

  test("any / all / custom: both bounds undefined", () => {
    expect(horizonBounds("any",    NOW)).toEqual({});
    expect(horizonBounds("all",    NOW)).toEqual({});
    expect(horizonBounds("custom", NOW)).toEqual({});
  });

  test("bounds anchor on UTC start-of-day regardless of input clock time", () => {
    const nowAfternoon = new Date(Date.UTC(2026, 4, 25, 14, 37, 0));
    const nowMidnight = new Date(Date.UTC(2026, 4, 25, 0, 0, 0));
    expect(horizonBounds("past_7d", nowAfternoon)).toEqual(horizonBounds("past_7d", nowMidnight));
  });
});

describe("isValidHorizon", () => {
  test("accepts every entry in ALL_HORIZONS plus 'all' (legacy)", () => {
    for (const h of ALL_HORIZONS) {
      expect(isValidHorizon(h)).toBe(true);
    }
    expect(isValidHorizon("all")).toBe(true);
  });

  test("rejects unknown strings, numbers, undefined, null", () => {
    expect(isValidHorizon("next_5d")).toBe(false);
    expect(isValidHorizon("past_100d")).toBe(false);
    expect(isValidHorizon("")).toBe(false);
    expect(isValidHorizon(7)).toBe(false);
    expect(isValidHorizon(undefined)).toBe(false);
    expect(isValidHorizon(null)).toBe(false);
  });
});

describe("isValidISODate", () => {
  test("accepts valid YYYY-MM-DD", () => {
    expect(isValidISODate("2026-05-25")).toBe(true);
    expect(isValidISODate("2026-12-31")).toBe(true);
    expect(isValidISODate("2024-02-29")).toBe(true);
  });

  test("rejects shape mismatches", () => {
    expect(isValidISODate("2026/05/25")).toBe(false);
    expect(isValidISODate("25.05.2026")).toBe(false);
    expect(isValidISODate("2026-5-25")).toBe(false);
    expect(isValidISODate("")).toBe(false);
    expect(isValidISODate(undefined)).toBe(false);
  });

  test("rejects calendar-impossible dates (Date.parse silently rolls over)", () => {
    expect(isValidISODate("2026-02-30")).toBe(false);
    expect(isValidISODate("2026-13-01")).toBe(false);
    expect(isValidISODate("2026-04-31")).toBe(false);
  });

  test("rejects 2025-02-29 (non-leap February)", () => {
    expect(isValidISODate("2025-02-29")).toBe(false);
  });
});

describe("validateCustomRange", () => {
  test("requires both bounds present and valid", () => {
    expect(validateCustomRange(undefined, undefined)).toBe("date_range.custom.invalid_missing");
    expect(validateCustomRange("2026-05-25", undefined)).toBe("date_range.custom.invalid_missing");
    expect(validateCustomRange(undefined, "2026-05-25")).toBe("date_range.custom.invalid_missing");
  });

  test("rejects malformed dates with format error", () => {
    expect(validateCustomRange("bogus", "2026-05-25")).toBe("date_range.custom.invalid_format");
    expect(validateCustomRange("2026-13-01", "2026-12-31")).toBe("date_range.custom.invalid_format");
  });

  test("rejects to <= from with invalid error", () => {
    expect(validateCustomRange("2026-05-25", "2026-05-25")).toBe("date_range.custom.invalid");
    expect(validateCustomRange("2026-05-25", "2026-05-24")).toBe("date_range.custom.invalid");
  });

  test("accepts strictly-ordered valid pair", () => {
    expect(validateCustomRange("2026-05-25", "2026-05-26")).toBeNull();
    expect(validateCustomRange("2026-01-01", "2026-12-31")).toBeNull();
  });
});

describe("parseURL", () => {
  test("missing horizon yields contract default", () => {
    expect(parseURL(new URLSearchParams(""))).toEqual({ horizon: "any" });
    expect(parseURL(new URLSearchParams(""), { default: "next_30d" })).toEqual({ horizon: "next_30d" });
  });

  test("unknown horizon falls back to default, doesn't throw", () => {
    expect(parseURL(new URLSearchParams("horizon=mystery"), { default: "next_7d" }))
      .toEqual({ horizon: "next_7d" });
  });

  test("every fan horizon round-trips on a fresh URLSearchParams", () => {
    for (const h of ALL_HORIZONS) {
      if (h === "custom") continue;
      const params = new URLSearchParams(`horizon=${h}`);
      expect(parseURL(params)).toEqual({ horizon: h });
    }
  });

  test("custom horizon reads from+to", () => {
    const params = new URLSearchParams("horizon=custom&horizon_from=2026-03-15&horizon_to=2026-04-30");
    expect(parseURL(params)).toEqual({
      horizon: "custom",
      from: "2026-03-15",
      to: "2026-04-30",
    });
  });

  test("custom with malformed dates falls back to default rather than half-state", () => {
    const params = new URLSearchParams("horizon=custom&horizon_from=2026-99-99&horizon_to=2026-04-30");
    expect(parseURL(params, { default: "next_30d" })).toEqual({ horizon: "next_30d" });
  });

  test("custom with from>=to falls back", () => {
    const params = new URLSearchParams("horizon=custom&horizon_from=2026-05-25&horizon_to=2026-05-25");
    expect(parseURL(params)).toEqual({ horizon: "any" });
  });

  test("custom URL key override", () => {
    const params = new URLSearchParams("range=past_30d");
    expect(parseURL(params, { key: "range" })).toEqual({ horizon: "past_30d" });
    expect(parseURL(params)).toEqual({ horizon: "any" }); // default `horizon` key absent
  });
});

describe("serializeURL", () => {
  test("default horizon is omitted (canonical URL stays short)", () => {
    const params = new URLSearchParams();
    serializeURL({ horizon: "any" }, params);
    expect(params.toString()).toBe("");
  });

  test("explicit default param removed when value matches default", () => {
    const params = new URLSearchParams("horizon=past_30d&other=keep");
    serializeURL({ horizon: "past_30d" }, params, { default: "past_30d" });
    expect(params.toString()).toBe("other=keep");
  });

  test("non-default horizon is written", () => {
    const params = new URLSearchParams("other=keep");
    serializeURL({ horizon: "next_7d" }, params);
    expect(params.toString()).toBe("other=keep&horizon=next_7d");
  });

  test("custom writes horizon+from+to", () => {
    const params = new URLSearchParams();
    serializeURL({ horizon: "custom", from: "2026-03-15", to: "2026-04-30" }, params);
    expect(params.toString()).toBe("horizon=custom&horizon_from=2026-03-15&horizon_to=2026-04-30");
  });

  test("custom partial bounds: from/to are written individually", () => {
    const params = new URLSearchParams();
    serializeURL({ horizon: "custom", from: "2026-03-15" }, params);
    expect(params.toString()).toBe("horizon=custom&horizon_from=2026-03-15");
  });

  test("stale params cleared on re-serialize", () => {
    const params = new URLSearchParams("horizon=custom&horizon_from=2026-03-15&horizon_to=2026-04-30&other=keep");
    serializeURL({ horizon: "past_30d" }, params);
    expect(params.toString()).toBe("other=keep&horizon=past_30d");
    // Stale from/to must be gone.
    expect(params.has("horizon_from")).toBe(false);
    expect(params.has("horizon_to")).toBe(false);
  });

  test("key override propagates to from/to", () => {
    const params = new URLSearchParams();
    serializeURL({ horizon: "custom", from: "2026-03-15", to: "2026-04-30" }, params, { key: "range" });
    expect(params.toString()).toBe("range=custom&range_from=2026-03-15&range_to=2026-04-30");
  });

  test("URL round-trips through parse → serialize → parse", () => {
    const specs: TimeSpec[] = [
      { horizon: "any" },
      { horizon: "next_7d" },
      { horizon: "past_all" },
      { horizon: "next_all" },
      { horizon: "custom", from: "2026-03-15", to: "2026-04-30" },
    ];
    for (const spec of specs) {
      const params = new URLSearchParams();
      serializeURL(spec, params);
      expect(parseURL(params)).toEqual(spec);
    }
  });
});

describe("isDefault", () => {
  test("true when horizon matches default exactly", () => {
    expect(isDefault({ horizon: "any" }, "any")).toBe(true);
    expect(isDefault({ horizon: "next_30d" }, "next_30d")).toBe(true);
  });

  test("false when horizon differs", () => {
    expect(isDefault({ horizon: "past_7d" }, "any")).toBe(false);
    expect(isDefault({ horizon: "next_30d" }, "next_7d")).toBe(false);
  });

  test("custom is never default — even when bounds match", () => {
    // No surface treats "custom" as the natural default, so any custom
    // selection IS user-driven and the closed button must surface
    // the non-default indicator.
    expect(isDefault({ horizon: "custom", from: "2026-01-01", to: "2026-12-31" }, "custom" as TimeHorizon))
      .toBe(false);
  });
});
