import { describe, expect, test } from "bun:test";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
import { defineAuthz, getAuthzCatalog, mergeCatalogs, setAuthzCatalog } from "@wrnexus/authz";
import {
  applyAuthzManifestEarly,
  createProductionHandlers,
  type ProdManifest,
} from "../src/prod.ts";

const EMPTY_MANIFEST: ProdManifest = {
  pages: [],
  api: [],
  realtime: [],
  middleware: [],
  components: [],
  layouts: [],
};

const PROD_URL = pathToFileURL(join(import.meta.dir, "..", "src", "prod.ts")).href;

describe("createProductionHandlers authorization wiring (the idempotent second pass)", () => {
  test("an empty/absent authz array never throws, whatever the ambient catalog state", () => {
    createProductionHandlers(EMPTY_MANIFEST, { authz: [] });
    createProductionHandlers(EMPTY_MANIFEST, {});
  });

  test("starting from a genuinely unset catalog, an empty/absent authz array yields an empty catalog", async () => {
    // bun test does NOT isolate module instances between test files run in
    // the same invocation (see client.test.ts's comment on the same trap),
    // so "no catalog set yet" cannot be observed reliably in-process — some
    // other file's test may already have called setAuthzCatalog. A fresh
    // subprocess is the only way to guarantee that.
    const proc = Bun.spawn({
      cmd: [
        "bun",
        "-e",
        `const mod = await import(${JSON.stringify(PROD_URL)});
         const manifest = { pages: [], api: [], realtime: [], middleware: [], components: [], layouts: [] };
         mod.createProductionHandlers(manifest, { authz: [] });
         const { getAuthzCatalog } = await import("@wrnexus/authz");
         console.log("SIZE:" + getAuthzCatalog().permissions.size);`,
      ],
      stdout: "pipe",
      stderr: "pipe",
      cwd: join(import.meta.dir, ".."),
    });
    const [stdout, stderr, exitCode] = await Promise.all([
      new Response(proc.stdout).text(),
      new Response(proc.stderr).text(),
      proc.exited,
    ]);
    expect(stderr).toBe("");
    expect(exitCode).toBe(0);
    expect(stdout).toContain("SIZE:0");
  });

  test("a declaration with no default export warns and is skipped, not fatal", () => {
    const originalWarn = console.warn;
    const warnings: unknown[][] = [];
    console.warn = (...args: unknown[]) => {
      warnings.push(args);
    };
    try {
      createProductionHandlers(EMPTY_MANIFEST, {
        authz: [
          { source: "broken.ts", module: undefined },
          {
            source: "ok.ts",
            module: defineAuthz({ permissions: { "post:read": {} } }),
          },
        ],
      });
    } finally {
      console.warn = originalWarn;
    }
    expect(getAuthzCatalog().permissions.has("post:read")).toBe(true);
    expect(getAuthzCatalog().permissions.size).toBe(1);
    expect(warnings.some((args) => args.some((arg) => String(arg).includes("broken.ts")))).toBe(
      true,
    );
  });

  test("a conflicting pair of declarations throws, naming both source files", () => {
    expect(() =>
      createProductionHandlers(EMPTY_MANIFEST, {
        authz: [
          {
            source: "a.ts",
            module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }),
          },
          {
            source: "b.ts",
            module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }),
          },
        ],
      }),
    ).toThrow(/WRN-AUTHZ-CONFLICT/);

    let thrown: unknown;
    try {
      createProductionHandlers(EMPTY_MANIFEST, {
        authz: [
          {
            source: "a.ts",
            module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }),
          },
          {
            source: "b.ts",
            module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }),
          },
        ],
      });
    } catch (error) {
      thrown = error;
    }
    expect(thrown).toBeInstanceOf(Error);
    const message = (thrown as Error).message;
    expect(message).toContain("a.ts");
    expect(message).toContain("b.ts");
  });

  test("calling createProductionHandlers a second time with different declarations re-validates, not skips", () => {
    // Regression guard for the "skip merging if a catalog is already set"
    // trap: since setAuthzCatalog is a process-wide singleton, an earlier
    // test (or an earlier createProductionHandlers call in the same process)
    // can leave hasAuthzCatalog() true. This call must still independently
    // merge+validate its OWN opts.authz, not silently trust a stale catalog
    // left over from something else.
    createProductionHandlers(EMPTY_MANIFEST, {
      authz: [{ source: "first.ts", module: defineAuthz({ permissions: { "a:read": {} } }) }],
    });
    expect(getAuthzCatalog().permissions.has("a:read")).toBe(true);

    createProductionHandlers(EMPTY_MANIFEST, {
      authz: [{ source: "second.ts", module: defineAuthz({ permissions: { "b:read": {} } }) }],
    });
    expect(getAuthzCatalog().permissions.has("a:read")).toBe(false);
    expect(getAuthzCatalog().permissions.has("b:read")).toBe(true);
  });

  test("a caller-set catalog survives when opts.authz is omitted (the client.ts escape hatch)", () => {
    // client.ts documents that a direct caller of createProductionHandlers may
    // call setAuthzCatalog(catalog) itself, before importing anything that
    // reads it, when it bypasses the generated `.authz-setup.ts` entry. That
    // catalog must not be wiped just because this call's own opts.authz is
    // empty/absent.
    const preset = mergeCatalogs([
      {
        source: "preset.ts",
        module: defineAuthz({
          permissions: { "preset:read": {}, "preset:write": {}, "preset:delete": {} },
        }),
      },
    ]);
    setAuthzCatalog(preset);
    expect(getAuthzCatalog().permissions.size).toBe(3);

    createProductionHandlers(EMPTY_MANIFEST, {});

    expect(getAuthzCatalog()).toBe(preset);
    expect(getAuthzCatalog().permissions.size).toBe(3);
    expect(getAuthzCatalog().permissions.has("preset:read")).toBe(true);
  });

  test("a non-empty opts.authz still sets (and still throws on a conflict), even over a pre-set catalog", () => {
    const preset = mergeCatalogs([
      { source: "preset.ts", module: defineAuthz({ permissions: { "preset:read": {} } }) },
    ]);
    setAuthzCatalog(preset);

    // A non-empty authz array must still replace the pre-set catalog with the
    // merged result of ITS OWN declarations, not defer to the pre-set one.
    createProductionHandlers(EMPTY_MANIFEST, {
      authz: [{ source: "own.ts", module: defineAuthz({ permissions: { "own:read": {} } }) }],
    });
    expect(getAuthzCatalog()).not.toBe(preset);
    expect(getAuthzCatalog().permissions.has("own:read")).toBe(true);
    expect(getAuthzCatalog().permissions.has("preset:read")).toBe(false);

    // And a genuine conflict inside that non-empty array still throws, exactly
    // as it did before this pass became conditional.
    setAuthzCatalog(preset);
    expect(() =>
      createProductionHandlers(EMPTY_MANIFEST, {
        authz: [
          {
            source: "a.ts",
            module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }),
          },
          {
            source: "b.ts",
            module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }),
          },
        ],
      }),
    ).toThrow(/WRN-AUTHZ-CONFLICT/);
  });
});

describe("applyAuthzManifestEarly (the eager, silent pass called only by the generated .authz-setup.ts)", () => {
  test("sets the catalog from valid declarations", () => {
    applyAuthzManifestEarly([
      { source: "early.ts", module: defineAuthz({ permissions: { "early:read": {} } }) },
    ]);
    expect(getAuthzCatalog().permissions.has("early:read")).toBe(true);
  });

  test("silently skips a missing default export — no warning, no throw", () => {
    const originalWarn = console.warn;
    let warnCalls = 0;
    console.warn = () => {
      warnCalls++;
    };
    try {
      expect(() =>
        applyAuthzManifestEarly([{ source: "broken.ts", module: undefined }]),
      ).not.toThrow();
    } finally {
      console.warn = originalWarn;
    }
    expect(warnCalls).toBe(0);
    expect(getAuthzCatalog().permissions.size).toBe(0);
  });

  test("still throws on a genuine conflict (fatal either way, just earlier)", () => {
    expect(() =>
      applyAuthzManifestEarly([
        { source: "a.ts", module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }) },
        { source: "b.ts", module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }) },
      ]),
    ).toThrow(/WRN-AUTHZ-CONFLICT/);
  });
});
