import { afterAll, afterEach, beforeAll, beforeEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { act, useState } from "react";
import type { Root } from "react-dom/client";
import { CalendarHeatmap } from "./CalendarHeatmap";
import { EditingWorkbench } from "./EditingWorkbench";
import { HabitColorPicker } from "./HabitColorPicker";
import { demoCalendar } from "./calendar-model";

const dom = new Window({ url: "http://localhost:3000/design-system" });
const originalGlobals = new Map<string, PropertyDescriptor | undefined>();
let createRoot: typeof import("react-dom/client").createRoot;
let root: Root;
let container: HTMLDivElement;

beforeAll(async () => {
  for (const key of ["window", "document", "navigator", "HTMLElement", "HTMLInputElement", "Element", "Node", "Event", "MouseEvent", "IS_REACT_ACT_ENVIRONMENT"]) {
    originalGlobals.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
    Object.defineProperty(globalThis, key, {
      configurable: true,
      writable: true,
      value: key === "window" ? dom : key === "IS_REACT_ACT_ENVIRONMENT" ? true : (dom as unknown as Record<string, unknown>)[key],
    });
  }
  ({ createRoot } = await import("react-dom/client"));
});

beforeEach(() => {
  container = document.createElement("div");
  document.body.append(container);
  root = createRoot(container);
});

afterEach(async () => {
  await act(async () => root.unmount());
  container.remove();
});

afterAll(() => {
  dom.happyDOM.abort();
  for (const [key, descriptor] of originalGlobals) {
    if (descriptor) Object.defineProperty(globalThis, key, descriptor);
    else Reflect.deleteProperty(globalThis, key);
  }
});

function button(text: string) {
  return [...container.querySelectorAll<HTMLButtonElement>("button")].find((element) => element.textContent?.trim() === text)!;
}

function description(element: Element) {
  return (element.getAttribute("aria-describedby") ?? "").split(" ")
    .map((id) => document.getElementById(id)?.textContent ?? "").join(" ");
}

test("calendar associates exact keyboard guidance and preserves focus when selection is vetoed", async () => {
  let attempted = "";
  await act(async () => root.render(<CalendarHeatmap days={demoCalendar(3, 8)} unit="glasses" label="Water history" selectedDate="2026-09-03" onSelectDate={(date) => { attempted = date; }} />));
  const selected = container.querySelector<HTMLButtonElement>('.ds-day[aria-pressed="true"]')!;
  selected.focus();
  expect(description(selected)).toContain("Left/right: one week");
  expect(description(selected)).not.toContain("of 8 glasses");
  await act(async () => selected.dispatchEvent(new dom.KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }) as unknown as KeyboardEvent));
  expect(attempted).toBe("2026-09-02");
  expect(document.activeElement).toBe(selected);
  expect(selected.tabIndex).toBe(0);
});

test("calendar focuses the accepted date after a controlled month window changes", async () => {
  function Calendar() {
    const [date, setDate] = useState("2026-09-01");
    return <CalendarHeatmap days={demoCalendar(3, 8)} unit="glasses" label="Water history" selectedDate={date} onSelectDate={setDate} />;
  }
  await act(async () => root.render(<Calendar />));
  await act(async () => button("3m").click());
  const selected = container.querySelector<HTMLButtonElement>('.ds-day[aria-pressed="true"]')!;
  selected.focus();
  await act(async () => selected.dispatchEvent(new dom.KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }) as unknown as KeyboardEvent));
  expect(document.activeElement?.getAttribute("aria-label")).toStartWith("2026-08-31:");
  expect(document.activeElement?.getAttribute("aria-pressed")).toBe("true");
  expect((document.activeElement as HTMLElement).tabIndex).toBe(0);
});

