import { test, expect } from "bun:test";
import {
  buildRouter,
  compileRoutePattern,
  fileToRoute,
  generateRoutesFile,
  matchRoute,
  sortRoutes,
  type Route,
} from "../src/index.ts";

function route(raw: string): Route {
  return { raw, file: raw, ...compileRoutePattern(raw) };
}

test("generateRoutesFile emits a Routes map with param types", () => {
  const code = generateRoutesFile([
    route("/"),
    route("/about"),
    route("/users/[id]"),
    route("/docs/[[...slug]]"),
  ]);
  expect(code).toContain('"/": Record<string, never>;');
  expect(code).toContain('"/about": Record<string, never>;');
  expect(code).toContain('"/users/[id]": { "id": string };');
  expect(code).toContain('"/docs/[[...slug]]": { "slug"?: string | readonly string[] };');
  expect(code).toContain("export function href");
  expect(code).toContain('"users.id": "/users/[id]";');
  expect(code).toContain("export function route");
});

test("generated href() fills required, optional and catch-all params", async () => {
  const code = generateRoutesFile([
    route("/"),
    route("/users/[id]"),
    route("/org/[org]/team/[team]"),
    route("/blog/[[page]]"),
    route("/docs/[...slug]"),
  ]);
  const { tmpdir } = await import("node:os");
  const { writeFileSync, mkdirSync } = await import("node:fs");
  const { join } = await import("node:path");
  const { pathToFileURL } = await import("node:url");
  const dir = join(tmpdir(), "wrnexus-routes-test");
  mkdirSync(dir, { recursive: true });
  const file = join(dir, `r${Date.now()}.ts`);
  writeFileSync(file, code);
  const mod = (await import(pathToFileURL(file).href)) as {
    href: (p: string, params?: Record<string, string | string[]>) => string;
    route: (
      name: string,
      params?: Record<string, string | string[]>,
      query?: Record<string, string | number | boolean>,
    ) => string;
  };
  expect(mod.href("/")).toBe("/");
  expect(mod.href("/users/[id]", { id: "42" })).toBe("/users/42");
  expect(mod.href("/org/[org]/team/[team]", { org: "acme", team: "core" })).toBe(
    "/org/acme/team/core",
  );
  expect(mod.href("/users/[id]", { id: "a b" })).toBe("/users/a%20b");
  expect(mod.href("/blog/[[page]]")).toBe("/blog");
  expect(mod.href("/docs/[...slug]", { slug: ["guide", "start here"] })).toBe(
    "/docs/guide/start%20here",
  );
  expect(mod.route("users.id", { id: "42" }, { tab: "profile", compact: true })).toBe(
    "/users/42?tab=profile&compact=true",
  );
});

test("matches optional and catch-all routes", () => {
  expect(matchRoute([route("/blog/[[page]]")], "/blog")?.params).toEqual({});
  expect(matchRoute([route("/blog/[[page]]")], "/blog/2")?.params).toEqual({ page: "2" });
  expect(matchRoute([route("/docs/[...slug]")], "/docs/a/b")?.params).toEqual({ slug: "a/b" });
  expect(matchRoute([route("/docs/[[...slug]]")], "/docs")?.params).toEqual({});
});

test("static routes sort ahead of dynamic and catch-all routes", () => {
  const sorted = sortRoutes([route("/docs/[...slug]"), route("/docs/[id]"), route("/docs/new")]);
  expect(sorted.map((item) => item.raw)).toEqual(["/docs/new", "/docs/[id]", "/docs/[...slug]"]);
});

test("route groups and page/index filenames do not affect URLs", () => {
  expect(fileToRoute("(marketing)/pricing/page.wrn")).toBe("/pricing");
  expect(fileToRoute("(dashboard)/index.wrn")).toBe("/");
});

test("malformed encoded route params return no match instead of throwing", () => {
  expect(matchRoute([route("/users/[id]")], "/users/%E0%A4%A")).toBeNull();
});

test("application routes override package routes with the same path", async () => {
  const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = await import("node:fs");
  const { tmpdir } = await import("node:os");
  const { join } = await import("node:path");
  const root = mkdtempSync(join(tmpdir(), "wrnexus-router-override-"));

  try {
    const appDir = join(root, "app");
    const appRoute = join(appDir, "pages", "account.ts");
    const packageRoute = join(root, "package-account.ts");
    mkdirSync(join(appDir, "pages"), { recursive: true });
    writeFileSync(appRoute, "export default function account() {}\n");
    writeFileSync(packageRoute, "export default function packageAccount() {}\n");

    const router = buildRouter(appDir, {
      externalRoutes: [{ kind: "page", path: "/account", entry: packageRoute }],
    });

    expect(router.matchPage("/account")?.route.file).toBe(appRoute);
    expect(router.pages.filter((route) => route.raw === "/account")).toHaveLength(1);
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("components use declaration names with case-insensitive overrides", async () => {
  const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = await import("node:fs");
  const { tmpdir } = await import("node:os");
  const { join } = await import("node:path");
  const root = mkdtempSync(join(tmpdir(), "wrnexus-router-components-"));

  try {
    const appDir = join(root, "app");
    const componentsDir = join(root, "library-components");
    mkdirSync(join(appDir, "pages"), { recursive: true });
    mkdirSync(componentsDir, { recursive: true });
    writeFileSync(join(appDir, "pages", "index.wrn"), "page Home { view { <p>Home</p> } }\n");
    writeFileSync(
      join(componentsDir, "button.wrn"),
      'component Button { view { <button type="button">Button</button> } }\n',
    );

    const router = buildRouter(appDir, { componentDirs: [componentsDir] });

    expect(router.components).toHaveLength(1);
    expect(router.components[0]).toEqual({
      name: "Button",
      file: join(componentsDir, "button.wrn"),
    });
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});
