import { expect, test } from "bun:test";
import { gzipSync } from "bun";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { compileWireFile, parse } from "../../compiler/src/index.ts";
import { mountHtml, renderComponent } from "../../test/src/index.ts";
import { renderThemeCss, resolveThemeConfig } from "../../styles/src/theme.ts";
// The filesystem helpers moved to the server-only `registry` subpath; the
// package entry deliberately stays free of node:* imports so it can be
// bundled for the browser.
import { uiComponentNames, uiComponentsDir, uiComponentPath, uiCss } from "../src/registry.ts";

function uiStyles(): string {
  return [
    uiCss(),
    ...uiComponentNames().map((name) => readFileSync(uiComponentPath(name), "utf8")),
  ].join("\n");
}

test("bundled UI assets are discoverable and readable", () => {
  expect(uiComponentNames()).toContain("Button");
  expect(uiComponentNames()).toContain("Accordion");
  expect(uiComponentNames()).toContain("DataTable");
  expect(uiComponentNames()).toContain("WysiwygEditor");
  expect(readFileSync(uiComponentPath("Button"), "utf8")).toContain("component Button");
  expect(uiCss()).toContain("--wire-");
});

test("global UI CSS stays below its migration ratchet", () => {
  const css = uiCss();
  // Component selectors belong to their owning .wrn files. Keep this asset to
  // resets, tokens, accessibility behavior, motion and reusable utilities.
  expect(gzipSync(new TextEncoder().encode(css)).length).toBeLessThanOrEqual(3_800);
  for (const localOrRemovedFamily of [
    ".wire-switch",
    ".wire-alert",
    ".wire-card",
    ".wire-footer",
    ".wire-metric-card",
    ".wire-segmented-group",
    ".wire-dropdown",
    ".wire-btn",
  ]) {
    expect(css).not.toContain(localOrRemovedFamily);
  }
});

test("components do not carry the standalone wire-next generation artifact", () => {
  const offenders = uiComponentNames().filter((name) => {
    const source = readFileSync(uiComponentPath(name), "utf8");
    return [...source.matchAll(/class=["']([^"']*)["']/g)].some((match) =>
      match[1]!.split(/\s+/).includes("wire-next"),
    );
  });

  expect(offenders).toEqual([]);
});

test("every bundled component carries local styles", () => {
  const unstyled = uiComponentNames().filter(
    (name) => !/^\s{2,4}style \{/m.test(readFileSync(uiComponentPath(name), "utf8")),
  );
  expect(unstyled).toEqual([]);
});

test("generated component reference documents every bundled component and its props", () => {
  const reference = JSON.parse(
    readFileSync(join(uiComponentsDir(), "..", "component-reference.json"), "utf8"),
  ) as { count: number; components: Array<{ name: string; props: Array<{ name: string }> }> };
  const catalog = JSON.parse(
    readFileSync(join(uiComponentsDir(), "..", "component-catalog.json"), "utf8"),
  ) as { components: Array<{ name: string }> };
  expect(reference.count).toBe(uiComponentNames().length);
  expect(reference.count).toBe(102);
  expect(reference.components.map((component) => component.name)).toEqual(
    expect.arrayContaining(uiComponentNames()),
  );
  expect(catalog.components.map((component) => component.name).sort()).toEqual(
    uiComponentNames().sort(),
  );
  expect(reference.components.find((component) => component.name === "Button")?.props).toEqual(
    expect.arrayContaining([
      expect.objectContaining({ name: "label" }),
      expect.objectContaining({ name: "as" }),
    ]),
  );
  expect(
    reference.components
      .find((component) => component.name === "PinInput")
      ?.props.find((prop) => prop.name === "length"),
  ).toEqual(expect.objectContaining({ default: "4" }));
});

test("interactive components expose their must-have public event contracts", () => {
  const reference = JSON.parse(
    readFileSync(join(uiComponentsDir(), "..", "component-reference.json"), "utf8"),
  ) as { components: Array<{ name: string; events: string[] }> };
  const eventsByComponent = new Map(
    reference.components.map((component) => [component.name, component.events]),
  );
  const requiredEvents: Record<string, string[]> = {
    Button: ["click", "focus", "blur"],
    Input: ["input", "change", "focus", "blur", "invalid"],
    Select: ["input", "change", "open", "close"],
    Checkbox: ["input", "change", "focus", "blur", "invalid"],
    ColorPicker: ["input", "change", "focus", "blur"],
    FileInput: ["input", "change", "focus", "blur", "select", "clear", "invalid"],
    InputGroup: ["input", "change", "focus", "blur", "submit", "action"],
    Radio: ["input", "change", "focus", "blur", "invalid"],
    RangeSlider: ["input", "change", "focus", "blur"],
    Switch: ["input", "change", "focus", "blur"],
    Textarea: ["input", "change", "focus", "blur", "invalid"],
    TimePicker: ["input", "change", "focus", "blur", "open", "close", "invalid"],
    Accordion: ["change", "open", "close"],
    Modal: ["open", "close", "cancel", "confirm"],
    DataTable: ["sort", "select", "change", "rowClick", "pageChange"],
    WysiwygEditor: ["input", "change", "focus", "blur"],
  };

  for (const [component, events] of Object.entries(requiredEvents)) {
    expect(eventsByComponent.get(component)).toEqual(expect.arrayContaining(events));
  }
});

test("component reference contains only explicitly declared public events", () => {
  const reference = JSON.parse(
    readFileSync(join(uiComponentsDir(), "..", "component-reference.json"), "utf8"),
  ) as { components: Array<{ name: string; events: string[] }> };

  for (const component of reference.components) {
    const componentPath = uiComponentPath(component.name);
    const source = readFileSync(componentPath, "utf8");

    const ast = parse(source);
    const declaredEvents = [
      ...ast.events.map((event) => event.name),
      ...ast.outputs.map((output) => output.name),
    ];

    const expectedEvents = [...new Set(declaredEvents)];

    if (JSON.stringify(component.events) !== JSON.stringify(expectedEvents)) {
      console.error("UI EVENT REFERENCE MISMATCH", {
        component: component.name,
        componentPath,
        referenceEvents: component.events,
        declaredEvents: expectedEvents,
      });
    }

    expect(component.events).toEqual(expectedEvents);
  }
});

test("bundled components do not duplicate another component implementation", () => {
  const implementations = new Map<string, string>();

  for (const name of uiComponentNames()) {
    const source = readFileSync(uiComponentPath(name), "utf8")
      .replace(/^component\s+[A-Za-z0-9_]+/, "component __NAME__")
      .replace(/\r\n/g, "\n")
      .trim();
    expect(implementations.get(source)).toBeUndefined();
    implementations.set(source, name);
  }
});

test("removed duplicate aliases have valid canonical migration targets", () => {
  const migration = JSON.parse(
    readFileSync(join(uiComponentsDir(), "..", "component-migrations.json"), "utf8"),
  ) as { removedCount: number; replacements: Record<string, string> };
  const names = new Set(uiComponentNames());

  expect(Object.keys(migration.replacements)).toHaveLength(migration.removedCount);
  expect(migration.removedCount).toBe(6);
  for (const [removed, replacement] of Object.entries(migration.replacements)) {
    expect(names.has(removed)).toBe(false);
    expect(names.has(replacement)).toBe(true);
  }
});

test("screenshot-directed canonical components are bundled", () => {
  const required = [
    "Container",
    "Columns",
    "Grid",
    "Typography",
    "Button",
    "Accordion",
    "Alert",
    "Avatar",
    "Card",
    "Carousel",
    "DatePicker",
    "Progress",
    "Spinner",
    "Timeline",
    "Navbar",
    "Tabs",
    "Sidebar",
    "Breadcrumb",
    "Pagination",
    "Stepper",
    "Input",
    "Textarea",
    "Checkbox",
    "Radio",
    "Switch",
    "Select",
    "ComboBox",
    "TimePicker",
    "Modal",
    "Drawer",
    "Popover",
    "Tooltip",
    "DataTable",
    "Chart",
    "Map",
    "WysiwygEditor",
  ];

  expect(uiComponentNames()).toEqual(expect.arrayContaining(required));
});

test("navbar supports brands, nested menus, actions, utility slots, and public events", () => {
  const source = readFileSync(uiComponentPath("Navbar"), "utf8");
  expect(source).toContain('slot name="topbar"');
  expect(source).toContain('slot name="actions"');
  const contract = parse(source);
  const defaults = new Map(contract.props.map((prop) => [prop.name, prop.default]));
  expect(defaults.get("brand")).toBe("{}");
  expect(defaults.get("actions")).toBe("[]");
  expect(defaults.get("openOnHover")).toBe("false");
  expect(defaults.get("maxWidth")).toBe('"full"');
  expect(source).toContain("item.children");
  expect(source).toContain("child.children");
  expect(source).toContain("wire-navbar__dropdown--{item.type || 'dropdown'}");
  const outputs = new Set(contract.outputs.map((output) => output.name));
  for (const event of ["toggle", "open", "close", "select", "action"]) {
    expect(outputs.has(event)).toBe(true);
  }
});

test("auth form uses package schemas, disables native validation, and renders a full-width submit control", async () => {
  const source = readFileSync(uiComponentPath("AuthForm"), "utf8");
  expect(source).toContain('return "auth-login"');
  expect(source).toContain('data-schema="{schemaName()}"');
  expect(source).toContain('data-wrnexus-runtime="auth"');
  expect(source).toContain('novalidate="true"');
  expect(source).toContain('fullWidth="true"');

  const html = await renderComponent(source, { mode: "sign-in" });
  expect(html).toContain('data-schema="auth-login"');
  expect(html).toContain('data-wrnexus-runtime="auth"');
  expect(html).toContain("novalidate");
  expect(html).toContain("wire-auth-form__submit");
  expect(html).toContain('fullWidth="true"');
});

test("footer supports typed entries, responsive columns, pre/post slots, and events", () => {
  const source = readFileSync(uiComponentPath("Footer"), "utf8");
  expect(source).toMatch(/<slot\s+name="pre-footer"\s*>/);
  expect(source).toMatch(/<slot\s+name="post-footer"\s*>/);
  expect(source).toMatch(/<slot\s+name="copyright-left"\s*>/);
  expect(source).toMatch(/<slot\s+name="copyright-right"\s*>/);
  expect(source).toContain('item.type === "header"');
  expect(source).toContain("wire-footer--columns-{columns}");
  const outputs = new Set(parse(source).outputs.map((output) => output.name));
  expect(outputs.has("select")).toBe(true);
  expect(outputs.has("action")).toBe(true);
});

test("navbar renders a brand, nested dropdowns, mega groups, and actions", async () => {
  const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
    brand: {
      label: "Police Management System",
      description: "Public portal",
      href: "/",
    },
    items: [
      {
        label: "Services",
        type: "dropdown",
        children: [{ label: "Reports", href: "/reports" }],
      },
      {
        label: "Safety",
        type: "mega",
        columns: 2,
        children: [
          {
            label: "Resources",
            children: [{ label: "Guidance", href: "/guidance" }],
          },
        ],
      },
    ],
    actions: [{ label: "Sign in", href: "/login", variant: "primary" }],
  });

  expect(html).toContain("Police Management System");
  expect(html).toContain("Public portal");
  expect(html).toContain("Reports");
  expect(html).toContain("Guidance");
  expect(html).toContain("wire-navbar__dropdown--mega");
  expect(html).toContain("data-wrn-navbar");
  expect(html).toContain('name="wire-navbar-menu"');
  expect(html).toContain("Sign in");
});

test("navbar closes sibling menus and menus close after an outside pointer press", async () => {
  const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
    items: [
      { label: "Services", children: [{ label: "Reports", href: "/reports" }] },
      { label: "Safety", children: [{ label: "Guidance", href: "/guidance" }] },
    ],
  });
  const dom = mountHtml(html);
  const menus = Array.from(dom.querySelectorAll(".wire-navbar__dropdown")) as HTMLDetailsElement[];

  menus[0].open = true;
  menus[0].dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("toggle", { bubbles: false }),
  );
  menus[1].open = true;
  menus[1].dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("toggle", { bubbles: false }),
  );
  expect(menus[0].open).toBe(false);
  expect(menus[1].open).toBe(true);

  dom.document.body.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("pointerdown", { bubbles: true }),
  );
  expect(menus[1].open).toBe(false);
});

test("navbar optionally opens menus on hover and renders full-width mode", async () => {
  const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
    openOnHover: true,
    maxWidth: "full",
    items: [{ label: "Services", children: [{ label: "Reports", href: "/reports" }] }],
  });
  const dom = mountHtml(html);
  const navbar = dom.querySelector("[data-wrn-navbar]");
  const menu = dom.querySelector(".wire-navbar__dropdown") as HTMLDetailsElement;

  expect(navbar?.classList.contains("wire-navbar--width-full")).toBe(true);
  expect(navbar?.getAttribute("data-open-on-hover")).toBe("true");
  menu.dispatchEvent(new (dom.window as { Event: typeof Event }).Event("pointerenter"));
  expect(menu.open).toBe(true);
  menu.dispatchEvent(new (dom.window as { Event: typeof Event }).Event("pointerleave"));
  expect(menu.open).toBe(true);
  await new Promise((resolve) => setTimeout(resolve, 280));
  expect(menu.open).toBe(false);
});

test("navbar hover menu stays open while the pointer crosses into its panel", async () => {
  const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), {
    openOnHover: true,
    items: [{ label: "Services", children: [{ label: "Reports", href: "/reports" }] }],
  });
  const dom = mountHtml(html);
  const menu = dom.querySelector(".wire-navbar__dropdown") as HTMLDetailsElement;
  const panel = dom.querySelector(".wire-navbar__panel") as HTMLElement;
  const DomEvent = (dom.window as { Event: typeof Event }).Event;

  menu.dispatchEvent(new DomEvent("pointerenter"));
  menu.dispatchEvent(new DomEvent("pointerleave"));
  panel.dispatchEvent(new DomEvent("pointerenter", { bubbles: true }));
  await new Promise((resolve) => setTimeout(resolve, 280));

  expect(menu.open).toBe(true);
});

test("preference switcher keeps only one popover open and closes outside", async () => {
  const html = await renderComponent(
    readFileSync(uiComponentPath("PreferenceSwitcher"), "utf8"),
    {},
  );
  const dom = mountHtml(html);
  const menus = Array.from(dom.querySelectorAll(".wire-preferences__menu")) as HTMLDetailsElement[];
  const DomEvent = (dom.window as { Event: typeof Event }).Event;

  menus[0].open = true;
  menus[0].dispatchEvent(new DomEvent("toggle"));
  menus[1].open = true;
  menus[1].dispatchEvent(new DomEvent("toggle"));
  expect(menus[0].open).toBe(false);
  expect(menus[1].open).toBe(true);

  dom.document.body.dispatchEvent(new DomEvent("pointerdown", { bubbles: true }));
  expect(menus[1].open).toBe(false);
});

test("sidebar renders compact grouped submenu navigation and a mobile drawer trigger", async () => {
  const html = await renderComponent(readFileSync(uiComponentPath("Sidebar"), "utf8"), {
    label: "Operations",
    orientation: "vertical",
    items: [
      { label: "Dashboard", href: "/dashboard", value: "dashboard" },
      {
        label: "Case management",
        children: [
          { label: "Assigned cases", href: "/cases", value: "cases" },
          { label: "Evidence", href: "/evidence", value: "evidence" },
        ],
      },
    ],
  });

  // Classes moved to the BEM naming every other component uses, and the
  // off-canvas presentation is now Drawer rather than a hand-rolled backdrop.
  // Nesting via children still works: that is what shipped in 0.8.5.
  expect(html).toContain("wire-sidebar__launcher");
  expect(html).toContain("wire-sidebar__sublist");
  expect(html).toContain('data-component="Drawer"');
  expect(html).toContain("Assigned cases");
  expect(html).toContain("Evidence");
});

test("dropdown renders slotted account triggers, menu structure, and public events", async () => {
  const source = readFileSync(uiComponentPath("Dropdown"), "utf8");
  const html = await renderComponent(source, {
    label: "Account menu",
    items: [
      { type: "header", label: "Account" },
      { label: "Profile", href: "/profile", icon: "icon-[lucide--user]" },
      { type: "divider" },
      { label: "Sign out", href: "/logout", danger: true },
    ],
  });

  expect(source).toContain('slot name="trigger"');
  expect(parse(source).outputs.map((output) => output.name)).toContain("select");
  expect(html).toContain("wire-dropdown__panel");
  expect(html).toContain("Profile");
  expect(html).toContain('data-danger="true"');
});

