import { afterEach, beforeEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { ACTION_RUNTIME } from "../src/action-runtime.ts";
import { createActionClient } from "../src/actions.ts";

const originalGlobals = new Map(
  ["window", "document", "location", "CustomEvent", "FormData", "fetch"].map((key) => [
    key,
    (globalThis as Record<string, unknown>)[key],
  ]),
);

beforeEach(() => {
  for (const key of ["window", "document", "location", "CustomEvent", "FormData", "fetch"]) {
    delete (globalThis as Record<string, unknown>)[key];
  }
});
afterEach(() => {
  for (const [key, value] of originalGlobals) {
    if (value === undefined) delete (globalThis as Record<string, unknown>)[key];
    else (globalThis as Record<string, unknown>)[key] = value;
  }
});

test("action runtime exposes pending, optimistic, success, and invalidation state", async () => {
  const win = new Window({ url: "https://example.test/users" });
  win.document.cookie = "wire-csrf=token";
  win.document.body.innerHTML = `<form data-wrn-action="createUser"><input name="name" value="Ada"></form>`;
  const phases: string[] = [];
  ["optimistic", "pending", "success"].forEach((phase) =>
    win.document.addEventListener(`wrnexus:action:${phase}`, () => phases.push(phase)),
  );
  let invalidated: unknown;
  win.addEventListener("wrnexus:cache:invalidate", (event) => {
    invalidated = (event as unknown as CustomEvent).detail.tags;
  });
  const g = globalThis as Record<string, unknown>;
  Object.assign(g, {
    window: win,
    document: win.document,
    location: win.location,
    CustomEvent: win.CustomEvent,
    FormData: win.FormData,
    fetch: async () => Response.json({ data: { id: 1 }, invalidated: ["users"] }),
  });
  (0, eval)(ACTION_RUNTIME);
  win.document
    .querySelector("form")!
    .dispatchEvent(new win.Event("submit", { bubbles: true, cancelable: true }));
  await new Promise((resolve) => setTimeout(resolve, 0));
  expect(phases).toEqual(["optimistic", "pending", "success"]);
  expect(invalidated).toEqual(["users"]);
  expect(win.document.querySelector("form")?.getAttribute("data-wrn-action-state")).toBe("success");
});

test("typed action client returns invalidations and structured validation errors", async () => {
  const original = globalThis.fetch;
  try {
    globalThis.fetch = (async () =>
      Response.json({ data: { id: 1 }, invalidated: ["users"] })) as unknown as typeof fetch;
    const call = createActionClient<{ name: string }, { id: number }>("/users", "createUser");
    expect(await call({ name: "Ada" }, { csrfToken: "token" })).toEqual({
      data: { id: 1 },
      invalidated: ["users"],
    });
    globalThis.fetch = (async () =>
      Response.json({ errors: { name: "Required" } }, { status: 422 })) as unknown as typeof fetch;
    await expect(call({ name: "" })).rejects.toMatchObject({
      status: 422,
      errors: { name: "Required" },
    });
  } finally {
    globalThis.fetch = original;
  }
});
