import { describe, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createPluginRunner, discoverPlugins, resolvePlugins } from "../src/index.ts";
import { parse } from "@wrnexus/syntax";

describe("plugin ordering", () => {
  test("orders pre, normal, and post plugins", () => {
    expect(
      resolvePlugins([
        { name: "post", enforce: "post" },
        { name: "normal" },
        { name: "pre", enforce: "pre" },
      ]).map((plugin) => plugin.name),
    ).toEqual(["pre", "normal", "post"]);
  });

  test("respects explicit dependencies", () => {
    expect(
      resolvePlugins([{ name: "b", after: ["a"] }, { name: "a" }]).map((plugin) => plugin.name),
    ).toEqual(["a", "b"]);
  });
});

test("runs the complete lifecycle and exposes ecosystem contribution channels", async () => {
  const calls: string[] = [];
  const runner = createPluginRunner(
    {
      name: "ecosystem",
      setup: () => {
        calls.push("setup");
      },
      configure: () => {
        calls.push("configure");
      },
      configSchemas: [
        {
          namespace: "feature",
          validate(value) {
            calls.push(`schema:${String(value)}`);
          },
        },
      ],
      directives: [
        {
          name: "focus",
          transform: (value) => ({ name: "data-focus", value }),
        },
      ],
      cliCommands: [
        {
          name: "hello",
          run: () => {
            calls.push("cli");
          },
        },
      ],
      virtualModules: [{ id: "virtual:feature", load: () => "export default true" }],
      deploymentAdapters: [{ name: "test-cloud", build: (value) => value }],
      documentation: ["docs/feature.md"],
      typeDefinitions: ["types/feature.d.ts"],
      render: (html) => `${html}<!-- plugin -->`,
      hmrUpdate: (files) => {
        calls.push(`hmr:${files.join(",")}`);
      },
      deploy: () => {
        calls.push("deploy");
      },
      shutdown: () => {
        calls.push("shutdown");
      },
    },
    context("."),
  );
  await runner.configure({ feature: true });
  await runner.configResolved({ feature: true });
  const contributions = await runner.contributions();
  expect(contributions.directives[0]?.name).toBe("focus");
  expect(contributions.cliCommands[0]?.name).toBe("hello");
  expect(contributions.virtualModules[0]?.id).toBe("virtual:feature");
  expect(contributions.deploymentAdapters[0]?.name).toBe("test-cloud");
  expect(contributions.documentation).toEqual(["docs/feature.md"]);
  expect(contributions.typeDefinitions).toEqual(["types/feature.d.ts"]);
  const transformed = await runner.transformAst(
    parse('page Demo { view { <input use:focus="first"> } }'),
    "app/pages/demo.wrn",
  );
  const input = transformed.view.find((node) => node.type === "element");
  expect(input?.type === "element" ? input.attrs[0]?.name : undefined).toBe("data-focus");
  expect(await runner.render("<main></main>")).toContain("<!-- plugin -->");
  await runner.hook("hmrUpdate", ["app/page.wrn"]);
  await runner.hook("deploy", {});
  await runner.hook("shutdown");
  expect(calls).toEqual([
    "setup",
    "configure",
    "schema:true",
    "hmr:app/page.wrn",
    "deploy",
    "shutdown",
  ]);
});

function context(root: string) {
  return {
    root,
    mode: "development" as const,
    command: "dev" as const,
    metadata: new Map<string, unknown>(),
    warn() {},
  };
}