test("footer renders header and link entries with the requested column count", async () => {
  const html = await renderComponent(readFileSync(uiComponentPath("Footer"), "utf8"), {
    columns: 4,
    items: [
      { type: "header", label: "Support" },
      { type: "link", label: "Contact", href: "/contact" },
    ],
    copyright: "Public service platform",
  });

  expect(html).toContain("wire-footer--columns-4");
  expect(html).toContain("wire-footer__column");
  expect(html).toContain("wire-footer__column-links");
  expect(html).toContain("wire-footer__heading");
  expect(html).toContain("Support");
  expect(html).toContain('href="/contact"');
  expect(html).toContain("Public service platform");
});

test("component-system CSS includes responsive, theme-token, focus, and reduced-motion rules", () => {
  const css = uiStyles();
  expect(css).toContain("@media (max-width: 768px)");
  expect(css).toContain("@media (max-width: 480px)");
  expect(css).toContain("@media (prefers-reduced-motion: reduce)");
  expect(css).toContain(":focus-visible");
  expect(css).toContain("var(--wire-color-surface)");
  expect(css).toContain("min-height: 44px");
  expect(css).toContain("--wire-motion-base");
  expect(css).toContain("--wire-ease-emphasized");
  expect(css).toContain("@keyframes wire-component-enter");
  expect(css).toContain("@keyframes wire-dialog-enter");
  expect(css).toContain("@media (hover: hover) and (pointer: fine)");
  expect(css).toContain("--color-violet-600: var(--wire-color-primary)");
  expect(css).toContain("--color-blue-600: var(--wire-color-info)");
  expect(css).toContain("--color-red-600: var(--wire-color-danger)");
  expect(css).toMatch(/:root\s*\{[^}]*--color-violet-600: var\(--wire-color-primary\)/s);
  expect(css).toContain(".wire-bg-primary");
  expect(css).toContain(".wire-text-muted");
  expect(css).toContain(".wire-visually-hidden");
  expect(css).toContain(".wire-component--color-primary");
  expect(css).toContain(".wire-next--field");
  expect(css).toContain(".wire-next__strong-password-requirements");
  expect(css).toContain('[data-show="false"]');
  expect(css).toContain("display: none !important");
  /*
   * Navbar styles now ship with the component rather than from ui.css, so the
   * whole navigation group follows one convention. The rules themselves are
   * unchanged; only where they live moved.
   */
  const navbarSource = readFileSync(uiComponentPath("Navbar"), "utf8");
  expect(uiCss()).not.toContain(".wire-navbar");
  expect(navbarSource).toMatch(
    /\.wire-navbar__brand-copy strong\s*\{[^}]*font-size: 0\.9rem;[^}]*font-weight: 600;/s,
  );
  expect(navbarSource).toMatch(
    /\.wire-navbar__menu-link,[\s\S]*?font-size: 0\.8125rem;[\s\S]*?font-weight: 500;/,
  );
  expect(css).toMatch(
    /\.wire-footer__heading\s*\{[^}]*font-size: 0\.8125rem;[^}]*font-weight: 600;/s,
  );
  expect(css).toMatch(/\.wire-footer__link\s*\{[^}]*font-size: 0\.8125rem;[^}]*font-weight: 400;/s);
});

test("every component owns its styles without pseudo-component mounts", () => {
  const globalCss = uiCss();
  expect(globalCss).not.toContain(".wire-component");
  expect(globalCss).not.toContain(".wire-next");
  for (const name of uiComponentNames()) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    expect(source).toMatch(/\n\s*style\s*\{/);
    expect(source).not.toContain("ComponentBaseStyles");
    expect(source).not.toContain("FieldStyles");
  }
  expect(readFileSync(uiComponentPath("Alert"), "utf8")).toContain("Shared component foundation.");
  expect(readFileSync(uiComponentPath("Input"), "utf8")).toContain("Shared field foundation.");
});

test("component boundaries have no default margins and expose the canonical class prop", () => {
  for (const name of uiComponentNames()) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    const root = source.match(/view\s*\{\s*<[A-Za-z][^>]*>/)?.[0] ?? "";
    const classes = root.match(/\bclass="([^"]*)"/)?.[1]?.split(/\s+/) ?? [];
    expect(classes.filter((token) => /^-?m[trblxy]?-/.test(token))).toEqual([]);
    expect(source).toMatch(/^\s*class\s*(?::[^=\r\n]+)?=/m);
    expect(source).not.toMatch(/^\s*className\s*(?::[^=\r\n]+)?=/m);
    expect(root).toContain("{class}");
  }
});

test("every component exposes universal color and size configuration", () => {
  for (const name of uiComponentNames()) {
    const ast = parse(readFileSync(uiComponentPath(name), "utf8"));
    const props = new Set(ast.props.map((prop) => prop.name));
    expect(props.has("color")).toBe(true);
    expect(props.has("size")).toBe(true);
  }
});

// test("component markup keeps user-facing content and data behind props", () => {
//   for (const name of uiComponentNames()) {
//     const source = readFileSync(uiComponentPath(name), "utf8");
//     const view = source.slice(source.indexOf("view {"));
//     const literalText = [...view.matchAll(/>([^<>{}]*[A-Za-z][^<>{}]*)</g)]
//       .map((match) => match[1].trim())
//       .filter(Boolean);
//     const literalAccessibleText = [
//       ...view.matchAll(/\b(?:aria-label|title|placeholder|alt)="([^"{}]*[A-Za-z][^"{}]*)"/g),
//     ];
//     const literalDataAttributes = [
//       ...view.matchAll(/\b(?:href|src|action|value)="([^"{}]+)"/g),
//     ].filter((match) => !/^(?:true|false|on)$/.test(match[1]));

//     expect(literalText).toEqual([]);
//     expect(literalAccessibleText).toEqual([]);
//     expect(literalDataAttributes).toEqual([]);
//   }
// });

test("layout boundaries do not add default padding", () => {
  for (const name of ["Container", "Columns", "Grid", "LayoutSplitter", "Typography"]) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    const root = source.match(/view\s*\{\s*<[A-Za-z][^>]*>/)?.[0] ?? "";
    const classes = root.match(/\bclass="([^"]*)"/)?.[1]?.split(/\s+/) ?? [];
    expect(classes.filter((token) => /^p[trblxy]?-/.test(token))).toEqual([]);
  }
});

test("every bundled UI component compiles", () => {
  for (const name of uiComponentNames()) {
    const path = uiComponentPath(name);
    expect(() => compileWireFile(readFileSync(path, "utf8"), path)).not.toThrow();
  }
});

test("icon buttons expose their accessible label as a hover and focus tooltip", () => {
  const button = readFileSync(uiComponentPath("Button"), "utf8");
  expect(button).toContain('class="wire-btn__tooltip"');
  expect(button).toContain("{ariaLabel || label}");
  expect(uiStyles()).toContain(".wire-btn:hover > .wire-btn__tooltip");
  expect(uiStyles()).toContain(".wire-btn:focus-visible > .wire-btn__tooltip");
});

test("button links navigate by default and only download when the native attribute is passed", () => {
  const button = readFileSync(uiComponentPath("Button"), "utf8");
  expect(button).not.toMatch(/^\s+download\s*=/m);
  expect(button).not.toContain('download="{download}"');
  expect(button).toContain("{...attrs}");
});

test("button group renders connected actions and emits select and change events", async () => {
  const source = readFileSync(uiComponentPath("ButtonGroup"), "utf8");
  const html = await renderComponent(source, {
    items: [
      { label: "Profile", value: "profile" },
      { label: "Settings", value: "settings" },
      { label: "Messages", value: "messages", disabled: true },
    ],
    value: "profile",
    selectable: true,
    size: "sm",
    variant: "outline",
    ariaLabel: "Account views",
  });
  const dom = mountHtml(html);
  const group = dom.querySelector(".wire-next--button-group") as HTMLElement;
  const buttons = [...dom.querySelectorAll(".wire-next--button-group > button")];
  const events: Array<{ name: string; detail: unknown }> = [];

  for (const name of ["select", "change"]) {
    group.addEventListener(name, (event) =>
      events.push({ name, detail: (event as CustomEvent).detail }),
    );
  }

  expect(group.getAttribute("role")).toBe("group");
  expect(group.getAttribute("aria-label")).toBe("Account views");
  expect(group.getAttribute("data-size")).toBe("sm");
  expect(buttons).toHaveLength(3);
  expect(buttons[0]?.getAttribute("aria-pressed")).toBe("true");
  expect(buttons[2]?.hasAttribute("disabled")).toBe(true);

  (buttons[1] as HTMLButtonElement).click();

  expect(buttons[0]?.getAttribute("aria-pressed")).toBe("false");
  expect(buttons[1]?.getAttribute("aria-pressed")).toBe("true");
  expect(events.map((event) => event.name)).toEqual(["select", "change"]);
  expect((events[1]?.detail as { value: string }).value).toBe("settings");
  expect((events[1]?.detail as { previousValue: string }).previousValue).toBe("profile");
});

test("button group supports vertical, responsive, detached, and toolbar layouts", async () => {
  const source = readFileSync(uiComponentPath("ButtonGroup"), "utf8");
  const html = await renderComponent(source, {
    items: [{ label: "Bold", icon: "icon-[lucide--bold]" }],
    orientation: "vertical",
    responsive: true,
    attached: false,
    toolbar: true,
    ariaLabel: "Formatting",
  });
  const dom = mountHtml(html);
  const group = dom.querySelector(".wire-next--button-group") as HTMLElement;

  expect(group.getAttribute("role")).toBe("toolbar");
  expect(group.getAttribute("aria-orientation")).toBe("vertical");
  expect(group.getAttribute("data-responsive")).toBe("true");
  expect(group.getAttribute("data-attached")).toBe("false");
  expect(group.querySelector('[class*="icon-[lucide--bold]"]')).not.toBeNull();

  const css = uiStyles();
  expect(css).toContain('.wire-next--button-group[data-orientation="vertical"]');
  expect(css).toContain('.wire-next--button-group[data-responsive="true"]');
  expect(css).toContain('.wire-next--button-group[data-attached="false"]');
});

test("every bundled UI component forwards undeclared native attributes", () => {
  for (const name of uiComponentNames()) {
    const path = uiComponentPath(name);
    const output = compileWireFile(readFileSync(path, "utf8"), path);
    expect(output).toContain("${__wireSpreadAttrs(__attrs)}");
  }
});

