import { describe, expect, test } from "bun:test";
import { defineAuthz } from "../src/registry.ts";
import { emptyCatalog, mergeCatalogs } from "../src/catalog.ts";

describe("mergeCatalogs", () => {
  test("merges disjoint modules", () => {
    const catalog = mergeCatalogs([
      { source: "a.ts", module: defineAuthz({ permissions: { "post:read": {} } }) },
      { source: "b.ts", module: defineAuthz({ permissions: { "user:read": {} } }) },
    ]);
    expect([...catalog.permissions.keys()].sort()).toEqual(["post:read", "user:read"]);
  });

  test("re-declaring a permission with deep-equal metadata is a no-op", () => {
    const meta = { title: "View posts", risk: "low" as const };
    const catalog = mergeCatalogs([
      { source: "a.ts", module: defineAuthz({ permissions: { "post:read": meta } }) },
      { source: "b.ts", module: defineAuthz({ permissions: { "post:read": { ...meta } } }) },
    ]);
    expect(catalog.permissions.size).toBe(1);
  });

  test("conflicting metadata is a boot error naming both files", () => {
    expect(() =>
      mergeCatalogs([
        { source: "a.ts", module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }) },
        { source: "b.ts", module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }) },
      ]),
    ).toThrow(/a\.ts.*b\.ts|b\.ts.*a\.ts/s);
  });

  test("conflicting role definitions are a boot error", () => {
    expect(() =>
      mergeCatalogs([
        { source: "a.ts", module: defineAuthz({ roles: { editor: ["post:read"] } }) },
        { source: "b.ts", module: defineAuthz({ roles: { editor: ["post:write"] } }) },
      ]),
    ).toThrow(/editor/);
  });

  test("bindings for the same permission union across modules", () => {
    const p1 = defineAuthz({
      permissions: { "post:write": {} },
      policies: { ownsPost: async () => ({ allowed: true }) },
      bindings: { "post:write": ["ownsPost"] },
    });
    const p2 = defineAuthz({
      policies: { notLocked: async () => ({ allowed: true }) },
      bindings: { "post:write": ["notLocked"] },
    });
    const catalog = mergeCatalogs([
      { source: "a.ts", module: p1 },
      { source: "b.ts", module: p2 },
    ]);
    expect([...catalog.bindings.get("post:write")!].sort()).toEqual(["notLocked", "ownsPost"]);
  });

  test("a binding referencing a policy no module declares is a boot error", () => {
    expect(() =>
      mergeCatalogs([
        {
          source: "a.ts",
          module: { permissions: { "post:write": {} }, bindings: { "post:write": ["ghost"] } },
        },
      ]),
    ).toThrow(/ghost/);
  });

  test("the merged catalog is frozen", () => {
    const catalog = mergeCatalogs([]);
    expect(() => (catalog.permissions as Map<string, never>).set("x:y", {} as never)).toThrow();
  });

  test("a role's granted-entries array cannot be mutated to escalate it after boot", () => {
    // frozenMap only blocks the Map's own mutators (set/delete/clear) — the
    // VALUES it holds are a separate concern. Without freezing them too,
    // catalog.roles.get("editor").push("*") would succeed and silently
    // escalate "editor" to a full wildcard past an error string that claims
    // the catalog is frozen after boot.
    const catalog = mergeCatalogs([
      { source: "a.ts", module: defineAuthz({ roles: { editor: ["post:write"] } }) },
    ]);
    const editorRole = catalog.roles.get("editor")!;
    expect(() => (editorRole as string[]).push("*")).toThrow();
    expect(catalog.roles.get("editor")).toEqual(["post:write"]);
  });

  test("a permission's metadata object cannot be mutated after boot", () => {
    const catalog = mergeCatalogs([
      {
        source: "a.ts",
        module: defineAuthz({ permissions: { "post:delete": { risk: "low" } } }),
      },
    ]);
    const meta = catalog.permissions.get("post:delete")!;
    expect(() => {
      (meta as { risk?: string }).risk = "high";
    }).toThrow();
    expect(catalog.permissions.get("post:delete")!.risk).toBe("low");
  });

  test("an attribute's metadata object cannot be mutated after boot", () => {
    const catalog = mergeCatalogs([
      {
        source: "a.ts",
        module: defineAuthz({ attributes: { department: { description: "org unit" } } }),
      },
    ]);
    const meta = catalog.attributes.get("department")!;
    expect(() => {
      (meta as { description?: string }).description = "tampered";
    }).toThrow();
    expect(catalog.attributes.get("department")!.description).toBe("org unit");
  });

  test("a binding's policy-name array cannot be mutated after boot", () => {
    const catalog = mergeCatalogs([
      {
        source: "a.ts",
        module: defineAuthz({
          permissions: { "post:write": {} },
          policies: { ownsPost: async () => ({ allowed: true }) },
          bindings: { "post:write": ["ownsPost"] },
        }),
      },
    ]);
    const names = catalog.bindings.get("post:write")!;
    expect(() => (names as string[]).push("injectedPolicy")).toThrow();
    expect(catalog.bindings.get("post:write")).toEqual(["ownsPost"]);
  });

  test("emptyCatalog has no entries", () => {
    expect(emptyCatalog().permissions.size).toBe(0);
  });
});