test("normalizes package runtimes, assets, routes, middleware, and migrations", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-"));
  try {
    const runner = createPluginRunner(
      {
        name: "system",
        clientRuntimes: [{ id: "system", source: "window.system = true" }],
        assets: [{ id: "system-data", source: "{}", contentType: "application/json" }],
        routeEntries: [{ kind: "api", path: "/api/system", entry: join(root, "route.ts") }],
        middleware: [join(root, "middleware.ts")],
        migrations: [{ id: "system-schema", source: "-- +up\nSELECT 1;" }],
      },
      context(root),
    );
    const contributions = await runner.contributions();
    expect(contributions.clientRuntimes[0]?.publicPath).toBe("/__wrnexus/assets/system.js");
    expect(contributions.assets[0]?.publicPath).toBe("/__wrnexus/assets/system-data.json");
    expect(contributions.routes[0]?.path).toBe("/api/system");
    expect(contributions.middleware).toEqual([join(root, "middleware.ts")]);
    expect(contributions.migrations[0]?.id).toBe("system-schema");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("auto-discovers package.json contributions from workspace dependencies", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-discovery-"));
  const app = join(root, "apps", "web");
  const pkg = join(root, "packages", "example-system");
  try {
    mkdirSync(join(app, "app", "pages"), { recursive: true });
    mkdirSync(join(pkg, "components"), { recursive: true });
    mkdirSync(join(pkg, "assets"), { recursive: true });
    writeFileSync(
      join(root, "package.json"),
      JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }),
    );
    writeFileSync(
      join(app, "package.json"),
      JSON.stringify({ name: "web", dependencies: { "@wrnexus/example-system": "workspace:*" } }),
    );
    writeFileSync(
      join(pkg, "components", "Example.wrn"),
      "component Example { view { <div>Example</div> } }",
    );
    writeFileSync(join(pkg, "assets", "runtime.js"), "window.example = true;");
    writeFileSync(
      join(pkg, "package.json"),
      JSON.stringify({
        name: "@wrnexus/example-system",
        version: "0.4.0",
        wrnexus: {
          components: ["./components"],
          clientRuntimes: [{ id: "example-system", entry: "./assets/runtime.js" }],
          migrations: [{ id: "example-schema", source: "-- +up\nSELECT 1;" }],
        },
      }),
    );

    const discovered = await discoverPlugins(app, undefined);
    const runner = createPluginRunner(discovered, context(app));
    const contributions = await runner.contributions();
    expect(runner.plugins.map((plugin) => plugin.name)).toContain(
      "@wrnexus/example-system/manifest",
    );
    expect(contributions.componentDirs).toEqual([join(pkg, "components")]);
    expect(contributions.clientRuntimes[0]?.entry).toBe(join(pkg, "assets", "runtime.js"));
    expect(contributions.migrations[0]?.id).toBe("example-schema");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("rejects duplicate runtime paths and migration ids", async () => {
  const runner = createPluginRunner(
    [
      {
        name: "one",
        clientRuntimes: [{ id: "one", source: "", publicPath: "/same.js" }],
        migrations: [{ id: "schema", source: "SELECT 1" }],
      },
      {
        name: "two",
        clientRuntimes: [{ id: "two", source: "", publicPath: "/same.js" }],
        migrations: [{ id: "schema", source: "SELECT 2" }],
      },
    ],
    context("."),
  );
  await expect(runner.contributions()).rejects.toThrow("WRN-PLUGIN-RUNTIME-PATH-DUPLICATE");
});

test("rejects client-runtime and package-asset path collisions", async () => {
  const runner = createPluginRunner(
    [
      {
        name: "runtime-owner",
        clientRuntimes: [{ id: "runtime", source: "", publicPath: "/shared.js" }],
      },
      {
        name: "asset-owner",
        assets: [
          {
            id: "asset",
            source: "window.asset = true;",
            contentType: "text/javascript",
            publicPath: "/shared.js",
          },
        ],
      },
    ],
    context("."),
  );

  await expect(runner.contributions()).rejects.toThrow("WRN-PLUGIN-PUBLIC-PATH-DUPLICATE");
});

test("strict discovery surfaces invalid package plugin exports", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-strict-discovery-"));
  const app = join(root, "apps", "web");
  const pkg = join(root, "packages", "broken-system");
  try {
    mkdirSync(join(app, "app", "pages"), { recursive: true });
    mkdirSync(pkg, { recursive: true });
    writeFileSync(
      join(root, "package.json"),
      JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }),
    );
    writeFileSync(
      join(app, "package.json"),
      JSON.stringify({
        name: "web",
        dependencies: { "@wrnexus/broken-system": "workspace:*" },
      }),
    );
    writeFileSync(join(pkg, "plugin.mjs"), "export const unrelated = true;\n");
    writeFileSync(
      join(pkg, "package.json"),
      JSON.stringify({
        name: "@wrnexus/broken-system",
        version: "0.4.0",
        wrnexus: { plugin: "./plugin.mjs" },
      }),
    );

    await expect(discoverPlugins(app, undefined, { strict: true })).rejects.toThrow(
      "WRN-PLUGIN-DISCOVERY",
    );
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("discovery enforces declared deployment runtimes and capabilities", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-runtime-discovery-"));
  const app = join(root, "apps", "web");
  const pkg = join(root, "packages", "filesystem-plugin");
  try {
    mkdirSync(app, { recursive: true });
    mkdirSync(pkg, { recursive: true });
    writeFileSync(
      join(root, "package.json"),
      JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }),
    );
    writeFileSync(
      join(app, "package.json"),
      JSON.stringify({ name: "web", dependencies: { "filesystem-plugin": "workspace:*" } }),
    );
    writeFileSync(
      join(pkg, "package.json"),
      JSON.stringify({
        name: "filesystem-plugin",
        version: "1.0.0",
        wrnexus: { runtimes: ["bun", "node"], requires: ["filesystem"] },
      }),
    );
    await expect(
      discoverPlugins(app, undefined, { runtime: "edge", capabilities: ["crypto"] }),
    ).rejects.toThrow("WRN-PLUGIN-RUNTIME");
    await expect(
      discoverPlugins(app, undefined, { runtime: "bun", capabilities: ["filesystem"] }),
    ).resolves.toBeDefined();
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("discovery enforces declared and application-granted plugin permissions", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-permission-discovery-"));
  const app = join(root, "apps", "web");
  const pkg = join(root, "packages", "route-plugin");
  try {
    mkdirSync(app, { recursive: true });
    mkdirSync(pkg, { recursive: true });
    writeFileSync(
      join(root, "package.json"),
      JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }),
    );
    writeFileSync(
      join(app, "package.json"),
      JSON.stringify({ name: "web", dependencies: { "route-plugin": "workspace:*" } }),
    );
    writeFileSync(join(pkg, "route.ts"), "export default {};");
    writeFileSync(
      join(pkg, "package.json"),
      JSON.stringify({
        name: "route-plugin",
        version: "1.0.0",
        wrnexus: {
          permissions: ["routes"],
          routes: [{ kind: "api", path: "/api/plugin", entry: "./route.ts" }],
        },
      }),
    );
    await expect(
      discoverPlugins(app, undefined, { enforcePermissions: true, grantedPermissions: {} }),
    ).rejects.toThrow("WRN-PLUGIN-PERMISSION-DENIED");
    await expect(
      discoverPlugins(app, undefined, {
        enforcePermissions: true,
        grantedPermissions: { "route-plugin": ["routes"] },
      }),
    ).resolves.toBeDefined();

    writeFileSync(
      join(pkg, "package.json"),
      JSON.stringify({
        name: "route-plugin",
        version: "1.0.0",
        wrnexus: { routes: [{ kind: "api", path: "/api/plugin", entry: "./route.ts" }] },
      }),
    );
    await expect(
      discoverPlugins(app, undefined, {
        enforcePermissions: true,
        grantedPermissions: { "route-plugin": ["routes"] },
      }),
    ).rejects.toThrow("WRN-PLUGIN-PERMISSION-UNDECLARED");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("orders package style contributions by pre, normal, and post", async () => {
  const runner = createPluginRunner(
    {
      name: "styles",
      styleSources: [
        { id: "post", source: "post", order: "post" },
        { id: "normal", source: "normal" },
        { id: "pre", source: "pre", order: "pre" },
        { id: "normal-two", source: "normal-two", order: "normal" },
      ],
    },
    context("."),
  );

  expect((await runner.contributions()).styles.map((style) => style.id)).toEqual([
    "pre",
    "normal",
    "normal-two",
    "post",
  ]);
});
