import { expect, test } from "bun:test";
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { compileWireFile } from "../../../packages/compiler/src/index.ts";
import { parse as parseWireSyntax } from "../../../packages/syntax/src/parser.ts";

const root = join(import.meta.dir, "..");
const pagesDir = join(root, "app", "pages");
const detailPagesDir = join(pagesDir, "components");
const reference = JSON.parse(
  readFileSync(join(root, "..", "..", "packages", "ui", "component-reference.json"), "utf8"),
) as {
  count: number;
  components: Array<{
    name: string;
    mount: string;
    category: string;
    props: Array<{ name: string }>;
    slots: string[];
    events: string[];
  }>;
};
const manifest = JSON.parse(readFileSync(join(root, "showcase-manifest.json"), "utf8")) as {
  componentCount: number;
  categoryCount: number;
  totalDemoCount: number;
  components: Array<{
    name: string;
    mount: string;
    slug: string;
    category: string;
    demoCount: number;
    propCount: number;
    slots: string[];
    events: string[];
    profiled: boolean;
  }>;
};

const detailSource = (slug: string) => readFileSync(join(detailPagesDir, `${slug}.wrn`), "utf8");

const interactiveOverlayNames = [
  "ContextMenu",
  "Drawer",
  "Dropdown",
  "Modal",
  "Popover",
  "Tooltip",
];

function mountCount(source: string, mount: string): number {
  const tag = new RegExp(`<${mount}(?:\\s|>|/)`, "g");
  const legacy = new RegExp(`<div\\s+data-component="${mount}"`, "g");
  return (source.match(tag)?.length ?? 0) + (source.match(legacy)?.length ?? 0);
}

test("showcase configures local Lucide icon generation", () => {
  const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
    devDependencies: Record<string, string>;
  };
  const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");

  expect(packageJson.devDependencies["@iconify/tailwind4"]).toBeTruthy();
  expect(packageJson.devDependencies["@iconify-json/lucide"]).toBeTruthy();
  expect(styles).toContain('@plugin "@iconify/tailwind4";');
  expect(styles).toContain(".playground-preview-source > .wire-action:not(.w-full)");
  expect(styles).toContain('[data-ui-component="MetricGrid"]');
  expect(styles).toContain("width: 100%;");
});

test("showcase manifest matches the generated UI reference", () => {
  expect(manifest.componentCount).toBe(reference.count);
  expect(manifest.components).toHaveLength(reference.count);
  expect(manifest.totalDemoCount).toBeGreaterThanOrEqual(reference.count * 3);
  expect(new Set(manifest.components.map((component) => component.slug)).size).toBe(
    reference.count,
  );

  for (const component of reference.components) {
    const documented = manifest.components.find((entry) => entry.name === component.name);
    expect(documented).toBeTruthy();
    expect(documented?.mount).toBe(component.mount);
    expect(documented?.events).toEqual(component.events);
    expect(documented?.slots).toEqual(component.slots);
  }
});

test("showcase uses a searchable documentation shell", () => {
  const layout = readFileSync(join(root, "app", "layouts", "showcase.wrn"), "utf8");
  const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
  const runtime = readFileSync(join(root, "public", "playground.js"), "utf8");

  expect(layout).toContain("data-docs-directory");
  expect(layout).toContain("data-docs-search");
  expect(layout.match(/data-docs-component-link/g)).toHaveLength(reference.count);
  expect(styles).toContain("grid-template-columns: 17rem minmax(0, 1fr)");
  expect(runtime).toContain("data-docs-menu-toggle");
  expect(runtime).toContain('event.key.toLowerCase() === "k"');
});

test("showcase SSR document layout maps persisted design cookies to html attributes", () => {
  const source = readFileSync(join(root, "app", "layouts", "document.wrn"), "utf8");
  expect(source).toContain("<html");
  expect(source).toContain("<head></head>");
  expect(source).toContain('<div id="app"><slot /></div>');
  for (const setting of ["style", "palette", "mode", "font", "scale"]) {
    expect(source).toContain(`data-ui-${setting}`);
    expect(source).toContain(`cookies['wrn-ui-${setting}']`);
  }
});

