import { ReminderSettings } from "./components/ReminderSettings";
import { Modal } from "./components/design-system/Modal";
import {
  afterAll,
  afterEach,
  beforeAll,
  beforeEach,
  describe,
  expect,
  spyOn,
  test,
} from "bun:test";
import { Window } from "happy-dom";
import { act, StrictMode, useState } from "react";
import { MemoryRouter, useLocation, useNavigate } from "react-router";
import type { Root } from "react-dom/client";
import { App } from "./App";
import { CalendarHeatmap } from "./components/design-system/CalendarHeatmap";
import { fixture } from "./habits/test-fixture";

// Keep the design system independent of authentication and the local database.
const dom = new Window({ url: "http://localhost:3000/" });
const originalGlobals = new Map<string, PropertyDescriptor | undefined>();
let createRoot: typeof import("react-dom/client").createRoot;
let root: Root;
let container: HTMLDivElement;
let fetchMock: ReturnType<typeof spyOn<typeof globalThis, "fetch">>;
let apiFixture: ReturnType<typeof fixture> | undefined;

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);
  fetchMock = spyOn(globalThis, "fetch").mockResolvedValue(
    new Response(null, { status: 401 }),
  );
});

afterEach(async () => {
  await act(async () => root.unmount());
  container.remove();
  fetchMock.mockRestore();
  apiFixture?.close();
  apiFixture = undefined;
});

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

function LocationProbe() {
  const location = useLocation();
  const navigate = useNavigate();
  return <>
    <output data-testid="pathname">{location.pathname}</output>
    <output data-testid="hash">{location.hash}</output>
    <button onClick={() => navigate(-1)}>History back</button>
    <button onClick={() => navigate(1)}>History forward</button>
  </>;
}

async function render(path = "/design-system") {
  await act(async () =>
    root.render(
      <MemoryRouter initialEntries={[path]}>
        <App />
        <LocationProbe />
      </MemoryRouter>,
    ),
  );
}

function connectAccount() {
  apiFixture = fixture();
  const f = apiFixture;
  fetchMock.mockImplementation((async (input, init) => {
    const headers = new Headers(init?.headers);
    headers.set("Cookie", `minabot_session=${"a".repeat(43)}`);
    headers.set("Origin", f.origin);
    return f.app.request(new Request(new URL(String(input), f.origin), { ...init, headers }));
  }) as typeof fetch);
  return f;
}

const buttonNamed = (name: string) => [...container.querySelectorAll<HTMLButtonElement>("button")]
  .find(button => button.textContent === name || button.getAttribute("aria-label") === name)!;