test("accordion coordinates single-open sections and emits open, close, and change events", async () => {
  const source = readFileSync(uiComponentPath("Accordion"), "utf8");
  const html = await renderComponent(source, {
    id: "faq",
    defaultOpen: ["first"],
    items: [
      { value: "first", label: "First", content: "First content" },
      { value: "second", label: "Second", content: "Second content" },
    ],
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-accordion]") as HTMLElement;
  const events: string[] = [];
  for (const name of ["open", "close", "change"]) {
    root.addEventListener(name, () => events.push(name));
  }

  expect(dom.querySelector("#faq-trigger-0")?.getAttribute("aria-expanded")).toBe("true");
  expect(dom.querySelector("#faq-panel-0")?.getAttribute("aria-hidden")).toBe("false");

  (dom.querySelector("#faq-trigger-1") as HTMLButtonElement).click();

  expect(dom.querySelector("#faq-trigger-0")?.getAttribute("aria-expanded")).toBe("false");
  expect(dom.querySelector("#faq-trigger-1")?.getAttribute("aria-expanded")).toBe("true");
  expect(events).toEqual(["open", "change"]);

  (dom.querySelector("#faq-trigger-1") as HTMLButtonElement).click();
  expect(dom.querySelector("#faq-trigger-1")?.getAttribute("aria-expanded")).toBe("false");
  expect(events).toEqual(["open", "change", "close", "change"]);
});

test("accordion supports always-open, nested, and disabled sections", async () => {
  const source = readFileSync(uiComponentPath("Accordion"), "utf8");
  const html = await renderComponent(source, {
    id: "nested-faq",
    alwaysOpen: true,
    defaultOpen: ["first", "first.child"],
    items: [
      {
        value: "first",
        label: "First",
        content: "First content",
        children: [{ value: "child", label: "Child", content: "Child content" }],
      },
      { value: "second", label: "Second", content: "Second content" },
      { value: "third", label: "Third", content: "Third content", disabled: true },
    ],
  });
  const dom = mountHtml(html);

  expect(dom.querySelector("#nested-faq-trigger-0")?.getAttribute("aria-expanded")).toBe("true");
  expect(dom.querySelector("#nested-faq-trigger-0-0")?.getAttribute("aria-expanded")).toBe("true");

  (dom.querySelector("#nested-faq-trigger-1") as HTMLButtonElement).click();
  expect(dom.querySelector("#nested-faq-trigger-0")?.getAttribute("aria-expanded")).toBe("true");
  expect(dom.querySelector("#nested-faq-trigger-1")?.getAttribute("aria-expanded")).toBe("true");
  expect((dom.querySelector("#nested-faq-trigger-2") as HTMLButtonElement).disabled).toBe(true);
});

test("accordion chevrons use a consistent icon instead of a font glyph", () => {
  const source = readFileSync(uiComponentPath("Accordion"), "utf8");

  expect(source).toContain("icon-[lucide--chevron-down]");
  expect(source).not.toContain('return "⌄"');
});

test("alert renders structured content, lists, actions, and accessible semantics", async () => {
  const source = readFileSync(uiComponentPath("Alert"), "utf8");
  const html = await renderComponent(source, {
    title: "Cannot save changes",
    description: "Review the following fields.",
    color: "danger",
    variant: "soft",
    showIcon: true,
    items: [{ label: "Email is required" }, { label: "Phone number is invalid" }],
    actions: [{ label: "Review form", href: "#form" }],
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-alert]") as HTMLElement;
  let detail: Record<string, unknown> | undefined;
  root.addEventListener("action", (event) => {
    detail = (event as unknown as CustomEvent<Record<string, unknown>>).detail;
  });

  expect(root.getAttribute("role")).toBe("alert");
  expect(root.getAttribute("aria-live")).toBe("polite");
  expect(root.dataset.color).toBe("danger");
  expect(dom.querySelectorAll(".wire-next__alert-body li").map((node) => node.textContent)).toEqual(
    ["Email is required", "Phone number is invalid"],
  );

  (dom.querySelector(".wire-next__alert-actions a") as HTMLAnchorElement).click();
  expect(detail).toMatchObject({
    component: "Alert",
    action: { label: "Review form", href: "#form" },
  });
});

test("dismissible alert hides itself and emits dismiss details", async () => {
  const source = readFileSync(uiComponentPath("Alert"), "utf8");
  const html = await renderComponent(source, {
    title: "Update available",
    description: "Install the latest version.",
    dismissible: true,
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-alert]") as HTMLElement;
  let dismissed = false;
  root.addEventListener("dismiss", () => {
    dismissed = true;
  });

  (dom.querySelector(".wire-next__alert-dismiss") as HTMLButtonElement).click();

  expect(dismissed).toBe(true);
  expect(root.getAttribute("aria-hidden")).toBe("true");
  expect(root.style.display).toBe("none");
});

test("solid alert palettes preserve readable foregrounds and compact inline layout", () => {
  const css = uiStyles();

  expect(css).toContain('.wire-next--alert[data-color="secondary"]');
  expect(css).toContain("--wire-alert-color: #737373");
  expect(css).toContain(
    ".wire-next--alert-compact:not(:has(.wire-next__alert-icon)):not(:has(.wire-next__alert-dismiss))",
  );
  expect(css).toContain(".wire-next--alert-compact .wire-next__alert-body > p");
  expect(css).toContain("color: currentColor");
});

test("alert exposes configurable radius and elevation treatments", async () => {
  const source = readFileSync(uiComponentPath("Alert"), "utf8");
  const html = await renderComponent(source, {
    title: "Elevated alert",
    radius: "xl",
    shadow: "lg",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-alert]") as HTMLElement;
  const css = uiStyles();

  expect(root.dataset.radius).toBe("xl");
  expect(root.dataset.shadow).toBe("lg");
  expect(css).toContain('.wire-next--alert[data-radius="xl"]');
  expect(css).toContain('.wire-next--alert[data-shadow="lg"]');
  expect(css).toContain("border-radius: calc(var(--wire-radius, 0.75rem) * 1.75)");
  expect(css).toContain("0 20px 48px color-mix(in srgb, #000 24%, transparent)");
});

test("avatar renders images, fallbacks, presence, badges, tooltips, and media copy", async () => {
  const source = readFileSync(uiComponentPath("Avatar"), "utf8");
  const imageHtml = await renderComponent(source, {
    src: "/avatar.jpg",
    alt: "Avery Chen",
    shape: "rounded",
    statusPosition: "top",
    status: "online",
    statusLabel: "Online",
    tooltip: "Avery Chen · Online",
  });
  const imageDom = mountHtml(imageHtml);
  const imageRoot = imageDom.querySelector("[data-wrn-avatar]") as HTMLElement;

  expect(imageRoot.querySelector('img[alt="Avery Chen"]')).not.toBeNull();
  expect(imageRoot.querySelector('[data-status="online"]')?.getAttribute("aria-label")).toBe(
    "Online",
  );
  expect(imageRoot.querySelector('[role="tooltip"]')?.textContent).toContain("Avery Chen");

  const initialsDom = mountHtml(
    await renderComponent(source, {
      initials: "MW",
      color: "success",
      badgeIcon: "icon-[lucide--message-square]",
      badgeLabel: "Messaging account",
      name: "Mark Wanner",
      description: "mark@example.com",
    }),
  );
  expect(initialsDom.querySelector(".wire-next__avatar-initials")?.textContent).toBe("MW");
  expect(initialsDom.querySelector(".wire-next__avatar-badge")?.getAttribute("aria-label")).toBe(
    "Messaging account",
  );
  expect(initialsDom.querySelector(".wire-next__avatar-copy")?.textContent).toContain(
    "mark@example.com",
  );

  const placeholderDom = mountHtml(await renderComponent(source, { size: "sm" }));
  expect(placeholderDom.querySelector(".wire-next__avatar-placeholder")).not.toBeNull();
  expect(source).not.toContain("items =");
  expect(uiStyles()).toContain('.wire-next__avatar-wrap[data-shape="rounded"]');
});

test("avatar group limits members and opens an accessible overflow menu", async () => {
  const source = readFileSync(uiComponentPath("AvatarGroup"), "utf8");
  const html = await renderComponent(source, {
    maxVisible: 2,
    borderColor: "#2583ff",
    items: [
      { src: "/one.svg", name: "One" },
      { initials: "TW", name: "Two" },
      { initials: "TH", name: "Three" },
      { initials: "FO", name: "Four" },
    ],
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-avatar-group]") as HTMLElement;
  const button = root.querySelector(
    ".wire-next__avatar-group-overflow-button",
  ) as HTMLButtonElement;
  let eventDetail: { open: boolean; hiddenCount: number } | undefined;
  root.addEventListener("overflow", (event) => {
    eventDetail = (event as CustomEvent).detail;
  });

  expect(root.querySelectorAll(".wire-next__avatar-group-member")).toHaveLength(2);
  expect(button.textContent?.trim()).toBe("+2");
  expect(button.getAttribute("aria-expanded")).toBe("false");
  expect(root.getAttribute("style")).toContain("#2583ff");
  expect(root.querySelector(".wire-next__avatar-group-menu")?.getAttribute("data-show")).toBe(
    "false",
  );

  button.click();

  expect(
    dom.querySelector(".wire-next__avatar-group-overflow-button")?.getAttribute("aria-expanded"),
  ).toBe("true");
  expect(dom.querySelectorAll('[role="menuitem"]')).toHaveLength(2);
  expect(dom.querySelector(".wire-next__avatar-group-menu")?.getAttribute("data-show")).toBe(
    "true",
  );
  expect(eventDetail).toEqual(expect.objectContaining({ open: true, hiddenCount: 2 }));
  expect(uiStyles()).toContain('.wire-next--avatar-group[data-layout="grid"]');
});

test("badge supports rich content, anchored placement, animation, and dismissal", async () => {
  const source = readFileSync(uiComponentPath("Badge"), "utf8");
  const html = await renderComponent(source, {
    label: "Christina",
    color: "success",
    variant: "soft",
    icon: "icon-[lucide--circle-check]",
    dot: true,
    avatarSrc: "/avatar.svg",
    avatarAlt: "Christina",
    dismissible: true,
    animated: true,
    anchorIcon: "icon-[lucide--bell]",
    anchorLabelText: "Open notifications",
    placement: "top-right",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-badge]") as HTMLElement;
  let dismissed = "";
  root.addEventListener("dismiss", (event) => {
    dismissed = (event as CustomEvent).detail.label;
  });

  expect(root.dataset.placement).toBe("top-right");
  expect(root.querySelector(".wire-next__badge-anchor")?.getAttribute("aria-label")).toBe(
    "Open notifications",
  );
  expect(root.querySelector('img[alt="Christina"]')).not.toBeNull();
  expect(root.querySelector(".wire-next__badge-dot")).not.toBeNull();
  expect(root.querySelector(".wire-next__badge-ping")).not.toBeNull();

  (root.querySelector(".wire-next__badge-dismiss") as HTMLButtonElement).click();

  expect(dom.querySelector("[data-wrn-badge]")?.getAttribute("data-show")).toBe("false");
  expect(dismissed).toBe("Christina");
  expect(source).not.toContain("items =");
  expect(uiStyles()).toContain("@keyframes wire-badge-ping");
});

test("blockquote renders semantic quote and citation content", async () => {
  const source = readFileSync(uiComponentPath("Blockquote"), "utf8");
  const html = await renderComponent(source, {
    quote: "Design is intelligence made visible.",
    citation: "Alina Wheeler",
    citationTitle: "Author, Designing Brand Identity",
    citationUrl: "https://example.com/source",
    avatarSrc: "/authors/alina.jpg",
    avatarAlt: "Alina Wheeler",
    size: "lg",
    color: "info",
    align: "center",
  });
  const dom = mountHtml(html);
  const figure = dom.querySelector("figure") as HTMLElement;
  const quote = figure.querySelector("blockquote");
  const citation = figure.querySelector("figcaption");

  expect(figure.getAttribute("data-size")).toBe("lg");
  expect(figure.getAttribute("data-color")).toBe("info");
  expect(figure.getAttribute("data-align")).toBe("center");
  expect(quote?.getAttribute("cite")).toBe("https://example.com/source");
  expect(quote?.textContent).toContain("Design is intelligence made visible.");
  expect(quote?.querySelector('[aria-hidden="true"]')?.textContent).toBe("“");
  expect(citation?.querySelector("cite a")?.textContent).toBe("Alina Wheeler");
  expect(citation?.textContent).toContain("Author, Designing Brand Identity");
  expect(citation?.querySelector("img")?.getAttribute("alt")).toBe("Alina Wheeler");
});

test("blockquote supports bordered, right-aligned, non-italic, and slotted quotes", async () => {
  const source = readFileSync(uiComponentPath("Blockquote"), "utf8");
  const html = await renderComponent(source, {
    quote: "",
    variant: "bordered",
    align: "right",
    italic: false,
    quoteMark: false,
  });
  const dom = mountHtml(html);
  const figure = dom.querySelector("figure") as HTMLElement;

  expect(figure.getAttribute("data-variant")).toBe("bordered");
  expect(figure.getAttribute("data-align")).toBe("right");
  expect(figure.getAttribute("data-italic")).toBe("false");
  expect(figure.querySelector(".wire-next__blockquote-mark")).toBeNull();
  expect(source).toContain("<slot />");

  const css = uiStyles();
  expect(css).toContain('.wire-next--blockquote[data-variant="bordered"]');
  expect(css).toContain('.wire-next--blockquote[data-align="center"]');
  expect(css).toContain("--wire-blockquote-font-size");
});

test("card renders structured media, header, body, action, and footer content", async () => {
  const source = readFileSync(uiComponentPath("Card"), "utf8");
  const html = await renderComponent(source, {
    title: "Release notes",
    subtitle: "Version 2.4",
    description: "A structured card description.",
    header: "Featured",
    footer: "Updated today",
    imageSrc: "/release.jpg",
    imageAlt: "Release preview",
    actionLabel: "Read more",
    actionHref: "/releases/2-4",
    size: "lg",
    color: "info",
  });
  const dom = mountHtml(html);
  const card = dom.querySelector(".wire-next--card") as HTMLElement;

  expect(card.getAttribute("data-size")).toBe("lg");
  expect(card.getAttribute("data-color")).toBe("info");
  expect(card.querySelector("article")).not.toBeNull();
  expect(card.querySelector("header")?.textContent).toContain("Featured");
  expect(card.querySelector("img")?.getAttribute("alt")).toBe("Release preview");
  expect(card.querySelector("h3")?.textContent).toBe("Release notes");
  expect(card.querySelector(".wire-next__card-subtitle")?.textContent).toBe("Version 2.4");
  expect(card.querySelector(".wire-next__card-action")?.getAttribute("href")).toBe("/releases/2-4");
  expect(card.querySelector("footer")?.textContent).toContain("Updated today");
});

test("card navigation, header actions, and dismissal emit public events", async () => {
  const source = readFileSync(uiComponentPath("Card"), "utf8");
  const html = await renderComponent(source, {
    title: "Account",
    header: "Card actions",
    navigation: [
      { label: "Profile", value: "profile" },
      { label: "Security", value: "security" },
    ],
    activeNav: "profile",
    headerActions: [{ label: "Refresh", icon: "icon-[lucide--refresh-cw]" }],
    dismissible: true,
  });
  const dom = mountHtml(html);
  const card = dom.querySelector(".wire-next--card") as HTMLElement;
  const events: string[] = [];

  for (const name of ["navigate", "action", "dismiss"]) {
    card.addEventListener(name, () => events.push(name));
  }

  (dom.querySelectorAll(".wire-next__card-tabs button")[1] as HTMLButtonElement).click();
  (dom.querySelector(".wire-next__card-header-actions button") as HTMLButtonElement).click();
  (dom.querySelectorAll(".wire-next__card-header-actions button")[1] as HTMLButtonElement).click();

  expect(events).toEqual(["navigate", "action", "dismiss"]);
  expect(dom.querySelector(".wire-next__card-panel")?.hasAttribute("hidden")).toBe(true);
});

test("card supports overlays, horizontal media, groups, alerts, empty and scrolling panels", async () => {
  const source = readFileSync(uiComponentPath("Card"), "utf8");
  const overlay = mountHtml(
    await renderComponent(source, {
      title: "Overlay",
      description: "Overlay copy",
      imageSrc: "/overlay.jpg",
      imagePosition: "overlay",
    }),
  );
  const group = mountHtml(
    await renderComponent(source, {
      items: [
        { title: "First", description: "First card" },
        { title: "Second", description: "Second card" },
      ],
    }),
  );
  const panel = mountHtml(
    await renderComponent(source, {
      title: "",
      alertTitle: "Attention",
      alertDescription: "Review this state.",
      empty: true,
      scrollable: true,
      maxHeight: "12rem",
    }),
  );

  expect(overlay.querySelector(".wire-next__card-overlay")).not.toBeNull();
  expect(group.querySelectorAll(".wire-next__card-group > article")).toHaveLength(2);
  expect(panel.querySelector(".wire-next__card-alert")?.getAttribute("role")).toBe("status");
  expect(panel.querySelector(".wire-next__card-empty")?.textContent).toContain("No data to show");
  expect(panel.querySelector(".wire-next__card-body")?.getAttribute("data-scrollable")).toBe(
    "true",
  );

  const css = uiStyles();
  expect(css).toContain('.wire-next--card[data-layout="horizontal"]');
  expect(css).toContain('.wire-next--card[data-hover="image"]');
  expect(css).toContain(".wire-next__card-group");
  expect(css).toContain(".wire-next__card-navigation-select");
  expect(css).toContain("aspect-ratio: 16 / 9");
  expect(css).toContain("rgb(2 6 23 / 0.86)");
});

test("chat bubble renders conversations, avatars, metadata, links, and public events", async () => {
  const source = readFileSync(uiComponentPath("ChatBubble"), "utf8");
  const events: string[] = [];
  const dom = mountHtml(
    await renderComponent(source, {
      ariaLabel: "Support conversation",
      oneSided: true,
      showAvatars: true,
      showMetadata: true,
      items: [
        {
          direction: "incoming",
          author: "Support",
          avatarFallback: "SP",
          title: "How can we help?",
          text: "Choose a guide.",
          bullets: ["Installation", "Components"],
          links: [{ label: "Installation guide", href: "/installation" }],
          status: "Sent",
          timestamp: "10:32",
        },
        {
          direction: "outgoing",
          text: "Please retry.",
          status: "Not sent",
          statusTone: "danger",
          action: "retry",
          actionLabel: "Retry",
        },
      ],
    }),
  );

  const root = dom.querySelector(".wire-next--chat-bubble") as HTMLElement;
  for (const [name, label] of [
    ["messageClick", "message"],
    ["avatarClick", "avatar"],
    ["linkClick", "link"],
    ["action", "action"],
  ]) {
    root.addEventListener(name, () => events.push(label));
  }
  expect(root.getAttribute("role")).toBe("log");
  expect(root.getAttribute("aria-label")).toBe("Support conversation");
  expect(root.dataset.oneSided).toBe("true");
  expect(dom.querySelectorAll(".wire-next__chat-message")).toHaveLength(2);
  expect(dom.querySelectorAll(".wire-next__chat-content li")).toHaveLength(2);
  expect(dom.querySelector(".wire-next__chat-meta[data-tone='danger']")).not.toBeNull();
  expect(uiStyles()).toContain("color: var(--wire-color-primary-contrast, #fff)");

  (dom.querySelector(".wire-next__chat-content") as HTMLElement).click();
  (dom.querySelector(".wire-next__chat-avatar") as HTMLButtonElement).click();
  (dom.querySelector(".wire-next__chat-content a") as HTMLAnchorElement).click();
  (dom.querySelector(".wire-next__chat-meta button") as HTMLButtonElement).click();
  expect(events).toEqual(["message", "avatar", "link", "action"]);
});

test("collapse toggles accessible panels and emits open, close, and toggle events", async () => {
  const source = readFileSync(uiComponentPath("Collapse"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      ariaLabel: "Product details",
      initialOpenIndexes: [0],
      items: [
        {
          id: "product-details",
          label: "Expand",
          closeLabel: "Collapse",
          title: "Details",
          content: "Additional product information.",
        },
      ],
    }),
  );
  const root = dom.querySelector(".wire-next--collapse") as HTMLElement;
  const trigger = dom.querySelector(".wire-next__collapse-trigger") as HTMLButtonElement;
  const panel = dom.querySelector(".wire-next__collapse-panel") as HTMLElement;
  const events: string[] = [];
  for (const name of ["open", "close", "toggle"]) {
    root.addEventListener(name, () => events.push(name));
  }

  expect(root.getAttribute("aria-label")).toBe("Product details");
  expect(trigger.getAttribute("aria-expanded")).toBe("true");
  expect(trigger.getAttribute("aria-controls")).toBe("product-details");
  expect(panel.dataset.open).toBe("true");
  expect(panel.getAttribute("aria-hidden")).toBe("false");
  expect(uiStyles()).toContain("max-height: 40rem");
  expect(trigger.textContent).toContain("Collapse");

  trigger.click();
  expect(trigger.getAttribute("aria-expanded")).toBe("false");
  expect(panel.dataset.open).toBe("false");
  expect(panel.getAttribute("aria-hidden")).toBe("true");
  expect(events).toEqual(["close", "toggle"]);

  trigger.click();
  expect(panel.dataset.open).toBe("true");
  expect(events).toEqual(["close", "toggle", "open", "toggle"]);
});

test("collapse supports inline read-more and single or multiple open panels", async () => {
  const source = readFileSync(uiComponentPath("Collapse"), "utf8");
  const items = [
    { id: "first-collapse", label: "Read more", preview: "First preview", content: "First" },
    { id: "second-collapse", label: "Read more", preview: "Second preview", content: "Second" },
  ];
  const single = mountHtml(await renderComponent(source, { mode: "inline", items }));
  const singleTriggers = single.querySelectorAll(
    ".wire-next__collapse-trigger",
  ) as HTMLButtonElement[];
  singleTriggers[0]?.click();
  singleTriggers[1]?.click();
  expect(singleTriggers[0]?.getAttribute("aria-expanded")).toBe("false");
  expect(singleTriggers[1]?.getAttribute("aria-expanded")).toBe("true");
  expect(single.querySelectorAll(".wire-next__collapse-preview")).toHaveLength(2);

  const multiple = mountHtml(await renderComponent(source, { multiple: true, items }));
  const multipleTriggers = multiple.querySelectorAll(
    ".wire-next__collapse-trigger",
  ) as HTMLButtonElement[];
  multipleTriggers[0]?.click();
  multipleTriggers[1]?.click();
  expect(multipleTriggers[0]?.getAttribute("aria-expanded")).toBe("true");
  expect(multipleTriggers[1]?.getAttribute("aria-expanded")).toBe("true");
});

test("basic form fields expose shared labels, variants, states, hints, values, and validation targets", async () => {
  const { checkField } = await import("../../validation/src/index.ts");
  const validation = checkField(
    {
      type: "string",
      optional: false,
      trim: true,
      rules: [{ kind: "email", message: "Enter a valid email address." }],
    },
    "not-an-email",
  );
  expect(validation.error).toBe("Enter a valid email address.");

  const source = readFileSync(uiComponentPath("Input"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      id: "account-email",
      name: "email",
      label: "Email address",
      placeholder: "you@example.com",
      value: "not-an-email",
      type: "email",
      variant: "floating",
      icon: "icon-[lucide--mail]",
      iconPosition: "end",
      helperText: "Used for account notices.",
      cornerHint: "Required",
      error: validation.error,
      inline: true,
      required: true,
    }),
  );
  const root = dom.querySelector(".wire-next--input") as HTMLElement;
  const input = dom.querySelector("input") as HTMLInputElement;
  expect(root.dataset.variant).toBe("floating");
  expect(root.dataset.inline).toBe("true");
  expect(root.dataset.invalid).toBe("true");
  expect(input.id).toBe("account-email");
  expect(input.name).toBe("email");
  expect(input.value).toBe("not-an-email");
  expect(input.getAttribute("aria-invalid")).toBe("true");
  expect(dom.querySelector("[data-error='email']")?.textContent).toContain(
    "Enter a valid email address.",
  );
  expect(dom.querySelector(".wire-next__field-hint")?.textContent).toContain("Required");
  expect(dom.querySelector(".wire-next__field-icon")).not.toBeNull();

  const emitted: string[] = [];
  for (const name of ["input", "change", "focus", "blur", "keydown", "keyup"]) {
    root.addEventListener(name, () => emitted.push(name));
  }
  input.dispatchEvent(new (dom.window as any).Event("input", { bubbles: true }));
  input.dispatchEvent(new (dom.window as any).Event("change", { bubbles: true }));
  input.dispatchEvent(new (dom.window as any).Event("focus", { bubbles: true }));
  input.dispatchEvent(new (dom.window as any).Event("blur", { bubbles: true }));
  input.dispatchEvent(
    new (dom.window as any).KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
  );
  input.dispatchEvent(
    new (dom.window as any).KeyboardEvent("keyup", { key: "Enter", bubbles: true }),
  );
  expect(emitted).toEqual(["input", "change", "focus", "blur", "keydown", "keyup"]);
});

test("all basic form components render values and their must-have interaction events", async () => {
  const components = [
    "Checkbox",
    "ColorPicker",
    "FileInput",
    "Input",
    "InputGroup",
    "Radio",
    "RangeSlider",
    "Select",
    "Switch",
    "Textarea",
    "TimePicker",
  ];
  for (const component of components) {
    const source = readFileSync(uiComponentPath(component), "utf8");
    const props: Record<string, unknown> = {
      id: `${component}-field`,
      name: `${component}Value`,
      label: `${component} label`,
      helperText: "Helper text",
      cornerHint: "Optional",
      error: "",
      inline: true,
      readonly: true,
      disabled: false,
      value: component === "RangeSlider" ? 40 : "value",
      options: [{ label: "Value", value: "value" }],
    };
    const dom = mountHtml(await renderComponent(source, props));
    const control = dom.querySelector("input, textarea, select") as HTMLElement | null;
    expect(control).not.toBeNull();
    expect(control?.getAttribute("name")).toBe(`${component}Value`);
    expect(dom.querySelector(".wire-next__field-help")?.textContent).toContain("Helper text");
    expect(dom.querySelector(`[data-error="${component}Value"]`)).not.toBeNull();
  }
});

test("radio renders passed options in group, card, list, alignment, and description layouts", async () => {
  const source = readFileSync(uiComponentPath("Radio"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      id: "plan",
      name: "plan",
      label: "Choose a plan",
      value: "professional",
      orientation: "horizontal",
      card: true,
      list: true,
      rightAligned: true,
      options: [
        { label: "Starter", value: "starter", description: "For individuals." },
        { label: "Professional", value: "professional", description: "For teams." },
        { label: "Enterprise", value: "enterprise", disabled: true },
      ],
    }),
  );

  const root = dom.querySelector(".wire-next--radio-field") as HTMLElement;
  const controls = [...dom.querySelectorAll('input[type="radio"]')] as HTMLInputElement[];
  expect(root.dataset.orientation).toBe("horizontal");
  expect(root.dataset.card).toBe("true");
  expect(root.dataset.list).toBe("true");
  expect(root.dataset.rightAligned).toBe("true");
  expect(controls).toHaveLength(3);
  expect(controls[1]?.checked).toBe(true);
  expect(controls[2]?.disabled).toBe(true);
  expect(dom.querySelectorAll(".wire-next__choice-copy small")).toHaveLength(2);
});

test("checkbox renders passed values in group, card, list, alignment, description, and mixed layouts", async () => {
  const source = readFileSync(uiComponentPath("Checkbox"), "utf8");
  const group = mountHtml(
    await renderComponent(source, {
      id: "notifications",
      name: "notifications",
      label: "Notification preferences",
      values: ["account"],
      orientation: "horizontal",
      card: true,
      list: true,
      rightAligned: true,
      options: [
        { label: "Product", value: "product", description: "Product news." },
        { label: "Account", value: "account", description: "Security alerts." },
        { label: "Partners", value: "partners", disabled: true },
      ],
    }),
  );
  const controls = [...group.querySelectorAll('input[type="checkbox"]')] as HTMLInputElement[];
  expect(group.querySelector(".wire-next--checkbox-field")?.getAttribute("data-orientation")).toBe(
    "horizontal",
  );
  expect(controls).toHaveLength(3);
  expect(controls[1]?.checked).toBe(true);
  expect(controls[2]?.disabled).toBe(true);
  expect(group.querySelectorAll(".wire-next__choice-copy small")).toHaveLength(2);

  const mixed = mountHtml(
    await renderComponent(source, { id: "mixed", name: "mixed", indeterminate: true }),
  );
  expect(mixed.querySelector("input")?.getAttribute("aria-checked")).toBe("mixed");
});

test("range slider exposes value, minimum, maximum, step, bounds, and optional marks", async () => {
  const source = readFileSync(uiComponentPath("RangeSlider"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      id: "volume",
      name: "volume",
      value: 50,
      min: 0,
      max: 100,
      step: 25,
      showBounds: true,
      showSteps: true,
      marks: [
        { value: 0, label: "Minimum" },
        { value: 50, label: "Middle" },
        { value: 100, label: "Maximum" },
      ],
    }),
  );

  const control = dom.querySelector('input[type="number"]') as HTMLInputElement;
  const slider = dom.querySelector('[role="slider"]') as HTMLElement;
  expect(control.min).toBe("0");
  expect(control.max).toBe("100");
  expect(control.step).toBe("25");
  expect(slider.getAttribute("aria-valuenow")).toBe("50");
  expect(dom.querySelector(".wire-next__range-bounds")?.textContent).toContain("Step 25");
  expect(dom.querySelectorAll(".wire-next__slider-marks button")).toHaveLength(3);
  expect(slider.getAttribute("tabindex")).toBe("0");
  expect(source).toContain('sourceEvent.key === "ArrowRight"');
  const increase = dom.querySelector('[aria-label="Increase Range"]') as HTMLButtonElement;
  increase.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
  expect(dom.querySelector('[role="slider"]')?.getAttribute("aria-valuenow")).toBe("75");
  dom
    .querySelector('[role="slider"]')
    ?.dispatchEvent(
      new (dom.window as any).KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }),
    );
  expect(dom.querySelector('[role="slider"]')?.getAttribute("aria-valuenow")).toBe("100");
  const middleMark = dom.querySelectorAll(
    ".wire-next__slider-marks button",
  )[1] as HTMLButtonElement;
  middleMark.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
  expect(dom.querySelector('[role="slider"]')?.getAttribute("aria-valuenow")).toBe("50");
  expect((dom.querySelector('input[type="number"]') as HTMLInputElement).value).toBe("50");
});

