// Unit tests for the 12-hour-picker helpers. Covers the
// AM/PM disambiguation rules that fix the "1:15 silently saved as AM"
// bug — the picker MUST require an explicit toggle for any 1–12 hour
// without a typed AM/PM marker, and MUST accept any 13–23 hour or
// any explicit-marker shape directly. Also covers the 3-control
// `combineSplit` path that joins separate hour + minute fields.

import { test } from "node:test";
import assert from "node:assert/strict";
import {
  combineSplit,
  combineTextWithPeriod,
  formatCanonicalAs12h,
  periodFromCanonical,
  splitCanonicalForPicker,
} from "../lib/time-12h.ts";

test("periodFromCanonical: empty / unparseable returns null (no implicit default)", () => {
  assert.equal(periodFromCanonical(""), null);
  assert.equal(periodFromCanonical(null), null);
  assert.equal(periodFromCanonical(undefined), null);
  assert.equal(periodFromCanonical("not-a-time"), null);
});

test("periodFromCanonical: midnight is AM, noon is PM", () => {
  assert.equal(periodFromCanonical("00:00"), "am");
  assert.equal(periodFromCanonical("00:30"), "am");
  assert.equal(periodFromCanonical("11:59"), "am");
  assert.equal(periodFromCanonical("12:00"), "pm");
  assert.equal(periodFromCanonical("12:30"), "pm");
  assert.equal(periodFromCanonical("13:00"), "pm");
  assert.equal(periodFromCanonical("23:59"), "pm");
});

test("formatCanonicalAs12h: empty in, empty out", () => {
  assert.equal(formatCanonicalAs12h(""), "");
  assert.equal(formatCanonicalAs12h(null), "");
  assert.equal(formatCanonicalAs12h(undefined), "");
});

test("formatCanonicalAs12h: 24h hours collapse to 1..12", () => {
  assert.equal(formatCanonicalAs12h("00:30"), "12:30"); // 12:30 AM
  assert.equal(formatCanonicalAs12h("00:00"), "12:00"); // 12:00 AM (midnight)
  assert.equal(formatCanonicalAs12h("01:15"), "1:15");
  assert.equal(formatCanonicalAs12h("09:30"), "9:30");
  assert.equal(formatCanonicalAs12h("12:00"), "12:00"); // 12:00 PM (noon)
  assert.equal(formatCanonicalAs12h("12:45"), "12:45");
  assert.equal(formatCanonicalAs12h("13:30"), "1:30");
  assert.equal(formatCanonicalAs12h("23:59"), "11:59");
});

test("splitCanonicalForPicker: empty / unparseable returns blank fields", () => {
  assert.deepEqual(splitCanonicalForPicker(""), { hour: "", minute: "" });
  assert.deepEqual(splitCanonicalForPicker(null), { hour: "", minute: "" });
  assert.deepEqual(splitCanonicalForPicker(undefined), { hour: "", minute: "" });
  assert.deepEqual(splitCanonicalForPicker("not-a-time"), { hour: "", minute: "" });
});

test("splitCanonicalForPicker: seeds hour (1..12, no leading zero) and minute (zero-padded)", () => {
  assert.deepEqual(splitCanonicalForPicker("00:30"), { hour: "12", minute: "30" });
  assert.deepEqual(splitCanonicalForPicker("00:00"), { hour: "12", minute: "00" });
  assert.deepEqual(splitCanonicalForPicker("01:15"), { hour: "1", minute: "15" });
  assert.deepEqual(splitCanonicalForPicker("09:30"), { hour: "9", minute: "30" });
  assert.deepEqual(splitCanonicalForPicker("12:00"), { hour: "12", minute: "00" });
  assert.deepEqual(splitCanonicalForPicker("13:30"), { hour: "1", minute: "30" });
  assert.deepEqual(splitCanonicalForPicker("23:59"), { hour: "11", minute: "59" });
});

test("combineTextWithPeriod: empty input is { value: null, error: null }", () => {
  assert.deepEqual(combineTextWithPeriod("", null), { value: null, error: null });
  assert.deepEqual(combineTextWithPeriod("   ", "am"), { value: null, error: null });
  assert.deepEqual(combineTextWithPeriod(null, "pm"), { value: null, error: null });
});

test("combineTextWithPeriod: 1..12 hour with no toggle is ambiguous (the bug fix)", () => {
  // The exact shape that historically caused the silent "1:15 saved
  // as 01:15 AM" misinput.
  assert.deepEqual(combineTextWithPeriod("1:15", null), {
    value: null,
    error: "ambiguous",
  });
  assert.deepEqual(combineTextWithPeriod("9:30", null), {
    value: null,
    error: "ambiguous",
  });
  assert.deepEqual(combineTextWithPeriod("0930", null), {
    value: null,
    error: "ambiguous",
  });
  assert.deepEqual(combineTextWithPeriod("12:00", null), {
    value: null,
    error: "ambiguous",
  });
});