test("category pages preview every UI component exactly once", () => {
  const categoryPages = readdirSync(pagesDir)
    .filter((file) => manifest.components.some((component) => `${component.category}.wrn` === file))
    .map((file) => readFileSync(join(pagesDir, file), "utf8"))
    .join("\n");

  const invalidPreviews = reference.components.flatMap((component) => {
    const count = mountCount(categoryPages, component.mount);
    return count === 1
      ? []
      : [{ name: component.name, mount: component.mount, category: component.category, count }];
  });

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

test("every component has a detail page, playground, and manifest-driven demos", () => {
  const files = readdirSync(detailPagesDir).filter((entry) => entry.endsWith(".wrn"));
  expect(files).toHaveLength(reference.count);

  for (const component of manifest.components) {
    const source = detailSource(component.slug);
    expect(source).toContain("data-playground-preview");
    expect(source).toContain("data-playground-form");
    expect(source).toContain("data-playground-code");
    expect(source).toContain('href="#playground"');
    expect(source.match(/class="demo-workbench"/g)).toHaveLength(component.demoCount);
    expect(mountCount(source, component.mount)).toBeGreaterThanOrEqual(component.demoCount + 1);
  }
}, 20_000);

test("playgrounds preserve typed values, safe DOM replacement, themes, and event output", () => {
  const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
  const runtime = readFileSync(join(root, "public", "playground.js"), "utf8");

  expect(generator).toContain("JSON.parse(");
  expect(generator).toContain("Number(");
  expect(generator).toContain("data-playground-json");
  expect(generator).toContain("prop.name}='{${playgroundStateName(prop)}}'");
  expect(generator).toContain('data-slot="${escapeAttribute(slot)}"');
  const generatorWithoutDataSlots = generator.replaceAll(
    'data-slot="${escapeAttribute(slot)}"',
    "",
  );
  expect(generatorWithoutDataSlots).not.toContain('slot="${escapeAttribute(slot)}"');

  expect(runtime).toContain("fetch(url");
  expect(runtime).toContain("navigator.clipboard.writeText");
  expect(runtime).toContain("preview.replaceChildren");
  expect(runtime).not.toContain("preview.innerHTML");
  expect(runtime).toContain("__wrnexusHydrateScopes");
  expect(runtime).toContain("wireTheme?.bind?.(document)");
  expect(runtime).toContain("bindPlaygroundEvents");
  expect(runtime).toContain('attributes.push(`${name}="${field.checked}"`)');
});

test("playground enum controls use public profiles instead of inferred implementation comparisons", () => {
  const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
  expect(generator).toContain("profileFor(component.name)?.options?.[prop.name]");
  expect(generator).not.toContain("source.matchAll(pattern)");

  const pageHeader = manifest.components.find((component) => component.name === "PageHeader");
  if (pageHeader) {
    const source = detailSource(pageHeader.slug);
    expect(source).toContain('name="pg_icon" type="text"');
    expect(source).toMatch(/<option value="default"(?: selected)?>default<\/option>/);
    expect(source).toMatch(/<option value="solid"(?: selected)?>solid<\/option>/);
  }
});

test("every declared public event is documented and visible in the playground", () => {
  for (const component of manifest.components.filter((entry) => entry.events.length > 0)) {
    const source = detailSource(component.slug);
    expect(source).toContain('id="events"');
    expect(source).toContain('href="#events"');
    expect(source).toContain("data-playground-event-log");
    for (const event of component.events) {
      expect(source).toContain(`<code>@${event}</code>`);
      expect(source).toContain(`@${event}='console.log(payload)'`);
    }
  }
});

test("new and recently repaired components have production use-case profiles", () => {
  const expectedProfiles = [
    "PublicPageShell",
    "AnnouncementBar",
    "Breadcrumb",
    "PageHeader",
    "Hero",
    "HeroActions",
    "SplitHero",
    "Section",
    "SectionHeader",
    "MarketingSectionHeader",
    "TextLink",
    "FeatureGrid",
    "FeatureCard",
    "FeatureIconCard",
    "MetricGrid",
    "MetricCard",
    "StatsBar",
    "CTASection",
    "BackToTop",
    "Footer",
    "ContextMenu",
    "Drawer",
    "Dropdown",
    "Modal",
    "Popover",
    "Tooltip",
  ];

  for (const name of expectedProfiles) {
    const component = manifest.components.find((entry) => entry.name === name);
    if (!component) continue;
    expect(component.profiled).toBe(true);
    const source = detailSource(component.slug);
    expect(source.match(/class="demo-workbench"/g)?.length ?? 0).toBeGreaterThanOrEqual(3);
  }
});

test("grid, hero, shell, and overlay detail pages use responsive showcase stages", () => {
  const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
  for (const name of [
    "MetricGrid",
    "FeatureGrid",
    "StatsBar",
    "Hero",
    "SplitHero",
    "AnnouncementBar",
    "CTASection",
    "Footer",
  ]) {
    expect(styles).toContain(`[data-ui-component="${name}"]`);
  }

  const metricGrid = manifest.components.find((component) => component.name === "MetricGrid");
  if (metricGrid) {
    const source = detailSource(metricGrid.slug);
    expect(source).toContain('columns="3"');
    const metricGridReference = reference.components.find(
      (component) => component.name === "MetricGrid",
    );
    if (metricGridReference?.props.some((prop) => prop.name === "maxWidth")) {
      expect(source).toContain('maxWidth="full"');
    }
    expect(source).toContain("items='[");
  }
});

test("overlay previews start closed and expose usable click, hover, or context triggers", () => {
  const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
  const profiles = readFileSync(join(root, "scripts", "showcase-profiles.mjs"), "utf8");
  const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");

  expect(generator).toContain("interactiveOverlayComponents");
  expect(generator).toContain("overlayVisibilityProps.has(name)");
  expect(generator).toContain(
    'data-interactive-preview="${isInteractiveOverlay(component) ? "true" : "false"}"',
  );
  expect(styles).toContain('.catalog-card[data-interactive-preview="true"]');
  expect(styles).toContain('[data-ui-component="Drawer"]');
  expect(styles).toContain('[data-ui-component="Modal"]');

  const overlaysPage = readFileSync(join(pagesDir, "overlays.wrn"), "utf8");
  for (const name of interactiveOverlayNames) {
    const component = manifest.components.find((entry) => entry.name === name);
    if (!component) continue;

    const detail = detailSource(component.slug);
    expect(detail).toContain("demo-interaction-hint");
    expect(detail).not.toMatch(new RegExp(`<${component.mount}[^>]*(?:open|defaultOpen)="true"`));
    expect(overlaysPage).not.toMatch(
      new RegExp(`<${component.mount}[^>]*(?:open|defaultOpen)="true"`),
    );
  }

  expect(profiles).toContain('trigger: "contextmenu"');
  expect(profiles).toContain('trigger: "click"');
  expect(profiles).toContain('trigger: "hover"');
  expect(profiles).toContain('trigger: "both"');
  expect(profiles).toContain('triggerLabel: "Open drawer"');
  expect(profiles).toContain('triggerLabel: "Open modal"');
  expect(profiles).toContain('triggerLabel: "View details"');
});

test("advanced select demos use validation schemas and a real remote API", () => {
  const component = manifest.components.find((entry) => entry.name === "AdvancedSelect");
  if (!component) return;
  const page = detailSource(component.slug);
  expect(page).toContain('data-schema="advanced-select-validation"');
  expect(page).toContain('data-schema="advanced-select-dynamic-validation"');
  expect(readFileSync(join(root, "app", "api", "teams.ts"), "utf8")).toContain("export const GET");
});

test("specialized form component examples remain complete", () => {
  const checks: Array<[string, string[]]> = [
    ["PinInput", ["Different lengths", 'length="3"', 'length="5"', 'length="7"']],
    [
      "TogglePassword",
      [
        "Checkbox-controlled toggle",
        "Synchronized password fields",
        'data-schema="toggle-password-validation"',
      ],
    ],
    [
      "StrongPassword",
      ["Live strength meter", "Requirements in a popover", 'specialCharactersSet="@#_-."'],
    ],
  ];
  for (const [name, needles] of checks) {
    const component = manifest.components.find((entry) => entry.name === name);
    if (!component) continue;
    const source = detailSource(component.slug);
    for (const needle of needles) expect(source).toContain(needle);
  }
});

test("complex component props are complete readable multiline values", () => {
  for (const name of ["AdvancedSelect", "ComboBox", "Select", "TogglePassword"]) {
    const component = manifest.components.find((entry) => entry.name === name);
    if (!component) continue;
    const source = detailSource(component.slug);
    expect(source).not.toMatch(/\b(?:options|groups|items|fields)='\[\.\.\.\]'/);
  }
});

test("generated playground boolean states avoid multiline-sensitive ternaries", () => {
  const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
  expect(generator).toContain('?? "${initial}") === "true"');
  expect(generator).not.toContain("=== null ? ${initial} :");

  for (const component of manifest.components) {
    const source = detailSource(component.slug);
    expect(source).not.toMatch(/state\s+playground_[A-Za-z0-9_$]+\s*=.*===\s*null\s*\?/);
  }
});

test("every generated showcase page compiles and is accepted by the route syntax parser", () => {
  const paths = [
    ...readdirSync(pagesDir)
      .filter((entry) => entry.endsWith(".wrn"))
      .map((entry) => join(pagesDir, entry)),
    ...readdirSync(detailPagesDir)
      .filter((entry) => entry.endsWith(".wrn"))
      .map((entry) => join(detailPagesDir, entry)),
  ];

  for (const path of paths) {
    const source = readFileSync(path, "utf8");
    expect(() => parseWireSyntax(source)).not.toThrow();
    expect(() => compileWireFile(source, path)).not.toThrow();
  }
});