test("date picker renders a custom calendar trigger with accessible field state and events", async () => {
  const source = readFileSync(uiComponentPath("DatePicker"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      id: "start-date",
      name: "startDate",
      label: "Start date",
      value: "2026-07-28",
      min: "2026-01-01",
      max: "2026-12-31",
      helperText: "Use your local date.",
      cornerHint: "Required",
      required: true,
    }),
  );
  const input = dom.querySelector("input") as HTMLInputElement;
  expect(input.type).toBe("text");
  expect(input.required).toBe(true);
  expect(dom.querySelector("label")?.getAttribute("for")).toBe("start-date");
  expect(dom.querySelector('button[aria-haspopup="dialog"]')).not.toBeNull();
  expect(source).not.toContain('type="{type}"');
  expect(source).toContain("months =");
  expect(dom.querySelector(".wire-next__field-help")?.textContent).toContain("local date");
  const trigger = dom.querySelector('button[aria-haspopup="dialog"]') as HTMLButtonElement;
  expect(dom.querySelector(".wire-next--date-picker")?.getAttribute("data-expanded")).toBe("false");
  trigger.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
  expect(dom.querySelector(".wire-next--date-picker")?.getAttribute("data-expanded")).toBe("true");
  expect(dom.querySelector('button[aria-haspopup="dialog"]')?.getAttribute("aria-expanded")).toBe(
    "true",
  );
  expect(dom.querySelectorAll(".wire-next__calendar-grid button")).toHaveLength(31);
  const fifteenth = dom.querySelectorAll(
    ".wire-next__calendar-grid button",
  )[14] as HTMLButtonElement;
  fifteenth.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
  expect(input.value).toBe("2026-07-15");
  expect(dom.querySelector(".wire-next__date-trigger span")?.textContent).toBe("2026-07-15");
});

test("time picker uses a custom reactive panel instead of the browser time control", async () => {
  const source = readFileSync(uiComponentPath("TimePicker"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      id: "meeting-time",
      name: "meetingTime",
      value: "13:30",
      format: "12",
      minuteStep: 15,
    }),
  );
  expect(source).not.toContain('type="time"');
  const trigger = dom.querySelector(".wire-next__time-trigger") as HTMLButtonElement;
  expect(trigger.textContent).toContain("13:30");
  trigger.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
  expect(dom.querySelector(".wire-next--time-picker")?.getAttribute("data-expanded")).toBe("true");
  expect(dom.querySelectorAll(".wire-next__time-options").length).toBe(2);
  expect(dom.querySelectorAll(".wire-next__time-options button")).toHaveLength(36);
  const nine = [...dom.querySelectorAll(".wire-next__time-options button")].find(
    (button) => button.textContent?.trim() === "09",
  ) as HTMLButtonElement;
  nine.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
  expect((dom.querySelector('input[name="meetingTime"]') as HTMLInputElement).value).toBe("09:30");
  expect(dom.querySelector(".wire-next__time-trigger span")?.textContent).toBe("09:30");
});

test("file upload progress reacts to cancel, retry, and complete actions", async () => {
  const source = readFileSync(uiComponentPath("FileUploadProgress"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      fileName: "design-system.zip",
      fileSize: "20 MB",
      uploadedSize: "10 MB",
      value: 50,
      max: 100,
      status: "uploading",
    }),
  );
  expect(dom.querySelector(".wire-next--file-upload-progress")?.getAttribute("data-status")).toBe(
    "uploading",
  );
  expect(dom.querySelector(".wire-next__row")?.textContent).toContain("design-system.zip");
  const buttons = dom.querySelectorAll(".wire-next__upload-actions button") as HTMLButtonElement[];
  buttons[0]?.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
  expect(dom.querySelector(".wire-next--file-upload-progress")?.getAttribute("data-status")).toBe(
    "cancelled",
  );
});