test("combineTextWithPeriod: 1..12 hour with toggle resolves correctly", () => {
  assert.deepEqual(combineTextWithPeriod("1:15", "am"), { value: "01:15", error: null });
  assert.deepEqual(combineTextWithPeriod("1:15", "pm"), { value: "13:15", error: null });
  assert.deepEqual(combineTextWithPeriod("9:30", "am"), { value: "09:30", error: null });
  assert.deepEqual(combineTextWithPeriod("9:30", "pm"), { value: "21:30", error: null });
  // Compact shapes from the task's "Done looks like" list.
  assert.deepEqual(combineTextWithPeriod("0115", "pm"), { value: "13:15", error: null });
  assert.deepEqual(combineTextWithPeriod("0930", "am"), { value: "09:30", error: null });
  // 12 AM → 00:xx, 12 PM → 12:xx.
  assert.deepEqual(combineTextWithPeriod("12:00", "am"), { value: "00:00", error: null });
  assert.deepEqual(combineTextWithPeriod("12:30", "am"), { value: "00:30", error: null });
  assert.deepEqual(combineTextWithPeriod("12:00", "pm"), { value: "12:00", error: null });
});

test("combineTextWithPeriod: 13..23 hour is unambiguous, accepted without a toggle", () => {
  assert.deepEqual(combineTextWithPeriod("13:30", null), { value: "13:30", error: null });
  assert.deepEqual(combineTextWithPeriod("14:45", null), { value: "14:45", error: null });
  assert.deepEqual(combineTextWithPeriod("23:59", null), { value: "23:59", error: null });
  // Toggle is irrelevant for 24h-only hours.
  assert.deepEqual(combineTextWithPeriod("13:30", "am"), { value: "13:30", error: null });
  assert.deepEqual(combineTextWithPeriod("13:30", "pm"), { value: "13:30", error: null });
});

test("combineTextWithPeriod: hour 0 (midnight 24h) is unambiguous, accepted without a toggle", () => {
  // Typed "00:30" / "00:00" is only producible in 24h notation
  // (12h would write midnight as "12:00 AM"), so it cannot be
  // misread as a 1–12 PM hour. Accept directly without forcing
  // the user to also tap the AM toggle.
  assert.deepEqual(combineTextWithPeriod("00:00", null), { value: "00:00", error: null });
  assert.deepEqual(combineTextWithPeriod("00:30", null), { value: "00:30", error: null });
  // Toggle is irrelevant — must not flip the value.
  assert.deepEqual(combineTextWithPeriod("00:30", "am"), { value: "00:30", error: null });
  assert.deepEqual(combineTextWithPeriod("00:30", "pm"), { value: "00:30", error: null });
});

test("combineTextWithPeriod: typed AM/PM marker overrides the toggle", () => {
  // Power-user types "9:30 PM" — the marker wins even if the toggle
  // happens to point at AM (or is unset).
  assert.deepEqual(combineTextWithPeriod("9:30 PM", null), { value: "21:30", error: null });
  assert.deepEqual(combineTextWithPeriod("9:30 PM", "am"), { value: "21:30", error: null });
  assert.deepEqual(combineTextWithPeriod("9:30 AM", "pm"), { value: "09:30", error: null });
  // Arabic markers behave the same way.
  assert.deepEqual(combineTextWithPeriod("9:30 ص", null), { value: "09:30", error: null });
  assert.deepEqual(combineTextWithPeriod("9:30 م", null), { value: "21:30", error: null });
  // Lowercase / dotted markers (parseTypedTime accepts them).
  assert.deepEqual(combineTextWithPeriod("9:30pm", null), { value: "21:30", error: null });
  assert.deepEqual(combineTextWithPeriod("9:30 p.m.", null), { value: "21:30", error: null });
});

test("combineTextWithPeriod: unparseable input is invalid regardless of toggle", () => {
  assert.deepEqual(combineTextWithPeriod("25:99", null), { value: null, error: "invalid" });
  assert.deepEqual(combineTextWithPeriod("25:99", "pm"), { value: null, error: "invalid" });
  assert.deepEqual(combineTextWithPeriod("abc", "am"), { value: null, error: "invalid" });
});

test("combineTextWithPeriod: marker in text + invalid hour is invalid", () => {
  // "13:00 PM" — parseTypedTime rejects 13 in 12h context.
  assert.deepEqual(combineTextWithPeriod("13:00 PM", null), {
    value: null,
    error: "invalid",
  });
});

// -------------------------------------------------------------------
// combineSplit — the 3-control path (hour input + minute input + AM/PM toggle)
// -------------------------------------------------------------------