test("rejected calendar clicks and repeated arrows do not leave stale focus requests", async () => {
  let setExternalDate!: (date: string) => void;
  function Calendar() {
    const [date, setDate] = useState("2026-09-03");
    setExternalDate = setDate;
    return <><input aria-label="Other control" /><CalendarHeatmap days={demoCalendar(3, 8)} unit="glasses" label="Water history" selectedDate={date} onSelectDate={() => {}} /></>;
  }
  await act(async () => root.render(<Calendar />));
  const selected = container.querySelector<HTMLButtonElement>('.ds-day[aria-pressed="true"]')!;
  const rejected = container.querySelector<HTMLButtonElement>('.ds-day[aria-label^="2026-09-02:"]')!;
  rejected.focus(); // Browser pointer activation focuses the clicked button first.
  await act(async () => rejected.click());
  expect(document.activeElement).toBe(selected);
  for (let attempt = 0; attempt < 2; attempt++) {
    await act(async () => selected.dispatchEvent(new dom.KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }) as unknown as KeyboardEvent));
    expect(document.activeElement).toBe(selected);
  }
  const other = container.querySelector<HTMLInputElement>('input[aria-label="Other control"]')!;
  other.focus();
  await act(async () => setExternalDate("2026-09-02"));
  expect(document.activeElement).toBe(other);
  expect(rejected.getAttribute("aria-pressed")).toBe("true");
});

test("editor controls expose their explanatory hints", async () => {
  await act(async () => root.render(<EditingWorkbench />));
  expect(description(container.querySelector("input[readonly]")!)).toContain("Method is fixed");
  await act(async () => button("Backfill progress").click());
  expect(description(container.querySelector('input[type="date"]')!)).toContain("Select a date");
  expect(description(container.querySelector('input[type="number"]')!)).toContain("Enter the total, not an increment");
});

test("adding and removing tasks keeps focus in the task editing workflow", async () => {
  await act(async () => root.render(<EditingWorkbench />));
  await act(async () => button("Tasks").click());
  const originalCount = container.querySelectorAll(".ds-edit-task").length;
  await act(async () => button("Add task +").click());
  const added = container.querySelector<HTMLFieldSetElement>(".ds-edit-task:last-of-type")!;
  expect(container.querySelectorAll(".ds-edit-task")).toHaveLength(originalCount + 1);
  expect(added.querySelector("legend")?.textContent).toBe(`Step ${originalCount + 1}`);
  expect(document.activeElement).toBe(added.querySelector("input"));
  await act(async () => added.querySelector<HTMLButtonElement>("button")!.click());
  expect(document.activeElement).toBe(container.querySelector(".ds-edit-task:last-of-type input"));
  while (container.querySelector(".ds-edit-task")) {
    await act(async () => container.querySelector<HTMLButtonElement>(".ds-edit-task button")!.click());
  }
  expect(document.activeElement).toBe(button("Add task +"));
  expect(container.querySelector('[role="status"].ds-save-notice')?.textContent).toContain("removed from the draft");
});

test("invalid custom colors associate both guidance and corrective feedback", async () => {
  await act(async () => root.render(<HabitColorPicker value="#bad" onChange={() => {}} />));
  const input = container.querySelector('input[type="text"]')!;
  expect(input.getAttribute("aria-invalid")).toBe("true");
  expect(description(input)).toContain("Choose a suggested shade");
  expect(description(input)).toContain("Use a six-digit hex color");
});