test("carousel renders accessible slides, pagination, counter, and navigation events", async () => {
  const source = readFileSync(uiComponentPath("Carousel"), "utf8");
  const html = await renderComponent(source, {
    ariaLabel: "Featured stories",
    showPagination: true,
    showCounter: true,
    items: [
      { title: "First story", description: "First description" },
      { title: "Second story", description: "Second description" },
      { title: "Third story", description: "Third description" },
    ],
  });
  const dom = mountHtml(html);
  const carousel = dom.querySelector(".wire-next--carousel") as HTMLElement;
  const scopeRoot = dom.querySelector("[data-wrn-scope]") as HTMLElement;
  const encodedScope = scopeRoot.getAttribute("data-wrn-scope") ?? "";
  const hydratedScope = JSON.parse(Buffer.from(encodedScope, "base64").toString("utf8")) as {
    items: Array<{ title: string }>;
  };
  const events: string[] = [];
  for (const name of ["change", "next", "previous"]) {
    carousel.addEventListener(name, () => events.push(name));
  }

  expect(carousel.getAttribute("role")).toBe("region");
  expect(carousel.getAttribute("aria-roledescription")).toBe("carousel");
  expect(carousel.getAttribute("aria-label")).toBe("Featured stories");
  expect(hydratedScope.items.map((item) => item.title)).toEqual([
    "First story",
    "Second story",
    "Third story",
  ]);
  expect(dom.querySelectorAll('[aria-roledescription="slide"]')).toHaveLength(3);
  expect(dom.querySelectorAll(".wire-next__carousel-pagination button")).toHaveLength(3);
  expect(dom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("1 / 3");

  (dom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement).click();
  expect(dom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("2 / 3");
  expect(events).toEqual(["change", "next"]);

  (dom.querySelector('[aria-label="Previous slide"]') as HTMLButtonElement).click();
  expect(events).toEqual(["change", "next", "change", "previous"]);
});

test("carousel supports RTL, multiple slides, dragging, snap, and thumbnail layouts", async () => {
  const source = readFileSync(uiComponentPath("Carousel"), "utf8");
  const items = [{ title: "First" }, { title: "Second" }, { title: "Third" }, { title: "Fourth" }];
  const html = await renderComponent(source, {
    items,
    slidesPerView: 3,
    isRTL: true,
    isDraggable: true,
    isAutoHeight: true,
    thumbnails: "vertical",
  });
  const dom = mountHtml(html);
  const carousel = dom.querySelector(".wire-next--carousel") as HTMLElement;
  const track = dom.querySelector(".wire-next__carousel-track") as HTMLElement;

  expect(carousel.getAttribute("dir")).toBe("rtl");
  expect(carousel.dataset.draggable).toBe("true");
  expect(carousel.dataset.autoHeight).toBe("true");
  expect(carousel.dataset.thumbnails).toBe("vertical");
  expect(track.getAttribute("style")).toContain("--wire-carousel-per-view: 3");
  expect(dom.querySelectorAll(".wire-next__carousel-thumbnails button")).toHaveLength(4);

  const snapHtml = await renderComponent(source, {
    items,
    isSnap: true,
    isDraggable: true,
  });
  const snapDom = mountHtml(snapHtml);
  const snapRoot = snapDom.querySelector(".wire-next--carousel") as HTMLElement;
  expect(snapRoot.dataset.snap).toBe("true");
  expect(snapRoot.dataset.draggable).toBe("false");

  const css = uiStyles();
  expect(css).toContain('.wire-next--carousel[data-rtl="true"]');
  expect(css).toContain('.wire-next--carousel[data-snap="true"]');
  expect(css).toContain('.wire-next--carousel[data-thumbnails="vertical"]');
  expect(css).toContain("user-select: none");
  expect(css).toContain('.wire-next--carousel[data-centered="true"] .wire-next__carousel-track');
});

test("carousel autoplay timers are available in the browser reactive runtime", async () => {
  const { getReactiveRuntime } = await import("../../csr/src/index.ts");
  const runtime = getReactiveRuntime();
  expect(runtime).toContain('name === "setInterval"');
  expect(runtime).toContain('name === "clearInterval"');
});

test("carousel snap controls scroll and multiple slides stop at the last full group", async () => {
  const source = readFileSync(uiComponentPath("Carousel"), "utf8");
  const items = [
    { title: "First" },
    { title: "Second" },
    { title: "Third" },
    { title: "Fourth" },
    { title: "Fifth" },
    { title: "Sixth" },
  ];
  const snapDom = mountHtml(
    await renderComponent(source, {
      items,
      isSnap: true,
      showPagination: true,
    }),
  );
  const viewport = snapDom.querySelector(".wire-next__carousel-viewport") as HTMLElement & {
    scrollTo: (options: ScrollToOptions) => void;
  };
  let scrollLeft: number | undefined;
  viewport.scrollTo = (options: ScrollToOptions | number) => {
    scrollLeft = typeof options === "number" ? options : options.left;
  };
  (
    snapDom.querySelectorAll(".wire-next__carousel-pagination button")[1] as HTMLButtonElement
  ).click();
  expect(scrollLeft).toBeDefined();
  const snapNext = snapDom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement;
  for (let index = 0; index < items.length; index++) snapNext.click();
  const paginationButtons = snapDom.querySelectorAll(".wire-next__carousel-pagination button");
  expect(paginationButtons[items.length - 1]?.getAttribute("aria-current")).toBe("true");
  expect(snapNext.disabled).toBe(true);

  const multipleDom = mountHtml(
    await renderComponent(source, {
      items,
      slidesPerView: 3,
      showCounter: true,
    }),
  );
  const next = multipleDom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement;
  for (let index = 0; index < 6; index++) next.click();
  expect(multipleDom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("4 / 6");
  expect(next.disabled).toBe(true);
});

test("carousel centers the last slide and auto-scrolls thumbnail rails", async () => {
  const source = readFileSync(uiComponentPath("Carousel"), "utf8");
  const items = [{ title: "First" }, { title: "Second" }, { title: "Third" }, { title: "Fourth" }];
  const centeredDom = mountHtml(
    await renderComponent(source, {
      items,
      isCentered: true,
      showCounter: true,
    }),
  );
  const centeredNext = centeredDom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement;
  for (let index = 0; index < items.length; index++) centeredNext.click();
  expect(centeredDom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("4 / 4");
  expect(centeredNext.disabled).toBe(true);

  for (const thumbnails of ["horizontal", "vertical"]) {
    const dom = mountHtml(await renderComponent(source, { items, thumbnails }));
    const buttons = dom.querySelectorAll(
      ".wire-next__carousel-thumbnails button",
    ) as HTMLButtonElement[];
    let scrolled = false;
    const rail = buttons[1]?.parentElement as HTMLElement & {
      scrollTo: (options: ScrollToOptions) => void;
    };
    rail.scrollTo = () => {
      scrolled = true;
    };
    (dom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement).click();
    expect(scrolled).toBe(true);
  }
});

test("carousel autoplay wraps to the first slide", async () => {
  const source = readFileSync(uiComponentPath("Carousel"), "utf8");
  const html = await renderComponent(source, {
    isAutoPlay: true,
    autoplayInterval: 1000,
    activeIndex: 2,
    showCounter: true,
    items: [{ title: "First" }, { title: "Second" }, { title: "Third" }],
  });
  const dom = mountHtml(html);
  await Bun.sleep(1100);
  expect(dom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("1 / 3");
  dom
    .querySelector(".wire-next--carousel")
    ?.dispatchEvent(new (dom.window as any).Event("mouseenter", { bubbles: true }));
});

test("input number increments, decrements, applies steps, and respects limits", async () => {
  const source = readFileSync(uiComponentPath("InputNumber"), "utf8");
  const compiled = compileWireFile(source, uiComponentPath("InputNumber"));
  expect(compiled).toContain("export const __wrnexusStyles");
  expect(compiled).toContain(".wire-input-number__control");
  const html = await renderComponent(source, {
    name: "quantity",
    value: 1,
    min: 0,
    max: 5,
    step: 2,
  });
  const dom = mountHtml(html);
  const input = () => dom.querySelector('input[name="quantity"]') as HTMLInputElement;
  const decrement = () => dom.querySelector('[aria-label="Decrease value"]') as HTMLButtonElement;
  const increment = () => dom.querySelector('[aria-label="Increase value"]') as HTMLButtonElement;

  expect(input().value).toBe("1");

  increment().click();
  expect(input().value).toBe("3");

  increment().click();
  expect(input().value).toBe("5");
  expect(increment().disabled).toBe(true);

  decrement().click();
  expect(input().value).toBe("3");

  decrement().click();
  decrement().click();
  expect(input().value).toBe("0");
  expect(decrement().disabled).toBe(true);
});

test("advanced select opens, filters, and selects without reactive handler errors", async () => {
  const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
  const html = await renderComponent(source, {
    name: "team",
    options: [
      { value: "design", label: "Design" },
      { value: "engineering", label: "Engineering" },
      { value: "growth", label: "Growth" },
    ],
  });
  const dom = mountHtml(html);
  const trigger = dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement;
  expect(trigger).not.toBeNull();
  trigger.click();
  expect(
    dom.querySelector(".wire-next--advanced-select")?.classList.contains("wire-next--open"),
  ).toBe(true);
  expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("true");
  expect(dom.querySelector('[role="listbox"]')).not.toBeNull();
  const search = dom.querySelector(".wire-next__select-search input") as HTMLInputElement;
  search.value = "engine";
  search.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  const options = dom.querySelectorAll(".wire-next__select-option") as HTMLButtonElement[];
  expect(options.filter((option) => option.style.display !== "none")).toHaveLength(1);
  options.find((option) => option.style.display !== "none")?.click();
  expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe("Engineering");
  expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false");
  (dom.querySelector(".wire-next__clear-select") as HTMLButtonElement).click();
  expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe(
    "Select an option",
  );
  expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("");
});

test("advanced select clears, restores its placeholder, and keeps multiple counters accurate", async () => {
  const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
  const html = await renderComponent(source, {
    name: "team",
    multiple: true,
    values: ["design", "engineering"],
    showCounter: true,
    maxSelections: 3,
    placeholder: "Choose teams",
    options: [
      { value: "design", label: "Design" },
      { value: "engineering", label: "Engineering" },
      { value: "growth", label: "Growth" },
    ],
  });
  const dom = mountHtml(html);
  const trigger = dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement;
  const counter = dom.querySelector(".wire-next__row small");
  expect(counter?.textContent).toBe("2 / 3 selected");

  trigger.click();
  const buttons = dom.querySelectorAll(".wire-next__select-option") as HTMLButtonElement[];
  const scope = dom.querySelector("[data-wrn-select]") as HTMLElement & {
    __wrnexusScopeApi?: {
      get(name: string): unknown;
      call(name: string, value?: unknown): unknown;
    };
  };
  expect(scope.__wrnexusScopeApi?.get("maxSelections")).toBe(3);
  expect(scope.__wrnexusScopeApi?.call("selectedCount")).toBe(2);
  expect(scope.__wrnexusScopeApi?.call("canSelect", { value: "growth", label: "Growth" })).toBe(
    true,
  );
  expect(buttons.find((button) => button.textContent?.includes("Growth"))?.disabled).toBe(false);
  buttons.find((button) => button.textContent?.includes("Engineering"))?.click();
  expect(counter?.textContent).toBe("1 / 3 selected");
  buttons.find((button) => button.textContent?.includes("Design"))?.click();
  expect(counter?.textContent).toBe("0 / 3 selected");
  expect(
    dom.querySelector(".wire-next__placeholder")?.parentElement?.getAttribute("data-show"),
  ).toBe("true");
  expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("");
});

test("advanced select closes when a pointer event occurs outside it", async () => {
  const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
  const html = await renderComponent(source, {
    name: "team",
    options: [{ value: "design", label: "Design" }],
  });
  const dom = mountHtml(html);
  (dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement).click();
  expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("true");
  dom.document.body.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("pointerdown", { bubbles: true }),
  );
  expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false");
});

test("advanced select fetches remote options and appends infinite pages", async () => {
  const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
  const originalFetch = globalThis.fetch;
  const requests: string[] = [];
  globalThis.fetch = (async (input: string | URL | Request) => {
    const url = String(input);
    requests.push(url);
    const parsedUrl = new URL(url);
    const page = parsedUrl.searchParams.get("page");
    const query = parsedUrl.searchParams.get("q");
    return Response.json(
      page === "1"
        ? {
            items: query
              ? [{ value: "engineering", label: "Engineering" }]
              : [{ value: "design", label: "Design" }],
            hasMore: true,
          }
        : { items: [{ value: "support", label: "Support" }], hasMore: false },
    );
  }) as unknown as typeof fetch;

  try {
    const html = await renderComponent(source, {
      name: "team",
      options: [],
      remote: true,
      remoteUrl: "https://example.test/api/teams",
      infinite: true,
      hasMore: true,
      remoteDebounce: 0,
    });
    const dom = mountHtml(html);
    const root = dom.querySelector("[data-wrn-select]") as HTMLElement & {
      __wrnexusSelectController?: { attempts: number };
      __wrnexusScopeApi?: { get(name: string): unknown };
    };
    expect(root.getAttribute("data-remote")).toBe("true");
    expect(root.getAttribute("data-remote-url")).toBe("https://example.test/api/teams");
    expect(root.getAttribute("data-remote-auto-load")).toBe("true");
    expect(root.__wrnexusSelectController).toBeDefined();
    expect(root.__wrnexusScopeApi).toBeDefined();
    expect(root.__wrnexusScopeApi?.get("minSearchLength")).toBe(0);
    expect(root.__wrnexusSelectController?.attempts).toBe(1);
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(requests).toHaveLength(1);
    expect(
      dom.querySelectorAll(".wire-next__select-option").map((node) => node.textContent),
    ).toEqual(["Design"]);
    (dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement).click();
    (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click();
    expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("design");
    expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe("Design");

    const search = dom.querySelector(".wire-next__select-search input") as HTMLInputElement;
    search.value = "engine";
    search.dispatchEvent(
      new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
    );
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(requests.at(-1)).toContain("q=engine");
    expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("design");
    expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe("Design");

    (dom.querySelector("[data-wrn-select-load-more]") as HTMLButtonElement).click();
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(
      dom.querySelectorAll(".wire-next__select-option").map((node) => node.textContent),
    ).toEqual(["Engineering", "Support"]);
    expect(requests).toHaveLength(3);
    expect(requests[2]).toContain("page=2");
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test("combobox filters editable input, selects suggestions, exposes methods, and clears", async () => {
  const source = readFileSync(uiComponentPath("ComboBox"), "utf8");
  const html = await renderComponent(source, {
    name: "team",
    placeholder: "Search teams",
    options: [
      { value: "design", label: "Design", description: "Product design" },
      { value: "engineering", label: "Engineering", description: "Platform engineering" },
      { value: "growth", label: "Growth", description: "Customer growth" },
    ],
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-combobox]") as HTMLElement & {
    wrnexusCombobox?: {
      open(): void;
      close(): void;
      clear(): void;
      setValue(value: string): void;
    };
  };
  const input = dom.querySelector(".wire-next__combobox-input") as HTMLInputElement;
  const hidden = dom.querySelector('input[type="hidden"]') as HTMLInputElement;

  expect(root.wrnexusCombobox).toBeDefined();
  input.focus();
  input.value = "engi";
  input.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  expect(
    dom
      .querySelectorAll(".wire-next__select-option")
      .filter((option) => (option as HTMLElement).style.display !== "none")
      .map((option) => option.textContent?.trim()),
  ).toEqual(["EngineeringPlatform engineering"]);

  input.value = "No matching team";
  input.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false");
  expect(input.value).toBe("No matching team");

  input.value = "engi";
  input.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  input.dispatchEvent(
    new (dom.window as { KeyboardEvent: typeof KeyboardEvent }).KeyboardEvent("keydown", {
      key: "Enter",
      bubbles: true,
    }),
  );
  await new Promise((resolve) => setTimeout(resolve, 0));
  expect(input.value).toBe("Engineering");
  expect(hidden.value).toBe("engineering");
  expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false");

  root.wrnexusCombobox?.clear();
  expect(input.value).toBe("");
  expect(hidden.value).toBe("");
  root.wrnexusCombobox?.setValue("growth");
  expect(input.value).toBe("Growth");
  expect(hidden.value).toBe("growth");
});

test("combobox auto-loads remote suggestions and retains its selected value while searching", async () => {
  const source = readFileSync(uiComponentPath("ComboBox"), "utf8");
  const originalFetch = globalThis.fetch;
  const requests: string[] = [];
  globalThis.fetch = (async (input: string | URL | Request) => {
    const url = new URL(String(input));
    requests.push(url.toString());
    const query = url.searchParams.get("q");
    return Response.json({
      items: query
        ? [{ value: "engineering", label: "Engineering" }]
        : [{ value: "design", label: "Design" }],
      hasMore: false,
    });
  }) as typeof fetch;

  try {
    const html = await renderComponent(source, {
      name: "remote-team",
      remote: true,
      remoteUrl: "https://example.test/api/teams",
      remoteDebounce: 0,
    });
    const dom = mountHtml(html);
    const input = dom.querySelector(".wire-next__combobox-input") as HTMLInputElement;
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(requests).toHaveLength(1);

    input.focus();
    (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click();
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(input.value).toBe("Design");
    expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("design");

    input.value = "engine";
    input.dispatchEvent(
      new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
    );
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(requests.at(-1)).toContain("q=engine");
    expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("");
    (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click();
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(input.value).toBe("Engineering");
    expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe(
      "engineering",
    );
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test("combobox emits search, select, change, clear, open, and close events", async () => {
  const source = readFileSync(uiComponentPath("ComboBox"), "utf8");
  const html = await renderComponent(source, {
    name: "events-team",
    options: [
      { value: "design", label: "Design" },
      { value: "engineering", label: "Engineering" },
    ],
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-combobox]") as HTMLElement;
  const input = dom.querySelector(".wire-next__combobox-input") as HTMLInputElement;
  const received: Array<{ name: string; detail: Record<string, unknown> }> = [];
  for (const name of ["search", "select", "change", "clear", "open", "close"]) {
    root.addEventListener(`wrnexus:${name}`, ((event: CustomEvent) => {
      received.push({ name, detail: event.detail });
    }) as EventListener);
  }

  input.focus();
  await new Promise((resolve) => setTimeout(resolve, 0));
  input.value = "engi";
  input.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  input.dispatchEvent(
    new (dom.window as { KeyboardEvent: typeof KeyboardEvent }).KeyboardEvent("keydown", {
      key: "Enter",
      bubbles: true,
    }),
  );
  await new Promise((resolve) => setTimeout(resolve, 0));
  (dom.querySelector(".wire-next__clear-select") as HTMLButtonElement).click();
  await new Promise((resolve) => setTimeout(resolve, 0));

  expect(received.map((event) => event.name)).toEqual(
    expect.arrayContaining(["search", "select", "change", "clear", "open", "close"]),
  );
  expect(received.find((event) => event.name === "search")?.detail).toEqual(
    expect.objectContaining({ component: "ComboBox", query: "engi" }),
  );
  expect(received.find((event) => event.name === "select")?.detail).toEqual(
    expect.objectContaining({ value: "engineering" }),
  );
});

test("advanced select emits selection events and remote load and error events", async () => {
  const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
  const originalFetch = globalThis.fetch;
  const originalConsoleError = console.error;
  let rejectRequest = false;
  globalThis.fetch = (async () => {
    if (rejectRequest) throw new Error("Remote unavailable");
    return Response.json({
      items: [{ value: "design", label: "Design" }],
      hasMore: false,
    });
  }) as unknown as typeof fetch;
  console.error = () => {};

  try {
    const html = await renderComponent(source, {
      name: "advanced-events",
      options: [],
      remote: true,
      remoteUrl: "https://example.test/api/teams",
      remoteAutoLoad: true,
      remoteDebounce: 0,
    });
    const dom = mountHtml(html);
    const root = dom.querySelector("[data-wrn-select]") as HTMLElement & {
      __wrnexusSelectController?: { load(append: boolean, allowEmptyQuery?: boolean): void };
    };
    const received: Array<{ name: string; detail: Record<string, unknown> }> = [];
    for (const name of ["load", "error", "select", "change", "clear"]) {
      root.addEventListener(`wrnexus:${name}`, ((event: CustomEvent) => {
        received.push({ name, detail: event.detail });
      }) as EventListener);
    }

    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    (dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement).click();
    (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click();
    await new Promise((resolve) => setTimeout(resolve, 0));
    (dom.querySelector(".wire-next__clear-select") as HTMLButtonElement).click();
    await new Promise((resolve) => setTimeout(resolve, 0));

    rejectRequest = true;
    root.__wrnexusSelectController?.load(false, true);
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));

    expect(received.map((event) => event.name)).toEqual(
      expect.arrayContaining(["load", "select", "change", "clear", "error"]),
    );
    expect(received.find((event) => event.name === "load")?.detail).toEqual(
      expect.objectContaining({
        component: "AdvancedSelect",
        page: 1,
        hasMore: false,
      }),
    );
    expect(received.find((event) => event.name === "error")?.detail).toEqual(
      expect.objectContaining({
        component: "AdvancedSelect",
        message: "Remote unavailable",
      }),
    );
  } finally {
    globalThis.fetch = originalFetch;
    console.error = originalConsoleError;
  }
});

test("pin input accepts matching characters, advances focus, navigates backward, and completes", async () => {
  const source = readFileSync(uiComponentPath("PinInput"), "utf8");
  const html = await renderComponent(source, {
    name: "verificationCode",
    length: 4,
    pattern: "[0-9]",
    value: "",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-pin-input]") as HTMLElement & {
    wrnexusPinInput?: {
      focus(index?: number): void;
      clear(): void;
      setValue(value: string): void;
      getValue(): string;
    };
  };
  const cells = dom.querySelectorAll("[data-pin-cell]") as HTMLInputElement[];
  const hidden = dom.querySelector("[data-pin-value]") as HTMLInputElement;
  const events: string[] = [];
  const publicEvents: string[] = [];
  for (const name of ["input", "change", "complete", "clear"]) {
    root.addEventListener(`wrnexus:${name}`, () => events.push(name));
    root.addEventListener(name, () => publicEvents.push(name));
  }

  expect(cells).toHaveLength(4);
  expect(root.wrnexusPinInput).toBeDefined();
  cells[0].focus();
  cells[0].value = "x";
  cells[0].dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  expect(cells[0].value).toBe("");
  expect(hidden.value).toBe("");

  for (let index = 0; index < 4; index += 1) {
    cells[index].value = String(index + 1);
    cells[index].dispatchEvent(
      new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
    );
  }
  expect(hidden.value).toBe("1234");
  expect(root.getAttribute("data-complete")).toBe("true");
  expect(events).toContain("complete");
  expect(publicEvents).toContain("complete");

  cells[3].value = "";
  cells[3].dispatchEvent(
    new (dom.window as { KeyboardEvent: typeof KeyboardEvent }).KeyboardEvent("keydown", {
      key: "Backspace",
      bubbles: true,
    }),
  );
  expect(cells[2].value).toBe("");
  expect(hidden.value).toBe("12");

  root.wrnexusPinInput?.setValue("9876");
  expect(root.wrnexusPinInput?.getValue()).toBe("9876");
  root.wrnexusPinInput?.clear();
  expect(hidden.value).toBe("");
  expect(events).toContain("clear");
});

test("pin input renders four separate cells when length is omitted", async () => {
  const source = readFileSync(uiComponentPath("PinInput"), "utf8");
  const html = await renderComponent(source, {
    name: "defaultPin",
  });
  const dom = mountHtml(html);

  expect(dom.querySelectorAll("[data-pin-cell]")).toHaveLength(4);
});

test("pin input distributes filtered clipboard content and emits paste details", async () => {
  const source = readFileSync(uiComponentPath("PinInput"), "utf8");
  const html = await renderComponent(source, {
    name: "emailCode",
    length: 6,
    pattern: "[A-Z0-9]",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-pin-input]") as HTMLElement;
  const firstCell = dom.querySelector("[data-pin-cell]") as HTMLInputElement;
  let pasteDetail: Record<string, unknown> | undefined;
  root.addEventListener("wrnexus:paste", ((event: CustomEvent) => {
    pasteDetail = event.detail;
  }) as EventListener);

  const pasteEvent = new (dom.window as { Event: typeof Event }).Event("paste", {
    bubbles: true,
    cancelable: true,
  });
  Object.defineProperty(pasteEvent, "clipboardData", {
    value: { getData: () => "A-1b2 C3" },
  });
  firstCell.dispatchEvent(pasteEvent);

  expect((dom.querySelector("[data-pin-value]") as HTMLInputElement).value).toBe("A1B2C3");
  expect(pasteDetail).toEqual(
    expect.objectContaining({
      component: "PinInput",
      pasted: "A1B2C3",
      value: "A1B2C3",
    }),
  );
});

test("pin input accepts lowercase alphanumeric entry and normalizes it to the configured pattern", async () => {
  const source = readFileSync(uiComponentPath("PinInput"), "utf8");
  const html = await renderComponent(source, {
    name: "securityCode",
    length: 4,
    pattern: "[A-Z0-9]",
    inputMode: "text",
  });
  const dom = mountHtml(html);
  const cells = dom.querySelectorAll("[data-pin-cell]") as HTMLInputElement[];

  cells[0].value = "a";
  cells[0].dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );

  expect(cells[0].value).toBe("A");
  expect((dom.querySelector("[data-pin-value]") as HTMLInputElement).value).toBe("A");
});

test("strong password scores input, supports custom special characters, and opens its popover", async () => {
  const source = readFileSync(uiComponentPath("StrongPassword"), "utf8");
  const html = await renderComponent(source, {
    name: "newPassword",
    presentation: "popover",
    specialCharactersSet: "@#",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-strong-password]") as HTMLElement;
  const input = dom.querySelector('input[name="newPassword"]') as HTMLInputElement;
  const strengthEvents: Array<{ level: string; percent: number }> = [];
  root.addEventListener("strength", (event) => {
    strengthEvents.push(
      (event as unknown as CustomEvent<{ level: string; percent: number }>).detail,
    );
  });

  expect(input.minLength).toBe(8);
  expect(root.dataset.strength).toBe("empty");

  input.value = "Secure#9";
  input.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  expect((dom.querySelector("[data-wrn-strong-password]") as HTMLElement).dataset.strength).toBe(
    "strong",
  );
  expect((dom.querySelector("[data-wrn-strong-password]") as HTMLElement).dataset.score).toBe("5");
  expect(strengthEvents.at(-1)).toMatchObject({ level: "strong", percent: 100 });

  const updatedInput = dom.querySelector('input[name="newPassword"]') as HTMLInputElement;
  updatedInput.value = "Secure!9";
  updatedInput.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }),
  );
  expect((dom.querySelector("[data-wrn-strong-password]") as HTMLElement).dataset.strength).toBe(
    "good",
  );
  expect(strengthEvents.at(-1)?.percent).toBe(80);

  const currentInput = dom.querySelector('input[name="newPassword"]') as HTMLInputElement;
  currentInput.dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("focus", { bubbles: true }),
  );
  expect(
    (dom.querySelector(".wire-next__strong-password-popover") as HTMLElement).dataset.open,
  ).toBe("true");
});

test("strong password completed requirements use a proper checkmark indicator", () => {
  const css = uiStyles();

  expect(css).toContain(
    '.wire-next__strong-password-requirements li[data-met="true"] > span[aria-hidden="true"]::after',
  );
  expect(css).toContain("transform: rotate(45deg)");
  expect(css).toContain("border-width: 0 0.1rem 0.1rem 0");
  expect(css).not.toContain(
    "linear-gradient(135deg, transparent 42%, var(--wire-color-surface) 42% 52%",
  );
});

test("toggle count switches pricing values and emits accessible change details", async () => {
  const source = readFileSync(uiComponentPath("ToggleCount"), "utf8");
  const html = await renderComponent(source, {
    name: "billing",
    value: "monthly",
    firstValue: "monthly",
    firstLabel: "Monthly",
    secondValue: "annual",
    secondLabel: "Annual",
    animate: false,
    items: [
      { label: "Startup", monthly: 19, annual: 190 },
      { label: "Team", monthly: 89, annual: 890 },
    ],
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-toggle-count]") as HTMLElement;
  let detail: Record<string, unknown> | undefined;
  root.addEventListener("toggle", (event) => {
    detail = (event as unknown as CustomEvent<Record<string, unknown>>).detail;
  });

  expect(root.dataset.value).toBe("monthly");
  expect(
    dom
      .querySelectorAll(".wire-next__toggle-count-items strong > span")
      .map((node) => node.textContent),
  ).toEqual(["19", "89"]);

  (dom.querySelector('button[aria-pressed="false"]') as HTMLButtonElement).click();

  const updatedRoot = dom.querySelector("[data-wrn-toggle-count]") as HTMLElement;
  expect(updatedRoot.dataset.value).toBe("annual");
  expect(
    dom
      .querySelectorAll(".wire-next__toggle-count-items strong > span")
      .map((node) => node.textContent),
  ).toEqual(["190", "890"]);
  expect((dom.querySelector('input[name="billing"]') as HTMLInputElement).value).toBe("annual");
  expect(dom.querySelector('button[aria-pressed="true"]')?.textContent).toBe("Annual");
  expect(detail).toMatchObject({
    component: "ToggleCount",
    name: "billing",
    value: "annual",
    previousValue: "monthly",
  });
});

test("toggle count animates every price upward and downward", async () => {
  const source = readFileSync(uiComponentPath("ToggleCount"), "utf8");
  const html = await renderComponent(source, {
    value: "monthly",
    animationDuration: 40,
    animationSteps: 4,
    items: [
      { label: "Startup", monthly: 2, annual: 10 },
      { label: "Team", monthly: 4, annual: 20 },
    ],
  });
  const dom = mountHtml(html);
  const values = () =>
    dom.querySelectorAll("[data-toggle-count-value]").map((node) => Number(node.textContent));
  const button = (label: string) =>
    dom
      .querySelectorAll(".wire-next__toggle-count-segmented button")
      .find((node) => node.textContent === label) as HTMLButtonElement;

  button("Annual").click();
  expect(values()).toEqual([2, 4]);
  await new Promise((resolve) => setTimeout(resolve, 15));
  expect(values().every((value) => Number.isInteger(value))).toBe(true);
  await new Promise((resolve) => setTimeout(resolve, 80));
  expect(values()).toEqual([10, 20]);

  button("Monthly").click();
  expect(values()).toEqual([10, 20]);
  await new Promise((resolve) => setTimeout(resolve, 15));
  expect(values().every((value) => Number.isInteger(value))).toBe(true);
  await new Promise((resolve) => setTimeout(resolve, 80));
  expect(values()).toEqual([2, 4]);
});

test("toggle count switch exposes switch semantics and respects disabled state", async () => {
  const source = readFileSync(uiComponentPath("ToggleCount"), "utf8");
  const html = await renderComponent(source, {
    variant: "switch",
    value: "monthly",
    disabled: true,
    items: [{ label: "Startup", monthly: 19, annual: 190 }],
  });
  const dom = mountHtml(html);
  const control = dom.querySelector('[role="switch"]') as HTMLButtonElement;

  expect(control.getAttribute("aria-checked")).toBe("false");
  expect(control.disabled).toBe(true);
  control.click();
  expect((dom.querySelector("[data-wrn-toggle-count]") as HTMLElement).dataset.value).toBe(
    "monthly",
  );
});

test("toggle password reveals and conceals its value with accessible declared events", async () => {
  const source = readFileSync(uiComponentPath("TogglePassword"), "utf8");
  const html = await renderComponent(source, {
    name: "accountPassword",
    value: "correct-horse",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector("[data-wrn-toggle-password]") as HTMLElement;
  const input = dom.querySelector('input[name="accountPassword"]') as HTMLInputElement;
  const toggle = dom.querySelector(".wire-next__password-toggle") as HTMLButtonElement;
  const showIcon = dom.querySelector(".wire-next__password-icon--show") as HTMLElement;
  const visibility: boolean[] = [];
  root.addEventListener("toggle", (event) => {
    visibility.push((event as unknown as CustomEvent).detail.visible);
  });

  expect(input.type).toBe("password");
  expect(input.pattern).toBe("(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}");
  expect(showIcon).not.toBeNull();
  expect(toggle.getAttribute("aria-pressed")).toBe("false");
  toggle.click();
  expect(input.type).toBe("text");
  expect(input.value).toBe("correct-horse");
  expect(toggle.getAttribute("aria-pressed")).toBe("true");
  expect(visibility).toEqual([true]);
  toggle.click();
  expect(input.type).toBe("password");
  expect(visibility).toEqual([true, false]);
});

test("toggle password supports checkbox control and synchronized password fields", async () => {
  const source = readFileSync(uiComponentPath("TogglePassword"), "utf8");
  const synchronizedHtml = await renderComponent(source, {
    name: "credentials",
    fields: [
      {
        label: "New password",
        name: "newPassword",
        placeholder: "Enter new password",
        autocomplete: "new-password",
      },
      {
        label: "Current password",
        name: "currentPassword",
        value: "existing-secret",
        autocomplete: "current-password",
        pattern: ".{12,}",
      },
    ],
  });
  const synchronizedDom = mountHtml(synchronizedHtml);
  const synchronizedInputs = synchronizedDom.querySelectorAll(
    ".wire-next__password-control > input",
  ) as HTMLInputElement[];
  const synchronizedToggle = synchronizedDom.querySelector(
    ".wire-next__password-toggle",
  ) as HTMLButtonElement;

  expect(synchronizedInputs).toHaveLength(2);
  expect(synchronizedInputs[0].pattern).toBe(
    "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}",
  );
  expect(synchronizedInputs[1].pattern).toBe(".{12,}");
  expect(synchronizedInputs.map((input) => input.type)).toEqual(["password", "password"]);
  synchronizedToggle.click();
  expect(synchronizedInputs.map((input) => input.type)).toEqual(["text", "text"]);

  const checkboxHtml = await renderComponent(source, {
    name: "checkboxPassword",
    toggleMode: "checkbox",
  });
  const checkboxDom = mountHtml(checkboxHtml);
  const checkbox = checkboxDom.querySelector(
    ".wire-next__password-checkbox input",
  ) as HTMLInputElement;
  const checkboxPassword = checkboxDom.querySelector(
    ".wire-next__password-control > input",
  ) as HTMLInputElement;
  checkbox.checked = true;
  checkbox.dispatchEvent(
    new (checkboxDom.window as { Event: typeof Event }).Event("change", { bubbles: true }),
  );

  expect(checkboxPassword.type).toBe("text");
});

test("pagination renders windowed page numbers and emits change", async () => {
  const source = readFileSync(uiComponentPath("Pagination"), "utf8");
  const html = await renderComponent(source, {
    page: 5,
    pageSize: 10,
    total: 200,
    variant: "numbered",
    siblingCount: 1,
  });
  const dom = mountHtml(html);
  const root = dom.querySelector(".wire-pagination") as HTMLElement;

  expect(root.getAttribute("role")).toBe("navigation");
  const current = dom.querySelector('.wire-pagination__page[aria-current="page"]');
  expect(current!.textContent!.trim()).toBe("5");
  expect(root.textContent).toContain("41");
  expect(root.textContent).toContain("50");
  expect(root.textContent).toContain("200");
});

test("pagination clamps an out-of-range page instead of rendering nothing", async () => {
  const source = readFileSync(uiComponentPath("Pagination"), "utf8");
  const html = await renderComponent(source, { page: 99, pageSize: 10, total: 30 });
  const dom = mountHtml(html);
  const current = dom.querySelector(".wire-pagination__compact");
  expect(current!.textContent).toContain("3");
});

test("pagination survives an empty dataset", async () => {
  const source = readFileSync(uiComponentPath("Pagination"), "utf8");
  const html = await renderComponent(source, { page: 1, pageSize: 10, total: 0 });
  const dom = mountHtml(html);
  expect(dom.querySelector(".wire-pagination")).not.toBeNull();
});

test("stepper marks complete, current and upcoming steps", async () => {
  const source = readFileSync(uiComponentPath("Stepper"), "utf8");
  const html = await renderComponent(source, {
    steps: [
      { label: "Account", description: "Your details" },
      { label: "Billing", description: "Payment method" },
      { label: "Confirm", description: "Review and submit" },
    ],
    active: 1,
  });
  const dom = mountHtml(html);
  const items = [...dom.querySelectorAll(".wire-stepper__step")];

  expect(items).toHaveLength(3);
  expect(items[0]!.getAttribute("data-status")).toBe("complete");
  expect(items[1]!.getAttribute("data-status")).toBe("current");
  expect(items[2]!.getAttribute("data-status")).toBe("upcoming");
  expect(items[1]!.getAttribute("aria-current")).toBe("step");
  // One root wraps the list, the panels and the controls; the steps
  // themselves are still an ordered list, which is what carries the semantics.
  expect(dom.querySelector(".wire-stepper")!.tagName.toLowerCase()).toBe("div");
  expect(dom.querySelector(".wire-stepper__list")!.tagName.toLowerCase()).toBe("ol");
});

test("stepper renders vertically and clamps an out-of-range active index", async () => {
  const source = readFileSync(uiComponentPath("Stepper"), "utf8");
  const html = await renderComponent(source, {
    steps: [{ label: "One" }, { label: "Two" }],
    active: 99,
    orientation: "vertical",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector(".wire-stepper") as HTMLElement;
  expect(root.getAttribute("data-orientation")).toBe("vertical");
  const items = [...dom.querySelectorAll(".wire-stepper__step")];
  expect(items[1]!.getAttribute("data-status")).toBe("current");
});

test("clickable stepper opts into roving focus", async () => {
  const source = readFileSync(uiComponentPath("Stepper"), "utf8");
  const html = await renderComponent(source, {
    steps: [{ label: "One" }, { label: "Two" }],
    active: 0,
    clickable: true,
  });
  const dom = mountHtml(html);
  expect(dom.querySelector('[data-wrn-roving="horizontal"]')).not.toBeNull();
  expect(dom.querySelectorAll("[data-wrn-roving-item]").length).toBe(2);
});

test("nav renders links, marks the active one, and shows icons and badges", async () => {
  const source = readFileSync(uiComponentPath("Nav"), "utf8");
  const html = await renderComponent(source, {
    items: [
      { label: "Home", href: "/", value: "home", icon: "icon-[lucide--house]" },
      { label: "Inbox", href: "/inbox", value: "inbox", badge: "9" },
      { label: "Archive", href: "/archive", value: "archive", disabled: true },
    ],
    active: "inbox",
  });
  const dom = mountHtml(html);

  expect(dom.querySelector(".wire-nav")!.getAttribute("role")).toBe("navigation");
  const current = dom.querySelector('[aria-current="page"]');
  expect(current!.textContent).toContain("Inbox");
  // Every item renders a badge span; the ones without a badge are empty and
  // carry data-show="false", so match on content rather than first-in-document.
  const badges = [...dom.querySelectorAll(".wire-nav__badge")]
    .map((node) => node.textContent!.trim())
    .filter(Boolean);
  expect(badges).toContain("9");
  expect(dom.querySelector(".wire-nav__icon")).not.toBeNull();
  expect(dom.querySelector('[aria-disabled="true"]')).not.toBeNull();
});

test("nav renders a submenu with a disclosure arrow and opts into roving focus", async () => {
  const source = readFileSync(uiComponentPath("Nav"), "utf8");
  const html = await renderComponent(source, {
    items: [
      {
        label: "Products",
        value: "products",
        items: [
          { label: "Overview", href: "/p", value: "p-overview" },
          {
            label: "More",
            value: "p-more",
            items: [{ label: "Deep", href: "/d", value: "deep" }],
          },
        ],
      },
    ],
    active: "p-overview",
  });
  const dom = mountHtml(html);

  expect(dom.querySelector('[data-wrn-roving="horizontal"]')).not.toBeNull();
  expect(dom.querySelectorAll("[data-wrn-roving-item]").length).toBeGreaterThan(0);
  expect(dom.querySelector(".wire-nav__arrow")).not.toBeNull();
  expect(dom.querySelector(".wire-nav__submenu")).not.toBeNull();
  expect(dom.querySelector(".wire-nav__submenu--level3")).not.toBeNull();
  // Every second-level item renders a third-level list; only the one whose
  // item actually has children is populated, so search across them all.
  const thirdLevel = [...dom.querySelectorAll(".wire-nav__submenu--level3")]
    .map((node) => node.textContent!.trim())
    .filter(Boolean);
  expect(thirdLevel.join(" ")).toContain("Deep");
});

test("nav renders its shell with no items and rejects a non-array", async () => {
  const source = readFileSync(uiComponentPath("Nav"), "utf8");
  const html = await renderComponent(source, { items: [], active: "" });
  const dom = mountHtml(html);
  expect(dom.querySelector(".wire-nav")).not.toBeNull();
  expect(dom.querySelectorAll(".wire-nav__link").length).toBe(0);

  // A declared unknown[] prop is coerced by the framework, which rejects a
  // non-array before the component runs. The itemList() guard is the second
  // line of defence for a missing prop, not a way to render junk.
  await expect(renderComponent(source, { items: "not-an-array", active: "" })).rejects.toThrow(
    "Expected an array prop",
  );
});

test("nav submenus are anchored so the viewport clamp can pull them back", async () => {
  const source = readFileSync(uiComponentPath("Nav"), "utf8");
  const html = await renderComponent(source, {
    items: [
      {
        label: "Products",
        value: "products",
        items: [{ label: "Overview", href: "/p", value: "p" }],
      },
    ],
    active: "p",
  });
  const dom = mountHtml(html);
  // visibility:hidden keeps layout, so the clamp can place a submenu correctly
  // before it is ever shown -- which is why CSS-driven hover still composes
  // with the runtime clamp.
  expect(dom.querySelectorAll(".wire-nav__submenu[data-wrn-anchored]").length).toBeGreaterThan(0);
});

test("tabs use wire classes and declare real outputs instead of raw events", async () => {
  const source = readFileSync(uiComponentPath("Tabs"), "utf8");
  // The whole point of the rewrite: themeable wire-* classes, not Tailwind.
  expect(source).toContain("outputs {");
  expect(source).toContain("output.change(");
  expect(source).not.toContain("new CustomEvent(");

  const html = await renderComponent(source, {
    items: [
      { label: "Overview", value: "overview", content: "First panel" },
      { label: "Pricing", value: "pricing", content: "Second panel" },
    ],
    active: "overview",
  });
  const dom = mountHtml(html);

  const list = dom.querySelector('[role="tablist"]') as HTMLElement;
  expect(list.getAttribute("data-wrn-roving")).toBe("horizontal");
  const tabs = dom.querySelectorAll('[role="tab"]');
  expect(tabs).toHaveLength(2);
  expect(tabs[0]!.getAttribute("aria-selected")).toBe("true");
  expect(tabs[1]!.getAttribute("aria-selected")).toBe("false");
  expect(dom.querySelector(".wire-tabs")).not.toBeNull();
  expect(dom.querySelectorAll('[role="tabpanel"]')).toHaveLength(2);
});

test("tabs in url mode declare the query parameter they sync to", async () => {
  const source = readFileSync(uiComponentPath("Tabs"), "utf8");
  const html = await renderComponent(source, {
    items: [
      { label: "One", value: "one" },
      { label: "Two", value: "two" },
    ],
    active: "one",
    mode: "url",
    param: "tab",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector(".wire-tabs") as HTMLElement;
  expect(root.getAttribute("data-mode")).toBe("url");
  expect(root.getAttribute("data-param")).toBe("tab");

  /*
   * In url mode the query parameter is the source of truth rather than
   * component state. The client router owns popstate and swaps the whole page
   * on back and forward, discarding component state, so the selection has to
   * fall out of the URL for history to work at all.
   */
  const source2 = readFileSync(uiComponentPath("Tabs"), "utf8");
  expect(source2).toContain("URLSearchParams(window.location.search)");
  expect(source2).toContain("history.pushState");
});

test("tabs vertical orientation switches the roving axis", async () => {
  const source = readFileSync(uiComponentPath("Tabs"), "utf8");
  const html = await renderComponent(source, {
    items: [{ label: "One", value: "one" }],
    active: "one",
    orientation: "vertical",
  });
  const dom = mountHtml(html);
  expect(dom.querySelector('[role="tablist"]')!.getAttribute("data-wrn-roving")).toBe("vertical");
});

test("mega menu renders column groups in an anchored panel", async () => {
  const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
  const html = await renderComponent(source, {
    label: "Products",
    columns: [
      {
        heading: "Platform",
        items: [
          { label: "Runtime", href: "/runtime", description: "The browser half" },
          { label: "Compiler", href: "/compiler" },
        ],
      },
      { heading: "Company", items: [{ label: "About", href: "/about" }] },
    ],
  });
  const dom = mountHtml(html);

  const trigger = dom.querySelector(".wire-mega__trigger") as HTMLElement;
  expect(trigger.getAttribute("aria-expanded")).toBe("false");
  expect(trigger.getAttribute("aria-haspopup")).toBe("true");

  const panel = dom.querySelector(".wire-mega__panel") as HTMLElement;
  expect(panel.getAttribute("data-wrn-anchored")).toBe("true");
  expect(dom.querySelectorAll(".wire-mega__column")).toHaveLength(2);
  expect(dom.querySelectorAll(".wire-mega__link").length).toBeGreaterThanOrEqual(3);
  expect(dom.querySelector(".wire-mega__heading")!.textContent).toContain("Platform");
  // Single level by design: a mega panel shows breadth flat, so there is no
  // nested submenu markup to find.
  expect(source).not.toContain("wire-mega__submenu");
});

test("mega menu columns take roving focus and survive an empty column list", async () => {
  const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
  const html = await renderComponent(source, { label: "Empty", columns: [] });
  const dom = mountHtml(html);
  expect(dom.querySelector(".wire-mega")).not.toBeNull();
  expect(dom.querySelectorAll(".wire-mega__link")).toHaveLength(0);
  expect(dom.querySelector('[data-wrn-roving="both"]')).not.toBeNull();
});

test("sidebar renders single items, labelled groups and nested levels", async () => {
  const source = readFileSync(uiComponentPath("Sidebar"), "utf8");
  const html = await renderComponent(source, {
    label: "Workspace",
    items: [
      { label: "Dashboard", href: "/", value: "dash", icon: "icon-[lucide--gauge]" },
      {
        heading: "Projects",
        items: [
          { label: "Active", href: "/active", value: "active" },
          {
            label: "Archive",
            value: "archive",
            items: [{ label: "2025", href: "/a/2025", value: "a2025" }],
          },
        ],
      },
    ],
    active: "active",
  });
  const dom = mountHtml(html);

  expect(dom.querySelector(".wire-sidebar")).not.toBeNull();
  // Every entry renders a heading node; only group entries fill it in, so
  // match on content rather than first-in-document.
  const headings = [...dom.querySelectorAll(".wire-sidebar__heading")]
    .map((node) => node.textContent!.trim())
    .filter(Boolean);
  expect(headings).toContain("Projects");
  expect(dom.querySelector('[aria-current="page"]')!.textContent).toContain("Active");
  expect(dom.querySelector(".wire-sidebar__icon")).not.toBeNull();
  // Third level exists and is reachable.
  const deep = [...dom.querySelectorAll(".wire-sidebar__sublist--level3")]
    .map((n) => n.textContent!.trim())
    .filter(Boolean);
  expect(deep.join(" ")).toContain("2025");
  expect(dom.querySelector('[data-wrn-roving="vertical"]')).not.toBeNull();
});

test("sidebar composes Drawer for its off-canvas presentation", async () => {
  const source = readFileSync(uiComponentPath("Sidebar"), "utf8");
  // Reusing Drawer means the focus trap and scroll lock come for free rather
  // than being reimplemented here.
  expect(source).toContain('import Drawer from "./Drawer.wrn"');
  expect(source).toContain("<Drawer");
});

test("scrollspy renders section links and marks the runtime observation target", async () => {
  const source = readFileSync(uiComponentPath("Scrollspy"), "utf8");
  const html = await renderComponent(source, {
    label: "On this page",
    items: [
      { label: "Overview", href: "#overview" },
      { label: "Install", href: "#install" },
      { label: "Usage", href: "#usage" },
    ],
    active: "#install",
  });
  const dom = mountHtml(html);

  const root = dom.querySelector(".wire-scrollspy") as HTMLElement;
  expect(root.getAttribute("role")).toBe("navigation");
  // The runtime owns which link is current; it needs a marker to find the nav.
  expect(root.getAttribute("data-wrn-scrollspy")).toBe("true");

  const links = [...dom.querySelectorAll(".wire-scrollspy__link")];
  expect(links).toHaveLength(3);
  expect(links[1]!.getAttribute("aria-current")).toBe("location");
  expect(links[0]!.getAttribute("aria-current")).toBe("false");
  expect(links[1]!.getAttribute("href")).toBe("#install");
});

test("scrollspy survives an empty item list", async () => {
  const source = readFileSync(uiComponentPath("Scrollspy"), "utf8");
  const html = await renderComponent(source, { items: [] });
  const dom = mountHtml(html);
  expect(dom.querySelector(".wire-scrollspy")).not.toBeNull();
  expect(dom.querySelectorAll(".wire-scrollspy__link")).toHaveLength(0);
});

test("navbar uses a valid aria-current and takes roving focus on its menu", async () => {
  const source = readFileSync(uiComponentPath("Navbar"), "utf8");
  /*
   * aria-current="" is not a valid value -- the attribute has to be absent or
   * carry a token. Emitting an empty string made every link look like it
   * declared a state it did not have.
   */
  expect(source).not.toContain("? 'page' : ''");
  expect(source).toContain("data-wrn-roving");

  const html = await renderComponent(source, {
    label: "Main",
    items: [
      { label: "Home", href: "/", value: "home" },
      { label: "Docs", href: "/docs", value: "docs" },
    ],
    active: "docs",
  });
  const dom = mountHtml(html);
  const menus = dom.querySelector(".wire-navbar__menus") as HTMLElement;
  expect(menus.getAttribute("data-wrn-roving")).toBe("horizontal");
  /*
   * Asserted against the rendered markup rather than the parsed DOM: the test
   * DOM drops aria-current from these anchors, while the served HTML carries
   * it correctly.
   */
  expect(html).toContain('href="/docs"');
  expect(html).toMatch(/aria-current="page"/);
  expect(html).not.toMatch(/aria-current=""/);
});

test("breadcrumb marks the trail end without emitting an empty aria-current", async () => {
  const source = readFileSync(uiComponentPath("Breadcrumb"), "utf8");
  const html = await renderComponent(source, {
    items: [
      { label: "Docs", href: "/docs" },
      { label: "Components", href: "/docs/components" },
      { label: "Breadcrumb" },
    ],
  });
  // aria-current="" is not a valid value; the attribute takes a token.
  expect(html).not.toMatch(/aria-current=""/);
  expect(html).toMatch(/aria-current="page"/);
  const dom = mountHtml(html);
  expect(dom.querySelector(".wire-breadcrumb")!.getAttribute("aria-label")).toBeTruthy();
  expect(dom.querySelectorAll(".wire-breadcrumb__item").length).toBeGreaterThanOrEqual(3);
});

test("mega menu bridges the gap between its trigger and panel", async () => {
  const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
  /*
   * The panel is offset below the trigger, and that offset is dead space
   * belonging to neither element: travelling to the panel fired mouseleave on
   * the root and closed the menu before the pointer arrived. A descendant
   * covering the gap keeps the pointer inside the component.
   */
  expect(source).toContain(".wire-mega__panel::before");
  expect(source).toMatch(/\.wire-mega__panel::before \{[^}]*bottom: 100%/s);
});

test("stepper shows only the active step content and offers back, next and skip", async () => {
  const source = readFileSync(uiComponentPath("Stepper"), "utf8");
  const html = await renderComponent(source, {
    steps: [
      { label: "Account", content: "ACCOUNT BODY" },
      { label: "Billing", content: "BILLING BODY" },
      { label: "Confirm", content: "CONFIRM BODY" },
    ],
    active: 1,
    showPanel: true,
    controls: true,
    allowSkip: true,
  });
  const dom = mountHtml(html);

  const panels = [...dom.querySelectorAll(".wire-stepper__panel")];
  expect(panels).toHaveLength(3);
  /*
   * Asserted through aria-hidden rather than data-show. data-show is emitted
   * verbatim for the client runtime to evaluate, so its server-rendered value
   * is not a reliable statement about which step is showing; aria-hidden is
   * interpolated at render and says exactly that.
   */
  expect(panels.map((p) => p.getAttribute("aria-hidden"))).toEqual(["true", "false", "true"]);

  expect(dom.querySelector(".wire-stepper__back")).not.toBeNull();
  expect(dom.querySelector(".wire-stepper__next")).not.toBeNull();
  expect(dom.querySelector(".wire-stepper__skip")).not.toBeNull();
});

test("stepper back is disabled on the first step and next becomes finish on the last", async () => {
  const source = readFileSync(uiComponentPath("Stepper"), "utf8");
  const steps = [{ label: "One" }, { label: "Two" }];

  const first = mountHtml(await renderComponent(source, { steps, active: 0, controls: true }));
  expect(first.querySelector(".wire-stepper__back")!.getAttribute("disabled")).not.toBeNull();
  expect(first.querySelector(".wire-stepper__next")!.textContent).toContain("Next");

  const last = mountHtml(await renderComponent(source, { steps, active: 1, controls: true }));
  expect(last.querySelector(".wire-stepper__next")!.textContent).toContain("Finish");
});

test("stepper next can be gated so a form can hold it until the step validates", async () => {
  const source = readFileSync(uiComponentPath("Stepper"), "utf8");
  const html = await renderComponent(source, {
    steps: [{ label: "One" }, { label: "Two" }],
    active: 0,
    controls: true,
    nextDisabled: true,
  });
  const dom = mountHtml(html);
  expect(dom.querySelector(".wire-stepper__next")!.getAttribute("disabled")).not.toBeNull();
});

test("layout splitter renders two panes and an operable separator", async () => {
  const source = readFileSync(uiComponentPath("LayoutSplitter"), "utf8");
  /*
   * It used to declare resizeStart, resize and resizeEnd with no pointer
   * handling at all, so a caller wired up @resize and received nothing for
   * ever. The behaviour now lives in the runtime behind these markers.
   */
  expect(source).toContain("data-wrn-splitter");
  expect(source).toContain("data-wrn-splitter-handle");
  /*
   * The output is sizeChange, not resize. An output named after a native DOM
   * event is emitted by the component but never reaches the parent binding.
   */
  expect(source).toContain("sizeChange(payload:");
  expect(source).not.toMatch(/^\s*resize\(payload:/m);

  const html = await renderComponent(source, {
    orientation: "horizontal",
    size: 40,
    minSize: 20,
    label: "Resize panels",
  });
  const dom = mountHtml(html);

  const root = dom.querySelector(".wire-splitter") as HTMLElement;
  expect(root.getAttribute("data-wrn-splitter")).toBe("horizontal");
  expect(root.getAttribute("style")).toContain("--wrn-split");

  const handle = dom.querySelector("[data-wrn-splitter-handle]") as HTMLElement;
  expect(handle.getAttribute("role")).toBe("separator");
  expect(handle.getAttribute("tabindex")).toBe("0");
  expect(handle.getAttribute("aria-valuenow")).toBe("40");
  expect(handle.getAttribute("aria-valuemin")).toBe("20");
  expect(handle.getAttribute("aria-valuemax")).toBe("80");
  expect(handle.getAttribute("aria-orientation")).toBe("vertical");

  expect(dom.querySelectorAll(".wire-splitter__pane")).toHaveLength(2);
});

test("layout splitter clamps an out-of-range size and flips orientation", async () => {
  const source = readFileSync(uiComponentPath("LayoutSplitter"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, { orientation: "vertical", size: 95, minSize: 25 }),
  );
  const handle = dom.querySelector("[data-wrn-splitter-handle]") as HTMLElement;
  // 95 is past the 75 bound implied by minSize, so it clamps rather than
  // leaving one pane unrecoverable.
  expect(handle.getAttribute("aria-valuenow")).toBe("75");
  // A vertical splitter stacks, so its separator is a horizontal bar.
  expect(handle.getAttribute("aria-orientation")).toBe("horizontal");
});

test("custom scrollbar styles the scrollbar and drops its fake output", async () => {
  const source = readFileSync(uiComponentPath("CustomScrollbar"), "utf8");
  /*
   * It declared a scroll output it never emitted, and its props were columns,
   * gap and maxWidth copied from a grid scaffold. A caller can listen for a
   * plain scroll event on the element, so the output is gone rather than left
   * unimplemented.
   */
  expect(source).not.toContain("outputs {");
  // Matched as a declaration: the word still appears in the comment recording
  // why those props were wrong.
  expect(source).not.toMatch(/^\s*columns:/m);
  expect(source).not.toMatch(/^\s*gap:/m);
  expect(source).toContain("scrollbar-color");
  expect(source).toContain("::-webkit-scrollbar");

  const html = await renderComponent(source, {
    axis: "vertical",
    thickness: 10,
    maxHeight: "18rem",
  });
  const dom = mountHtml(html);
  const root = dom.querySelector(".wire-scrollbar") as HTMLElement;
  expect(root.getAttribute("data-axis")).toBe("vertical");
  const style = root.getAttribute("style") || "";
  expect(style).toContain("--scrollbar-thickness");
  expect(style).toContain("18rem");
});

test("layout components ship their own styles instead of Tailwind utilities", () => {
  /*
   * These were built from utility classes and class: conditionals. That works
   * only where Tailwind is present, and a variant cost a dozen lines of
   * conditionals. They now carry wire-* classes, a local style block, and
   * data attributes the style block selects on.
   */
  const migrated = [
    "Container",
    "Columns",
    "Grid",
    "Divider",
    "Image",
    "Link",
    "Typography",
    "Kbd",
    "LayoutSplitter",
    "CustomScrollbar",
    "Section",
    "SectionHeader",
    "PublicPageShell",
    "PageHeader",
  ];
  const utility =
    /class:(grid-cols-|sm:|lg:|md:|max-w-|gap-[0-9]|px-[0-9]|py-[0-9]|border-l|bg-\[|h-0|text-xs|items-)/;

  for (const name of migrated) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    expect({ name, hasStyleBlock: /^\s{2}style \{/m.test(source) }).toEqual({
      name,
      hasStyleBlock: true,
    });
    expect({ name, usesUtilityConditionals: utility.test(source) }).toEqual({
      name,
      usesUtilityConditionals: false,
    });
    expect({ name, hasWireClass: /class='wire-[a-z-]+/.test(source) }).toEqual({
      name,
      hasWireClass: true,
    });
  }
});

test("remaining utility-styled components use local BEM styles", () => {
  const migrated = ["List", "InputNumber", "Marquee", "TextLink", "Map", "SearchBox", "Timeline"];
  const utilityMarkup =
    /class(?::[^=\s]+)?=['"][^'"]*\b(?:flex|grid|w-full|items-center|justify-between|gap-[0-9]|p[xytrbl]?-[0-9]|m[xytrbl]?-[0-9]|text-(?:xs|sm|lg|xl)|rounded-(?:md|lg|xl|2xl|full)|border-[lrtbxy]?|bg-\[|shadow-(?:sm|lg)|size-[0-9])\b/;

  for (const name of migrated) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    const markup = source.split(/^\s{2,4}style \{/m)[0] ?? source;
    expect({ name, hasStyleBlock: /^\s{2,4}style \{/m.test(source) }).toEqual({
      name,
      hasStyleBlock: true,
    });
    expect({ name, utilityMarkup: utilityMarkup.test(markup) }).toEqual({
      name,
      utilityMarkup: false,
    });
  }
});

test("every wire color token a component references is defined by the theme", () => {
  /*
   * Ten tokens were referenced by components and defined by nothing:
   * --wire-color-focus, --wire-color-surface-soft, --wire-color-on-danger and
   * the input-* family. An undefined custom property does not warn, it simply
   * resolves to nothing, so focus rings drew with no colour and soft surfaces
   * rendered transparent.
   *
   * Checked against the rendered theme CSS rather than the source: most of
   * these tokens are derived per palette, so they never appear as literals.
   */
  const css = renderThemeCss(resolveThemeConfig());
  const defined = new Set(
    [...css.matchAll(/(--wire-color-[a-z0-9-]+)\s*:/g)].map((match) => match[1]!),
  );

  const missing = new Map<string, string[]>();
  for (const name of uiComponentNames()) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    for (const match of source.matchAll(/var\((--wire-color-[a-z0-9-]+)/g)) {
      const token = match[1]!;
      if (defined.has(token)) continue;
      if (!missing.has(token)) missing.set(token, []);
      const users = missing.get(token)!;
      if (users.length < 4 && !users.includes(name)) users.push(name);
    }
  }

  expect([...missing].map(([token, users]) => `${token} <- ${users.join(", ")}`)).toEqual([]);
});

test("no component gains an output that nothing ever emits", () => {
  /*
   * An output only reaches a parent @binding when the component calls
   * output.<name>(). The runtime keeps parent handlers in a registry that only
   * invokeComponentOutput reads, so a component that instead dispatches its own
   * CustomEvent -- even a bubbling one, on its own root -- is emitting into
   * nothing: the parent binding is never invoked and no error is raised.
   * Eighteen components did exactly that and were converted; this pins what is
   * left, which are components with no emitter of any kind.
   *
   * Native event names are excluded, and that exclusion is real rather than
   * assumed: invokeComponentOutput falls back to dispatchComponentEvent when no
   * handler is registered, and a parent @click on a component tag is also bound
   * as an ordinary DOM listener, so a natively-named output does arrive.
   */
  const native = new Set([
    "click",
    "focus",
    "blur",
    "input",
    "change",
    "submit",
    "copy",
    "paste",
    "load",
    "error",
    "scroll",
    "toggle",
    "drop",
    "dragstart",
    "dragend",
    "dragenter",
    "dragleave",
    "dragover",
    "keydown",
    "keyup",
    "select",
  ]);
  const runtime = readFileSync(
    join(uiComponentsDir(), "..", "..", "csr", "src", "reactive-runtime.ts"),
    "utf8",
  );

  const offenders: string[] = [];
  for (const name of uiComponentNames()) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    const block = /^ {2}outputs \{([\s\S]*?)^ {2}\}/m.exec(source);
    if (!block) continue;
    for (const match of block[1]!.matchAll(/^\s*([A-Za-z][A-Za-z0-9_]*)\s*\(/gm)) {
      const output = match[1]!;
      if (source.slice(block.index).includes(`output.${output}(`)) continue;
      if (source.includes("output[")) continue;
      if (native.has(output.toLowerCase())) continue;
      // Emitted by a runtime controller written for this component.
      if (runtime.includes(`"${output}"`) && new RegExp(name, "i").test(runtime)) continue;
      offenders.push(`${name}.${output}`);
    }
  }

  /*
   * A ceiling, not a target. It only ever moves down: rebuilding one of these
   * components should tighten it.
   */
  expect(offenders.length).toBe(0);
  expect(offenders).not.toContain("LayoutSplitter.sizeChange");
  expect(offenders).not.toContain("CustomScrollbar.scroll");
});

test("list selects items without hrefs as well as navigation items", async () => {
  const source = readFileSync(uiComponentPath("List"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      title: "Actions",
      items: [{ label: "Run report" }],
    }),
  );
  const root = dom.querySelector('[data-ui-component="List"]') as HTMLElement;
  const events: Array<{ item: { label: string }; index: number }> = [];
  root.addEventListener("select", (event) => {
    events.push((event as CustomEvent).detail);
  });

  (root.querySelector("li") as HTMLElement).click();

  expect(events).toEqual([{ item: { label: "Run report" }, index: 0 }]);
});

test("chart renders accessible SVG bars and emits point and legend outputs", async () => {
  const source = readFileSync(uiComponentPath("Chart"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      items: [
        { label: "Alpha", value: 12 },
        { label: "Beta", value: 6 },
      ],
    }),
  );
  const root = dom.querySelector('[data-ui-component="Chart"]') as HTMLElement;
  const events: string[] = [];
  for (const name of ["dataPointClick", "select", "legendToggle"]) {
    root.addEventListener(name, () => events.push(name));
  }
  expect(root.querySelectorAll("svg rect")).toHaveLength(2);
  (root.querySelector("svg rect") as SVGElement).dispatchEvent(
    new (dom.window as { Event: typeof Event }).Event("click", { bubbles: true }),
  );
  (root.querySelector(".wire-chart__legend button") as HTMLButtonElement).click();
  expect(events).toEqual(["dataPointClick", "select", "legendToggle"]);
});

test("tree view expands branches, selects nodes, and emits public outputs", async () => {
  const source = readFileSync(uiComponentPath("TreeView"), "utf8");
  const dom = mountHtml(
    await renderComponent(source, {
      items: [{ value: "docs", label: "Docs", children: [{ value: "guide", label: "Guide" }] }],
    }),
  );
  const root = dom.querySelector('[data-ui-component="TreeView"]') as HTMLElement;
  const events: string[] = [];
  for (const name of ["toggle", "expand", "select"])
    root.addEventListener(name, () => events.push(name));
  (root.querySelector(".wire-tree-view__toggle") as HTMLButtonElement).click();
  expect(root.querySelector(".wire-tree-view__group")?.getAttribute("data-show")).toBe("true");
  (root.querySelector('[aria-level="2"] .wire-tree-view__node') as HTMLButtonElement).click();
  expect(events).toEqual(["toggle", "expand", "select"]);
});

test("confetti completes deferred bursts and clipboard reports successful copies", async () => {
  const confetti = mountHtml(
    await renderComponent(readFileSync(uiComponentPath("Confetti"), "utf8"), {
      duration: 5,
      count: 4,
    }),
  );
  const confettiRoot = confetti.querySelector('[data-ui-component="Confetti"]') as HTMLElement;
  const confettiEvents: string[] = [];
  confettiRoot.addEventListener("start", () => confettiEvents.push("start"));
  confettiRoot.addEventListener("complete", () => confettiEvents.push("complete"));
  (confettiRoot.querySelector("button") as HTMLButtonElement).click();
  await new Promise((resolve) => setTimeout(resolve, 15));
  expect(confettiEvents).toEqual(["start", "complete"]);

  const clipboard = mountHtml(
    await renderComponent(readFileSync(uiComponentPath("Clipboard"), "utf8"), {
      value: "npm add wrnexus",
    }),
  );
  Object.defineProperty((clipboard.window as { navigator: object }).navigator, "clipboard", {
    configurable: true,
    value: { writeText: () => Promise.resolve() },
  });
  const clipboardRoot = clipboard.querySelector('[data-ui-component="Clipboard"]') as HTMLElement;
  const clipboardEvents: string[] = [];
  for (const name of ["copy", "success"])
    clipboardRoot.addEventListener(name, () => clipboardEvents.push(name));
  (clipboardRoot.querySelector("button") as HTMLButtonElement).click();
  await new Promise((resolve) => setTimeout(resolve, 0));
  expect(clipboardEvents).toEqual(["copy", "success"]);
  expect(clipboardRoot.dataset.status).toBe("success");
});

test("a declared output is never emitted as a hand-built CustomEvent", () => {
  /*
   * The failure this prevents is silent in both directions: the component
   * looks like it emits, the caller looks like it listens, and the event
   * bubbles right past the binding because the runtime resolves parent
   * handlers from a registry rather than from the DOM. Verified in a browser
   * before this test was written -- an AnnouncementBar dispatching its own
   * bubbling "dismiss" never reached a page-level @dismiss, and the same
   * component reached it immediately once it called output.dismiss().
   *
   * Dispatching on window is a different thing and stays allowed: that is how
   * Toaster, Modal and DataTable signal across component boundaries, where
   * there is no parent binding to reach.
   */
  const offenders: string[] = [];
  for (const name of uiComponentNames()) {
    const source = readFileSync(uiComponentPath(name), "utf8");
    const block = /^ {2}outputs \{([\s\S]*?)^ {2}\}/m.exec(source);
    if (!block) continue;

    const declared = new Set(
      [...block[1]!.matchAll(/^\s*([A-Za-z][A-Za-z0-9_]*)\s*\(/gm)].map((m) => m[1]!),
    );

    for (const match of source.matchAll(
      /(\w+(?:\.\w+)*)\.dispatchEvent\(|initCustomEvent\(\s*"([A-Za-z][A-Za-z0-9_]*)"/g,
    )) {
      const target = match[1];
      if (target && /^window\b/.test(target)) continue;

      // Which event name is being built here?
      const around = source.slice(Math.max(0, match.index - 400), match.index + 200);
      for (const declaredName of declared) {
        const quoted = `"${declaredName}"`;
        if (
          around.includes(`CustomEvent(${quoted}`) ||
          around.includes(`initCustomEvent(${quoted}`)
        ) {
          offenders.push(`${name}.${declaredName}`);
        }
      }
    }
  }

  expect([...new Set(offenders)]).toEqual([]);
});