test("combineSplit: both fields blank is { value: null, error: null }", () => {
  assert.deepEqual(combineSplit("", "", null), { value: null, error: null });
  assert.deepEqual(combineSplit("   ", "  ", "am"), { value: null, error: null });
  assert.deepEqual(combineSplit(null, undefined, "pm"), { value: null, error: null });
});

test("combineSplit: split-mode (hour + minute typed separately) requires period for 1..12", () => {
  // The canonical 3-control flow: hour=9, minute=30, no toggle pick
  // → ambiguous (the bug fix).
  assert.deepEqual(combineSplit("9", "30", null), { value: null, error: "ambiguous" });
  assert.deepEqual(combineSplit("1", "15", null), { value: null, error: "ambiguous" });
  assert.deepEqual(combineSplit("12", "00", null), { value: null, error: "ambiguous" });
  // With AM toggle → AM resolution.
  assert.deepEqual(combineSplit("9", "30", "am"), { value: "09:30", error: null });
  assert.deepEqual(combineSplit("1", "15", "am"), { value: "01:15", error: null });
  // With PM toggle → PM resolution.
  assert.deepEqual(combineSplit("9", "30", "pm"), { value: "21:30", error: null });
  assert.deepEqual(combineSplit("1", "15", "pm"), { value: "13:15", error: null });
  // 12 AM → 00:xx, 12 PM → 12:xx.
  assert.deepEqual(combineSplit("12", "00", "am"), { value: "00:00", error: null });
  assert.deepEqual(combineSplit("12", "30", "am"), { value: "00:30", error: null });
  assert.deepEqual(combineSplit("12", "00", "pm"), { value: "12:00", error: null });
});

test("combineSplit: split-mode 13..23 hour is unambiguous, accepted without a toggle", () => {
  // User who knows the 24h time of their meeting can type "13" + "30"
  // and save without also tapping PM — same convenience as the
  // single-text path.
  assert.deepEqual(combineSplit("13", "30", null), { value: "13:30", error: null });
  assert.deepEqual(combineSplit("23", "59", null), { value: "23:59", error: null });
  // Toggle is irrelevant for 24h-only hours.
  assert.deepEqual(combineSplit("13", "30", "am"), { value: "13:30", error: null });
});

test("combineSplit: split-mode hour 0 (midnight) is unambiguous, accepted without a toggle", () => {
  assert.deepEqual(combineSplit("00", "30", null), { value: "00:30", error: null });
  assert.deepEqual(combineSplit("00", "00", null), { value: "00:00", error: null });
});

test("combineSplit: hour-field power-user shortcut (whole time in hour input)", () => {
  // Hour field contains a separator → parsed standalone, minute
  // field ignored. This preserves the e2e tests that fill the
  // entire time string into the hour input.
  assert.deepEqual(combineSplit("13:30", "", null), { value: "13:30", error: null });
  assert.deepEqual(combineSplit("9:30", "", "pm"), { value: "21:30", error: null });
  // Marker wins over toggle.
  assert.deepEqual(combineSplit("9:30 PM", "", null), { value: "21:30", error: null });
  assert.deepEqual(combineSplit("9:30 AM", "", "pm"), { value: "09:30", error: null });
  // Compact 4-digit form.
  assert.deepEqual(combineSplit("0930", "", "am"), { value: "09:30", error: null });
  assert.deepEqual(combineSplit("0115", "", "pm"), { value: "13:15", error: null });
  // Compact 3-digit form ("930" → 9:30).
  assert.deepEqual(combineSplit("930", "", "am"), { value: "09:30", error: null });
  // Arabic-Indic compact form.
  assert.deepEqual(combineSplit("٠٩٣٠", "", "am"), { value: "09:30", error: null });
  // Even when the minute field IS filled, the hour-field power-user
  // shortcut wins (separator in hour signals "I typed the whole time
  // here, ignore the minute field").
  assert.deepEqual(combineSplit("13:30", "99", null), { value: "13:30", error: null });
});

test("combineSplit: bare hour with no minute is invalid", () => {
  // User typed an hour but never filled the minute — surface as
  // invalid so the picker re-focuses the minute input rather than
  // saving a malformed value.
  assert.deepEqual(combineSplit("9", "", "am"), { value: null, error: "invalid" });
  assert.deepEqual(combineSplit("12", "", "pm"), { value: null, error: "invalid" });
});

test("combineSplit: minute-only with no hour is invalid", () => {
  assert.deepEqual(combineSplit("", "30", "am"), { value: null, error: "invalid" });
});

test("combineSplit: invalid numeric values are invalid", () => {
  assert.deepEqual(combineSplit("25", "99", null), { value: null, error: "invalid" });
  assert.deepEqual(combineSplit("abc", "30", "am"), { value: null, error: "invalid" });
  assert.deepEqual(combineSplit("9", "60", "am"), { value: null, error: "invalid" });
});