describe("homepage", () => {
  test("development Strict Mode does not restore background focus while a modal is open", async () => {
    await act(async () => root.render(<StrictMode><Modal id="strict-modal" title="Editor" onClose={() => {}} initialFocus="input"><input aria-label="Draft" /></Modal></StrictMode>));
    await new Promise(resolve => dom.requestAnimationFrame(() => dom.requestAnimationFrame(resolve)));
    expect(document.activeElement).toBe(container.querySelector("input"));
    expect(container.querySelector<HTMLDialogElement>("dialog")?.open).toBe(true);
    expect(document.documentElement.style.overflow).toBe("hidden");
  });
  test("settings open as a modal and Escape restores the opener and scrolling", async () => {
    connectAccount();
    await render("/");
    const opener = buttonNamed("Settings");
    opener.focus();
    await act(async () => opener.click());
    const dialog = container.querySelector<HTMLDialogElement>("dialog#account-settings")!;
    expect(dialog.open).toBe(true);
    expect(dialog.contains(document.activeElement)).toBe(true);
    expect(document.documentElement.style.overflow).toBe("hidden");
    await act(async () => dialog.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event));
    await new Promise(resolve => dom.requestAnimationFrame(resolve));
    expect(container.querySelector("dialog")).toBeNull();
    expect(document.activeElement).toBe(opener);
    expect(document.documentElement.style.overflow).toBe("");
  });

  test("modal dismissal waits for saves and nested dialogs retain the scroll lock", async () => {
    function Harness() {
      const [nested, setNested] = useState(true);
      const [busy, setBusy] = useState(true);
      return <Modal id="outer" title="Outer" onClose={() => {}}>
        {nested && <Modal id="inner" title="Inner" busy={busy} onClose={() => setNested(false)}>
          <button onClick={() => setBusy(false)}>Finish save</button>
        </Modal>}
      </Modal>;
    }
    await act(async () => root.render(<Harness />));
    const inner = container.querySelector<HTMLDialogElement>("#inner")!;
    await act(async () => inner.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event));
    expect(inner.open).toBe(true);
    expect(inner.querySelector<HTMLButtonElement>('[aria-label="Close dialog"]')?.disabled).toBe(true);
    await act(async () => buttonNamed("Finish save").click());
    await act(async () => inner.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event));
    expect(container.querySelector("#inner")).toBeNull();
    expect(container.querySelector<HTMLDialogElement>("#outer")?.open).toBe(true);
    expect(document.documentElement.style.overflow).toBe("hidden");
    await act(async () => root.render(null));
    expect(document.documentElement.style.overflow).toBe("");
  });

  test("archived habits expose history and restore to the dashboard", async () => {
    const f = connectAccount();
    f.setTime("2026-09-03T12:00:00Z");
    const h = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201);
    await f.json(`/habits/${h.id}/days/2026-09-03/progress`, "PUT", { done: true });
    f.setTime("2026-09-04T12:00:00Z");
    await f.request(`/habits/${h.id}`, "DELETE");
    await render("/");
    await act(async () => buttonNamed("Archived habits").click());
    expect(buttonNamed("Restore Reading")).toBeDefined();
    await act(async () => buttonNamed("View history").click());
    expect(container.querySelector('[aria-label="2026-09-03: 1 of 1 completion · Complete"]')).not.toBeNull();
    await act(async () => buttonNamed("Restore Reading").click());
    expect(container.textContent).toContain("No archived habits.");
    expect((await f.json("/today")).habits[0].name).toBe("Reading");
    expect((await f.json(`/habits/${h.id}/days/2026-09-03`)).complete).toBe(true);
  });

  test("historical check-ins use the selected date's tracking method and preserve today", async () => {
    const f = connectAccount();
    f.setTime("2026-09-03T12:00:00Z");
    const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201);
    f.setTime("2026-09-04T12:00:00Z");
    await f.json(`/habits/${habit.id}`, "PATCH", { method: "count", target: 10, unit: "pages" });
    await render("/");
    expect(container.querySelector(".ds-calendar")).toBeNull();
    expect(buttonNamed("View history for Reading").getAttribute("aria-expanded")).toBe("false");
    await act(async () => buttonNamed("View history for Reading").click());
    expect(buttonNamed("Hide history for Reading").getAttribute("aria-expanded")).toBe("true");
    await act(async () => container.querySelector<HTMLButtonElement>('[aria-label^="2026-09-03:"]')!.click());
    expect(buttonNamed("Edit selected check-in").closest(".ds-date-inspector")?.textContent).toContain("September 3, 2026");
    await act(async () => buttonNamed("Edit selected check-in").click());
    const past = [...container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')].find(input => input.closest("label")?.textContent === "Completed on this date")!;
    expect(past).toBeDefined();
    await act(async () => past.click());
    expect((await f.json(`/habits/${habit.id}/days/2026-09-03`)).complete).toBe(true);
    expect((await f.json(`/habits/${habit.id}/days/2026-09-04`)).value).toBe(0);
    expect(container.textContent).toContain("Saved check-in for 2026-09-03");
  });

  test("signed-out visitors can try progress without calling private habit APIs", async () => {
    await render("/");
    expect(container.querySelector("h1")?.textContent).toBe("Small steps.Lasting rhythm.");
    expect(fetchMock.mock.calls.map(call => call[0])).toEqual(["/api/me"]);
    await act(async () => buttonNamed("Increase glasses of water").click());
    expect(container.querySelector<HTMLInputElement>('[aria-label="Total glasses of water"]')?.value).toBe("4");
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  test("OAuth errors are retained at the homepage and unknown routes go home", async () => {
    await render("/?auth_error=denied");
    expect(container.querySelector('[role="alert"]')?.textContent).toContain("cancelled");
    // A new router is needed when changing initialEntries after mount.
    await act(async () => root.unmount());
    root = createRoot(container);
    await render("/missing");
    expect(container.querySelector('[data-testid="pathname"]')?.textContent).toBe("/");
    expect(container.querySelector("#welcome-title")).not.toBeNull();
  });

  test("does not flash the signed-out page while the account is loading", async () => {
    let resolve!: (response: Response) => void;
    fetchMock.mockImplementation(Object.assign(() => new Promise<Response>(done => { resolve = done; }), { preconnect: fetch.preconnect }));
    await render("/");
    expect(container.textContent).toContain("Getting things ready");
    expect(container.querySelector("#welcome-title")).toBeNull();
    await act(async () => resolve(new Response(null, { status: 401 })));
    expect(container.querySelector("#welcome-title")).not.toBeNull();
  });

  test("account failures offer retry without pretending the user is signed out", async () => {
    fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }));
    await render("/");
    expect(container.textContent).toContain("Account unavailable");
    expect(container.querySelector("#welcome-title")).toBeNull();
    await act(async () => buttonNamed("Try again").click());
    expect(container.querySelector("#welcome-title")).not.toBeNull();
  });

  test("signed-in users create a habit from the empty state and sign out", async () => {
    const f = connectAccount();
    await render("/");
    expect(container.textContent).toContain("Welcome back, alice.");
    expect(container.textContent).toContain("Start with one habit.");
    await act(async () => buttonNamed("Add a habit +").click());
    const input = container.querySelector<HTMLInputElement>("form input")!;
    expect(document.activeElement).toBe(input);
    await act(async () => {
      Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Read a little");
      input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event);
    });
    await act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
    expect((await f.json("/habits")).habits[0].name).toBe("Read a little");
    expect(container.querySelector("form")).toBeNull();
    expect(container.textContent).toContain("Read a little created.");
    expect(container.querySelector(".ds-habit-chart")).not.toBeNull();
    await act(async () => buttonNamed("Sign out").click());
    expect(container.querySelector("#welcome-title")).not.toBeNull();
    expect((await f.request("/me")).status).toBe(401);
  });

  test("count, manual, and task progress persist and update today's completion", async () => {
    const f = connectAccount();
    const water = await f.json("/habits", "POST", { name: "Water", method: "count", target: 8, unit: "glasses" }, 201);
    await f.json(`/habits/${water.id}/days/2026-09-04/progress`, "PUT", { count: 7 });
    await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201);
    await f.json("/habits", "POST", { name: "Evening", method: "tasks", tasks: [{ name: "Stretch" }] }, 201);
    await f.json("/habits", "POST", { name: "Sunday walk", method: "manual", schedule: { type: "weekdays", days: [0] } }, 201);
    await render("/");
    expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("0 / 3");
    await act(async () => buttonNamed("Increase Water").click());
    expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("1 / 3");
    expect(container.querySelector(`#history-${water.id} .ds-date-inspector`)).toBeNull();
    await act(async () => buttonNamed("View history for Water").click());
    expect(container.querySelector(`#history-${water.id} .ds-date-inspector`)?.textContent).toContain("8 of 8 glasses");
    expect(container.textContent).not.toContain("Sunday walk");
    await act(async () => buttonNamed("All habits · 4").click());
    const checkbox = (name: string) => [...container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')].find(input => (input.getAttribute("aria-label") || input.closest("label")?.textContent) === name)!;
    expect(checkbox("Sunday walk done").disabled).toBe(true);
    await act(async () => checkbox("Reading done").click());
    await act(async () => checkbox("Stretch").click());
    expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("3 / 3");
    expect((await f.json("/today")).completed).toBe(3);
    await act(async () => buttonNamed("Decrease Water").click());
    expect((await f.json("/today")).completed).toBe(2);
  });

  test("inline habit edits persist configuration and delete archives without losing earlier history", async () => {
    const f = connectAccount();
    f.setTime("2026-09-03T12:00:00Z");
    const habit = await f.json("/habits", "POST", { name: "Water", method: "count", target: 8, unit: "glasses" }, 201);
    await f.json(`/habits/${habit.id}/days/2026-09-03/progress`, "PUT", { count: 5 });
    f.setTime("2026-09-04T12:00:00Z");
    await render("/");
    await act(async () => buttonNamed("Edit habit Water").click());
    const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit habit Water"]')!;
    expect(document.activeElement).toBe(form.querySelector("input"));
    await act(async () => {
      const name = form.querySelector<HTMLInputElement>("input")!;
      Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(name, "Daily water");
      name.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event);
      const target = form.querySelector<HTMLInputElement>('input[type="number"]')!;
      Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(target, "10");
      target.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event);
    });
    await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
    const saved = await f.json(`/habits/${habit.id}`);
    expect(saved.name).toBe("Daily water");
    expect(saved.target).toBe(10);
    expect(container.querySelector("form")).toBeNull();
    expect(container.querySelector("h3")?.textContent).toContain("Daily water");
    await act(async () => buttonNamed("Archive habit Daily water").click());
    expect((await f.json("/habits")).habits).toHaveLength(1);
    await act(async () => buttonNamed("Cancel").click());
    expect((await f.json("/habits")).habits).toHaveLength(1);
    await act(async () => buttonNamed("Archive habit Daily water").click());
    await act(async () => buttonNamed("Archive habit").click());
    expect((await f.json("/habits")).habits).toHaveLength(0);
    expect(container.querySelector(".ds-habit-chart")).toBeNull();
    const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`);
    expect(previous.name).toBe("Water");
    expect(previous.target).toBe(8);
    expect(previous.value).toBe(5);
  });

  test("inline task rename keeps its completion and schedule; deletion updates totals and preserves yesterday", async () => {
    const f = connectAccount();
    f.setTime("2026-09-03T12:00:00Z");
    const habit = await f.json("/habits", "POST", { name: "Evening", method: "tasks", tasks: [{ name: "Stretch", schedule: { type: "weekdays", days: [4, 5] } }, { name: "Clear desk" }] }, 201);
    const task = habit.tasks[0];
    await f.json(`/habits/${habit.id}/days/2026-09-03/tasks/${task.id}`, "PUT", { done: true });
    f.setTime("2026-09-04T12:00:00Z");
    await f.json(`/habits/${habit.id}/days/2026-09-04/tasks/${task.id}`, "PUT", { done: true });
    await render("/");
    await act(async () => buttonNamed("Edit task Stretch").click());
    const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Stretch"]')!;
    await act(async () => {
      const input = form.querySelector("input")!;
      Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Stretch gently");
      input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event);
    });
    await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
    const saved = await f.json(`/habits/${habit.id}/tasks/${task.id}`);
    expect(saved.name).toBe("Stretch gently");
    expect(saved.schedule).toEqual({ type: "weekdays", days: [4, 5] });
    expect((await f.json("/today")).habits[0].value).toBe(1);
    await act(async () => buttonNamed("Delete task Stretch gently").click());
    await act(async () => buttonNamed("Delete task").click());
    const today = (await f.json("/today")).habits[0];
    expect(today.target).toBe(1);
    expect(today.value).toBe(0);
    expect(container.textContent).not.toContain("Stretch gently");
    const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`);
    expect(previous.tasks.find((item: { taskId: string }) => item.taskId === task.id).done).toBe(true);
    expect(previous.tasks.find((item: { taskId: string }) => item.taskId === task.id).name).toBe("Stretch");
  });

  test("task edits retain failed drafts for retry and remain available on days off", async () => {
    const f = connectAccount();
    const habit = await f.json("/habits", "POST", { name: "Sunday", method: "tasks", schedule: { type: "weekdays", days: [0] }, tasks: [{ name: "Walk" }] }, 201);
    await render("/");
    expect(container.querySelector(".ds-habit-chart")).toBeNull();
    await act(async () => buttonNamed("All habits · 1").click());
    expect(container.textContent).toContain("Not scheduled today");
    expect(container.querySelector<HTMLInputElement>('.ds-editable-task input[type="checkbox"]')?.disabled).toBe(true);
    await act(async () => buttonNamed("Edit task Walk").click());
    const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!;
    await act(async () => {
      const input = form.querySelector("input")!;
      Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Long walk");
      input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event);
    });
    fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: "Please try again" }), { status: 500 }));
    await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
    expect(form.querySelector("input")?.value).toBe("Long walk");
    expect(form.querySelector('[role="alert"]')?.textContent).toBe("Please try again");
    expect((await f.json(`/habits/${habit.id}/tasks`)).tasks[0].name).toBe("Walk");
    await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
    expect((await f.json(`/habits/${habit.id}/tasks`)).tasks[0].name).toBe("Long walk");
    expect(container.querySelector("form")).toBeNull();
  });

  test("task recurrence edits round-trip every schedule type and preserve earlier check-ins", async () => {
    const f = connectAccount();
    f.setTime("2026-09-03T12:00:00Z");
    const habit = await f.json("/habits", "POST", { name: "Routine", method: "tasks", tasks: [{ name: "Walk" }] }, 201);
    const id = habit.tasks[0].id;
    await f.json(`/habits/${habit.id}/days/2026-09-03/tasks/${id}`, "PUT", { done: true });
    f.setTime("2026-09-04T12:00:00Z");
    await render("/");
    const inputValue = async (input: HTMLInputElement, value: string) => act(async () => {
      Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, value);
      input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event);
    });
    const selectValue = async (select: HTMLSelectElement, value: string) => act(async () => {
      select.value = value;
      select.dispatchEvent(new dom.Event("change", { bubbles: true }) as unknown as Event);
    });
    for (const schedule of [
      { type: "interval", every: 3, anchor: "2026-09-05" },
      { type: "weekly", every: 2, anchor: "2026-09-04", weekday: 5 },
      { type: "weekdays", days: [1, 2, 3, 4, 5] },
      { type: "daily" },
    ] as const) {
      await act(async () => buttonNamed("Edit task Walk").click());
      const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!;
      await selectValue(form.querySelector("select")!, schedule.type);
      if (schedule.type === "interval" || schedule.type === "weekly") {
        await inputValue(form.querySelector('input[type="number"]')!, String(schedule.every));
        await inputValue(form.querySelector('input[type="date"]')!, schedule.anchor);
      }
      if (schedule.type === "weekly") await selectValue(form.querySelectorAll("select")[1]!, String(schedule.weekday));
      await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
      expect((await f.json(`/habits/${habit.id}/tasks/${id}`)).schedule).toEqual(schedule);
      if (schedule.type === "interval") expect(buttonNamed("All habits · 1").getAttribute("aria-pressed")).toBe("true");
      expect(container.querySelector("form")).toBeNull();
      await act(async () => buttonNamed("Edit task Walk").click());
      const reopened = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!;
      expect(reopened.querySelector("select")?.value).toBe(schedule.type);
      if (schedule.type === "interval" || schedule.type === "weekly") {
        expect(reopened.querySelector<HTMLInputElement>('input[type="number"]')?.value).toBe(String(schedule.every));
        expect(reopened.querySelector<HTMLInputElement>('input[type="date"]')?.value).toBe(schedule.anchor);
      }
      await act(async () => buttonNamed("Cancel").click());
    }
    const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`);
    expect(previous.tasks[0].done).toBe(true);
    expect(previous.requirements.tasks[0].schedule).toEqual({ type: "daily" });
  });

  test("habit method and carryover controls save, and task conversion supports adding recurring tasks", async () => {
    const f = connectAccount();
    const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201);
    await render("/");
    const selectMethod = async (method: string) => {
      const select = container.querySelector<HTMLSelectElement>('form select')!;
      await act(async () => {
        select.value = method;
        select.dispatchEvent(new dom.Event("change", { bubbles: true }) as unknown as Event);
      });
    };
    const submit = async () => act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
    await act(async () => buttonNamed("Edit habit Reading").click());
    await selectMethod("count");
    await act(async () => container.querySelector<HTMLInputElement>('form input[type="checkbox"]')!.click());
    await submit();
    expect((await f.json(`/habits/${habit.id}`)).method).toBe("count");
    expect((await f.json(`/habits/${habit.id}`)).carryPartialProgress).toBe(true);
    await act(async () => buttonNamed("Edit habit Reading").click());
    expect(container.querySelector<HTMLInputElement>('form input[type="checkbox"]')?.checked).toBe(true);
    await selectMethod("tasks");
    await submit();
    expect((await f.json(`/habits/${habit.id}`)).method).toBe("tasks");
    await act(async () => buttonNamed("Add task +").click());
    const form = container.querySelector<HTMLFormElement>('form[aria-label="Add task"]')!;
    await act(async () => {
      const input = form.querySelector("input")!;
      Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Read chapter");
      input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event);
    });
    await submit();
    const task = (await f.json(`/habits/${habit.id}/tasks`)).tasks[0];
    expect(task.name).toBe("Read chapter");
    expect(task.schedule).toEqual({ type: "daily" });
    expect(buttonNamed("Edit task Read chapter")).toBeDefined();
    expect((await f.json("/today")).habits[0].target).toBe(1);
  });

  test("a successful delete is not retried when the subsequent refresh fails", async () => {
    const f = connectAccount();
    const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201);
    await render("/");
    await act(async () => buttonNamed("Archive habit Reading").click());
    fetchMock.mockImplementationOnce((async (input, init) => {
      return f.request(String(input).replace("/api", ""), init?.method);
    }) as typeof fetch);
    fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }));
    await act(async () => buttonNamed("Archive habit").click());
    expect(container.querySelector("form")).toBeNull();
    expect(container.textContent).toContain("Your change was saved, but the dashboard could not refresh");
    expect(buttonNamed("Archive habit Reading").disabled).toBe(true);
    expect((await f.json(`/habits/${habit.id}`)).archived).toBe(true);
    await act(async () => buttonNamed("Try again").click());
    expect(container.querySelector(".ds-habit-chart")).toBeNull();
    expect(fetchMock.mock.calls.filter(call => call[1]?.method === "DELETE")).toHaveLength(1);
  });

  test("failed saves retain recorded progress and allow another attempt", async () => {
    const f = connectAccount();
    await f.json("/habits", "POST", { name: "Water", method: "count", target: 8 }, 201);
    await render("/");
    fetchMock.mockResolvedValueOnce(Response.json({ error: "Save unavailable" }, { status: 500 }));
    await act(async () => buttonNamed("Increase Water").click());
    expect(container.querySelector('[role="alert"]')?.textContent).toContain("Save unavailable");
    expect(container.querySelector<HTMLInputElement>('[aria-label="Total Water"]')?.value).toBe("0");
    expect(buttonNamed("Increase Water").disabled).toBe(false);
    await act(async () => buttonNamed("Increase Water").click());
    expect(container.querySelector<HTMLInputElement>('[aria-label="Total Water"]')?.value).toBe("1");
    expect((await f.json("/today")).habits[0].value).toBe(1);
  });
});

test("unscheduled dates stay inspectable without a progress-square fill", async () => {
  await act(async () =>
    root.render(
      <CalendarHeatmap
        label="Reading history"
        unit="pages"
        compact
        emptyColor="#ebedf0"
        days={[
          {
            date: "2026-09-03",
            value: 0,
            target: 0,
            state: "not-due",
            color: "#f5f5f5",
          },
          {
            date: "2026-09-04",
            value: 0,
            target: 20,
            state: "due",
            color: "#ebedf0",
          },
          {
            date: "2026-09-05",
            value: 0,
            target: 20,
            state: "future",
            color: "#dbeafe",
          },
        ]}
      />,
    ),
  );
  const restDay =
    container.querySelector<HTMLButtonElement>(".ds-day--not-due")!;
  expect(restDay.style.backgroundColor).toBe("transparent");
  expect(restDay.getAttribute("aria-label")).toContain(
    "Not included in the score",
  );
  expect(
    container.querySelector<HTMLElement>(".ds-day--due")!.style.backgroundColor,
  ).toBe("#ebedf0");
  expect(
    container.querySelector<HTMLElement>(".ds-day--future")!.style
      .backgroundColor,
  ).toBe("#ebedf0");
  expect(container.querySelector(".ds-legend-swatch--not-due")).not.toBeNull();
  await act(async () => restDay.click());
  expect(restDay.getAttribute("aria-pressed")).toBe("true");
  expect(restDay.tabIndex).toBe(0);
  expect(container.querySelector(".ds-date-inspector")?.textContent).toContain(
    "Nothing scheduled",
  );
  await act(async () =>
    restDay.dispatchEvent(
      new dom.KeyboardEvent("keydown", {
        key: "ArrowDown",
        bubbles: true,
      }) as unknown as KeyboardEvent,
    ),
  );
  expect(
    container.querySelector(".ds-day--due")?.getAttribute("aria-pressed"),
  ).toBe("true");
});

describe("independent design-system routing", () => {
  for (const path of ["/design-system", "/design-system/"]) {
    test(`renders only the public design system at ${path}`, async () => {
      await render(path);
      expect(container.querySelector("#ds-title")).not.toBeNull();
      expect(container.querySelectorAll("main")).toHaveLength(1);
      expect(["/design-system", "/design-system/"]).toContain(container.querySelector('[data-testid="pathname"]')!.textContent!);
      expect(container.querySelector(".landing-root")).toBeNull();
      expect(container.querySelector(".home-root")).toBeNull();
      expect(fetchMock).not.toHaveBeenCalled();
      expect(container.querySelectorAll('[role="tablist"]')).toHaveLength(1);
      expect(container.querySelectorAll('[role="tab"]')).toHaveLength(4);
      expect(container.querySelectorAll('[role="tabpanel"]:not([hidden])')).toHaveLength(1);
      for (const tab of container.querySelectorAll('[role="tab"]')) {
        const panel = document.getElementById(tab.getAttribute("aria-controls")!);
        expect(panel?.getAttribute("aria-labelledby")).toBe(tab.id);
      }
    });
  }

  test("calendar examples update individual and combined progress without API requests", async () => {
    await render("/design-system#calendar-states");
    const calendars = container.querySelectorAll<HTMLElement>("#ds-panel-calendar-states .ds-calendar");
    expect(calendars).toHaveLength(3);
    const inspected = (index: number) => calendars[index]!.querySelector(".ds-date-inspector")!.textContent!;
    expect(inspected(0)).toContain("7 of 8 glasses");
    expect(inspected(2)).toContain("1 of 2 habits complete");
    await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Increase glasses of water"]')!.click());
    expect(inspected(0)).toContain("8 of 8 glasses");
    expect(inspected(2)).toContain("2 of 2 habits complete");
    const reading = container.querySelector<HTMLInputElement>('#ds-panel-calendar-states input[type="checkbox"]')!;
    await act(async () => reading.click());
    expect(inspected(1)).toContain("0 of 1 reading session");
    expect(inspected(2)).toContain("1 of 2 habits complete");
    const reset = [...container.querySelectorAll<HTMLButtonElement>('#ds-panel-calendar-states button')].find(button => button.textContent === "Reset examples ↺")!;
    await act(async () => reset.click());
    expect(inspected(0)).toContain("7 of 8 glasses");
    expect(reading.checked).toBe(true);
    expect(fetchMock).not.toHaveBeenCalled();
  });
});

describe("design system tabs", () => {
  const tab = (id: string) => container.querySelector<HTMLButtonElement>(`#ds-tab-${id}`)!;
  const panel = () => container.querySelector<HTMLElement>('[role="tabpanel"]:not([hidden])')!;
  const clickTab = async (id: string) => { await act(async () => tab(id).click()); };
  const press = async (id: string, key: string) => {
    await act(async () => tab(id).dispatchEvent(
      new dom.KeyboardEvent("keydown", { key, bubbles: true }) as unknown as KeyboardEvent,
    ));
  };

  test("defaults to foundations and each tab exposes only its own panel", async () => {
    await render();
    expect(panel().id).toBe("ds-panel-foundations");
    for (const id of ["components", "containers", "calendar-states", "foundations"]) {
      await clickTab(id);
      expect(panel().id).toBe(`ds-panel-${id}`);
      expect(container.querySelectorAll('[role="tabpanel"]:not([hidden])')).toHaveLength(1);
      expect(tab(id).getAttribute("aria-selected")).toBe("true");
      expect(container.querySelectorAll('[role="tab"][tabindex="0"]')).toHaveLength(1);
      expect(container.querySelector('[data-testid="hash"]')?.textContent).toBe(`#${id}`);
    }
    expect(fetchMock).not.toHaveBeenCalled();
  });

  test("supports arrow keys, Home, End, and wraparound with focus", async () => {
    await render();
    await press("foundations", "ArrowLeft");
    expect(document.activeElement).toBe(tab("calendar-states"));
    expect(panel().id).toBe("ds-panel-calendar-states");
    await press("calendar-states", "ArrowRight");
    expect(document.activeElement).toBe(tab("foundations"));
    await press("foundations", "End");
    expect(document.activeElement).toBe(tab("calendar-states"));
    await press("calendar-states", "Home");
    expect(document.activeElement).toBe(tab("foundations"));
    expect(panel().id).toBe("ds-panel-foundations");
  });

  test("honors deep links and browser history", async () => {
    await render("/design-system#calendar-states");
    expect(panel().id).toBe("ds-panel-calendar-states");
    await clickTab("containers");
    const historyButton = (label: string) => Array.from(container.querySelectorAll("button")).find(button => button.textContent === label)!;
    await act(async () => historyButton("History back").click());
    expect(panel().id).toBe("ds-panel-calendar-states");
    await act(async () => historyButton("History forward").click());
    expect(panel().id).toBe("ds-panel-containers");
  });

  test("keeps example state and opens Calendars from View habit", async () => {
    await render("/design-system#components");
    await act(async () => panel().querySelector<HTMLButtonElement>('[aria-label="Increase example count"]')!.click());
    await clickTab("calendar-states");
    await clickTab("components");
    expect(panel().querySelector<HTMLInputElement>('[aria-label="Total example count"]')?.value).toBe("4");
    const viewHabit = Array.from(panel().querySelectorAll("button")).find(button => button.textContent?.startsWith("View habit"))!;
    await act(async () => viewHabit.click());
    expect(panel().id).toBe("ds-panel-calendar-states");
    expect(panel().querySelectorAll(".ds-calendar")).toHaveLength(3);
    expect(document.activeElement).toBe(tab("calendar-states"));
  });

  test("retains child component state and skip link does not switch tabs", async () => {
    await render("/design-system#containers");
    const reading = panel().querySelector<HTMLInputElement>('input[type="checkbox"]')!;
    await act(async () => reading.click());
    expect(reading.checked).toBe(true);
    await clickTab("foundations");
    await clickTab("containers");
    expect(reading.checked).toBe(true);
    await act(async () => container.querySelector<HTMLAnchorElement>(".ds-skip-link")!.click());
    expect(panel().id).toBe("ds-panel-containers");
    expect(document.activeElement?.id).toBe("ds-main");
  });

  for (const [retired, destination] of [["playground", "calendar-states"], ["editing", "foundations"], ["views", "foundations"]]) {
    test(`retired #${retired} links resolve to #${destination}`, async () => {
      await render(`/design-system#${retired}`);
      expect(panel().id).toBe(`ds-panel-${destination}`);
      expect(container.querySelector('[data-testid="hash"]')?.textContent).toBe(`#${destination}`);
      expect([...container.querySelectorAll('[role="tab"]')].map(item => item.textContent)).toEqual(["Foundations", "Components", "Layout", "Calendars"]);
      for (const id of ["playground", "editing", "views"]) {
        expect(container.querySelector(`#${id}`)).toBeNull();
        expect(container.querySelector(`#ds-panel-${id}`)).toBeNull();
      }
    });
  }

  test("an unknown section safely falls back to foundations", async () => {
    await render("/design-system#unknown");
    expect(panel().id).toBe("ds-panel-foundations");
  });

  test("mounted panels keep unique IDs and valid accessible references", async () => {
    await render();
    const ids = Array.from(container.querySelectorAll("[id]"), element => element.id);
    expect(new Set(ids).size).toBe(ids.length);
    for (const element of container.querySelectorAll("[aria-labelledby], [aria-describedby], [aria-controls], label[for]")) {
      for (const attribute of ["aria-labelledby", "aria-describedby", "aria-controls", "for"]) {
        for (const id of element.getAttribute(attribute)?.split(/\s+/).filter(Boolean) ?? []) {
          expect(document.getElementById(id)).not.toBeNull();
        }
      }
    }
    expect(fetchMock).not.toHaveBeenCalled();
  });
});


test("reminder form submits native time values and explicit opt-out", async () => {
  const saved: unknown[] = [];
  fetchMock.mockImplementation((async (_input, init) => {
    if (init?.method === "PUT") { saved.push(JSON.parse(String(init.body))); return Response.json({}); }
    return Response.json({ enabled: false, time: "20:00", quietStart: "22:00", quietEnd: "08:00", available: true, lastDelivery: null });
  }) as typeof fetch);
  await act(async () => root.render(<ReminderSettings timezone="Europe/Belgrade" />));
  const time = container.querySelector<HTMLInputElement>('input[name="time"]')!;
  await act(async () => {
    time.value = "14:30";
    container.querySelector<HTMLInputElement>('input[type="checkbox"]')!.click();
  });
  await act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
  expect(saved[0]).toEqual({ enabled: true, time: "14:30", quietStart: "22:00", quietEnd: "08:00" });
  await act(async () => container.querySelector<HTMLInputElement>('input[type="checkbox"]')!.click());
  await act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event));
  expect(saved[1]).toEqual({ enabled: false, time: "14:30", quietStart: "22:00", quietEnd: "08:00" });
});
