import { beforeEach, describe, expect, test } from "bun:test";
import type { PermissionStore } from "../src/store.ts";

/**
 * Every PermissionStore adapter must pass this suite, so the memory and db
 * implementations cannot drift apart.
 */
export function runStoreConformance(name: string, makeStore: () => Promise<PermissionStore>): void {
  describe(`PermissionStore conformance: ${name}`, () => {
    let store: PermissionStore;
    beforeEach(async () => {
      store = await makeStore();
    });

    test("an unknown subject has empty assignments", async () => {
      expect(await store.assignmentsFor("nobody")).toEqual({
        roles: [],
        grants: [],
        denies: [],
      });
    });

    test("assignRole then assignmentsFor round-trips", async () => {
      await store.assignRole("u1", "editor");
      expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
    });

    test("assignRole is idempotent", async () => {
      await store.assignRole("u1", "editor");
      await store.assignRole("u1", "editor");
      expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
    });

    test("revokeRole removes only that role", async () => {
      await store.assignRole("u1", "editor");
      await store.assignRole("u1", "admin");
      await store.revokeRole("u1", "editor");
      expect((await store.assignmentsFor("u1")).roles).toEqual(["admin"]);
    });

    test("revoking a role that was never assigned is a no-op", async () => {
      await store.revokeRole("u1", "ghost");
      expect((await store.assignmentsFor("u1")).roles).toEqual([]);
    });

    test("scoped assignments do not leak across tenants", async () => {
      await store.assignRole("u1", "editor", { tenantId: "t1" });
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]);
      expect((await store.assignmentsFor("u1", { tenantId: "t2" })).roles).toEqual([]);
    });

    test("a global assignment is visible inside every tenant", async () => {
      await store.assignRole("u1", "superadmin");
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["superadmin"]);
    });

    test("global and scoped roles union within a tenant", async () => {
      await store.assignRole("u1", "viewer");
      await store.assignRole("u1", "editor", { tenantId: "t1" });
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles.sort()).toEqual([
        "editor",
        "viewer",
      ]);
    });

    test("grant with allow and deny land in the right buckets", async () => {
      await store.grant("u1", "post:write", "allow");
      await store.grant("u1", "post:delete", "deny");
      const assignments = await store.assignmentsFor("u1");
      expect(assignments.grants).toEqual(["post:write"]);
      expect(assignments.denies).toEqual(["post:delete"]);
    });

    test("re-granting the same permission replaces its effect", async () => {
      await store.grant("u1", "post:write", "allow");
      await store.grant("u1", "post:write", "deny");
      const assignments = await store.assignmentsFor("u1");
      expect(assignments.grants).toEqual([]);
      expect(assignments.denies).toEqual(["post:write"]);
    });

    test("revokeGrant removes the permission entirely", async () => {
      await store.grant("u1", "post:write", "allow");
      await store.revokeGrant("u1", "post:write");
      expect((await store.assignmentsFor("u1")).grants).toEqual([]);
    });

    test("a tenant-scoped grant does not leak into another tenant", async () => {
      await store.grant("u1", "post:write", "allow", { tenantId: "t1" });
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).grants).toEqual(["post:write"]);
      expect((await store.assignmentsFor("u1", { tenantId: "t2" })).grants).toEqual([]);
    });

    test("a tenant-scoped deny does not leak into another tenant", async () => {
      await store.grant("u1", "post:delete", "deny", { tenantId: "t1" });
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).denies).toEqual([
        "post:delete",
      ]);
      expect((await store.assignmentsFor("u1", { tenantId: "t2" })).denies).toEqual([]);
    });

    test("a global grant is visible inside every tenant", async () => {
      await store.grant("u1", "post:publish", "allow");
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).grants).toEqual([
        "post:publish",
      ]);
    });

    test("revokeGrant is scope-isolated: revoking a tenant-scoped grant leaves the global grant intact", async () => {
      await store.grant("u1", "post:write", "allow");
      await store.grant("u1", "post:write", "allow", { tenantId: "t1" });
      await store.revokeGrant("u1", "post:write", { tenantId: "t1" });
      expect((await store.assignmentsFor("u1")).grants).toEqual(["post:write"]);
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).grants).toEqual(["post:write"]);
    });

    test("revokeRole is scope-isolated: revoking a tenant-scoped role leaves the global role intact", async () => {
      await store.assignRole("u1", "editor");
      await store.assignRole("u1", "editor", { tenantId: "t1" });
      await store.revokeRole("u1", "editor", { tenantId: "t1" });
      expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
      expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]);
    });

    test("listSubjects returns everyone with an assignment in scope", async () => {
      await store.assignRole("u1", "editor", { tenantId: "t1" });
      await store.assignRole("u2", "editor", { tenantId: "t1" });
      await store.assignRole("u3", "editor", { tenantId: "t2" });
      expect((await store.listSubjects({ tenantId: "t1" })).sort()).toEqual(["u1", "u2"]);
    });

    test("an explicitly empty tenantId is refused, not treated as global", async () => {
      await store.assignRole("g1", "viewer");
      // Otherwise a caller who controls the tenant id reaches global scope.
      await expect(store.assignmentsFor("g1", { tenantId: "" })).rejects.toThrow(/tenantId/);
      await expect(store.assignRole("g1", "admin", { tenantId: "" })).rejects.toThrow(/tenantId/);
    });

    test("a non-string tenantId is refused", async () => {
      // Same class as the empty-string case: the caller controls this value.
      for (const bad of [null, 0, false, {}]) {
        await expect(store.assignmentsFor("u1", { tenantId: bad as never })).rejects.toThrow(
          /tenantId/,
        );
      }
    });

    test("concurrent identical assignRole calls all resolve", async () => {
      // Check-then-act loses this race; the UNIQUE constraint then rejects
      // every loser even though the desired end state was already reached.
      await Promise.all(Array.from({ length: 20 }, () => store.assignRole("u1", "editor")));
      expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
    });

    test("concurrent grants on distinct keys all resolve", async () => {
      await Promise.all([
        store.grant("u1", "post:read", "allow"),
        store.grant("u1", "post:write", "allow"),
        store.grant("u1", "post:delete", "deny"),
      ]);
      const assignments = await store.assignmentsFor("u1");
      expect(assignments.grants.sort()).toEqual(["post:read", "post:write"]);
      expect(assignments.denies).toEqual(["post:delete"]);
    });

    test("a rejected write leaves unrelated state intact", async () => {
      await store.assignRole("victim", "admin");
      await store.grant("victim", "post:read", "allow");
      // An invalid effect must be refused without disturbing anything else.
      await expect(store.grant("victim", "post:write", "bogus" as never)).rejects.toThrow();
      const assignments = await store.assignmentsFor("victim");
      expect(assignments.roles).toEqual(["admin"]);
      expect(assignments.grants).toEqual(["post:read"]);
    });

    // NOTE: the shared-connection rollback hazard - where one method's open
    // transaction sweeps in a concurrent bare write from another method and
    // discards it, so a revoke resolves successfully while the role survives -
    // is prevented STRUCTURALLY, by the store using no transactions at all.
    // It is deliberately not covered here: reproducing it needs the bare write
    // to land inside the open transaction, which a single-process Promise.all
    // does not reliably arrange, so any such test would pass against the
    // defective implementation and give false assurance.

    test("listSubjects with no scope returns global assignees only", async () => {
      await store.assignRole("g1", "viewer");
      await store.assignRole("s1", "editor", { tenantId: "t1" });
      expect(await store.listSubjects()).toEqual(["g1"]);
    });

    test("listSubjects credits grant-only subjects", async () => {
      await store.grant("g1", "post:write", "allow", { tenantId: "t1" });
      expect((await store.listSubjects({ tenantId: "t1" })).sort()).toEqual(["g1"]);
    });
  });
}