test("unified calendar retains every date and navigates across months in every range", async () => {
  await act(async () => root.render(<CalendarHeatmap days={demoCalendar(3, 8)} unit="glasses" label="Water history" />));
  for (const months of [3, 4, 6, 12]) {
    await act(async () => button(`${months}m`).click());
    expect(container.querySelectorAll(".ds-calendar-grid")).toHaveLength(1);
    expect(container.querySelectorAll(".ds-calendar-weekdays")).toHaveLength(1);
    expect(container.querySelectorAll(".ds-calendar-month-label")).toHaveLength(months);
    const dates = [...container.querySelectorAll<HTMLButtonElement>(".ds-day")];
    expect(dates).toHaveLength({ 3: 92, 4: 122, 6: 183, 12: 365 }[months]!);
    expect(new Set(dates.map(day => day.getAttribute("aria-label"))).size).toBe(dates.length);
    expect(dates.filter(day => day.tabIndex === 0)).toHaveLength(1);
    const firstDate = new Date(`${dates[0]!.getAttribute("aria-label")!.slice(0, 10)}T00:00:00Z`);
    for (const day of dates) {
      const date = new Date(`${day.getAttribute("aria-label")!.slice(0, 10)}T12:00:00Z`);
      expect(Number(day.style.gridRow)).toBe(date.getUTCDay() + 1);
      const elapsed = Math.floor((date.getTime() - firstDate.getTime()) / 86_400_000);
      expect(Number(day.style.gridColumn)).toBe(Math.floor((elapsed + firstDate.getUTCDay()) / 7) + 1);
    }
  }
  const monthEnd = container.querySelector<HTMLButtonElement>('[aria-label^="2026-08-31:"]')!;
  await act(async () => monthEnd.click());
  await act(async () => monthEnd.dispatchEvent(new dom.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }) as unknown as KeyboardEvent));
  expect(document.activeElement?.getAttribute("aria-label")).toStartWith("2026-09-01:");
  for (const [key, date] of [["ArrowLeft", "2026-08-25"], ["End", "2026-09-30"], ["Home", "2025-10-01"]]) {
    await act(async () => document.activeElement!.dispatchEvent(new dom.KeyboardEvent("keydown", { key, bubbles: true }) as unknown as KeyboardEvent));
    expect(document.activeElement?.getAttribute("aria-label")).toStartWith(`${date}:`);
    expect(document.activeElement?.getAttribute("aria-pressed")).toBe("true");
  }
});

test("custom timeframe applies inclusive dates, supports single days and survives preset switches", async () => {
  await act(async () => root.render(<CalendarHeatmap days={demoCalendar(3, 8)} unit="glasses" label="Water history" />));
  await act(async () => button("Custom").click());
  const setDates = (from: string, to: string) => {
    container.querySelector<HTMLInputElement>('input[name="from"]')!.value = from;
    container.querySelector<HTMLInputElement>('input[name="to"]')!.value = to;
  };
  setDates("2026-08-31", "2026-09-01");
  expect(container.querySelectorAll(".ds-day")).toHaveLength(365);
  await act(async () => button("Apply range").click());
  expect(container.querySelectorAll(".ds-day")).toHaveLength(2);
  expect(container.querySelectorAll(".ds-calendar-month-label")).toHaveLength(1);
  expect(container.querySelector(".ds-calendar-range-label")?.textContent).toContain("Aug 31, 2026");
  expect(container.querySelector(".ds-calendar-caption")?.textContent).toStartWith("2 days");
  expect(container.querySelectorAll('.ds-day[tabindex="0"]')).toHaveLength(1);
  await act(async () => button("3m").click());
  expect(container.querySelector("form")).toBeNull();
  expect(container.querySelectorAll(".ds-day")).toHaveLength(92);
  await act(async () => button("Custom").click());
  expect(container.querySelectorAll(".ds-day")).toHaveLength(2);
  expect(container.querySelector<HTMLInputElement>('input[name="from"]')?.value).toBe("2026-08-31");
  setDates("2026-09-04", "2026-09-04");
  await act(async () => button("Apply range").click());
  expect(container.querySelectorAll(".ds-day")).toHaveLength(1);
  expect(container.querySelector(".ds-calendar-caption")?.textContent).toStartWith("1 day ");
  expect(container.querySelector(".ds-date-inspector")?.textContent).toContain("September 4, 2026");
});

test("custom timeframe rejects missing, reversed and unavailable dates without changing the chart", async () => {
  await act(async () => root.render(<CalendarHeatmap days={demoCalendar(3, 8)} unit="glasses" label="Water history" />));
  await act(async () => button("Custom").click());
  for (const [from, to, error] of [
    ["", "2026-09-04", "Choose both dates"],
    ["2026-09-04", "2026-08-01", "end date must be"],
    ["2025-01-01", "2026-09-04", "available history"],
  ]) {
    container.querySelector<HTMLInputElement>('input[name="from"]')!.value = from!;
    container.querySelector<HTMLInputElement>('input[name="to"]')!.value = to!;
    await act(async () => button("Apply range").click());
    expect(container.querySelector('[role="alert"]')?.textContent).toContain(error!);
    expect(container.querySelectorAll(".ds-day")).toHaveLength(365);
    expect(description(container.querySelector("input")!)).toContain(error!);
  }
});
