import { describe, expect, test } from "bun:test";
import { cachedPermissionStore, memoryPermissionStore } from "../src/store.ts";
import { runStoreConformance } from "./store-conformance.ts";

// A cache must not change observable behaviour: writes invalidate internally.
runStoreConformance("cached(memory)", async () => cachedPermissionStore(memoryPermissionStore()));

describe("cachedPermissionStore", () => {
  test("serves a repeat read from cache", async () => {
    const inner = memoryPermissionStore();
    let reads = 0;
    const counting = {
      ...inner,
      assignmentsFor: (id: string, scope?: { tenantId?: string }) => {
        reads++;
        return inner.assignmentsFor(id, scope);
      },
    };
    const store = cachedPermissionStore(counting, { ttlMs: 60_000 });
    await store.assignmentsFor("u1");
    await store.assignmentsFor("u1");
    expect(reads).toBe(1);
  });

  test("a write invalidates that subject", async () => {
    const store = cachedPermissionStore(memoryPermissionStore(), { ttlMs: 60_000 });
    await store.assignmentsFor("u1");
    await store.assignRole("u1", "editor");
    expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
  });

  test("invalidate() drops a cached subject", async () => {
    const inner = memoryPermissionStore();
    const store = cachedPermissionStore(inner, { ttlMs: 60_000 });
    await store.assignmentsFor("u1");
    await inner.assignRole("u1", "editor"); // behind the cache's back
    expect((await store.assignmentsFor("u1")).roles).toEqual([]);
    store.invalidate("u1");
    expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
  });

  test("entries expire after ttlMs", async () => {
    const inner = memoryPermissionStore();
    const store = cachedPermissionStore(inner, { ttlMs: 1 });
    await store.assignmentsFor("u1");
    await inner.assignRole("u1", "editor");
    await Bun.sleep(5);
    expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
  });

  test("cache is bounded by max", async () => {
    const store = cachedPermissionStore(memoryPermissionStore(), { ttlMs: 60_000, max: 2 });
    await store.assignmentsFor("a");
    await store.assignmentsFor("b");
    await store.assignmentsFor("c");
    expect(store.size()).toBeLessThanOrEqual(2);
  });

  test("scoped and global reads cache separately", async () => {
    const inner = memoryPermissionStore();
    const store = cachedPermissionStore(inner, { ttlMs: 60_000 });
    await inner.assignRole("u1", "editor", { tenantId: "t1" });
    expect((await store.assignmentsFor("u1")).roles).toEqual([]);
    expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]);
  });

  test("a global write invalidates the subject in every tenant", async () => {
    const inner = memoryPermissionStore();
    const store = cachedPermissionStore(inner, { ttlMs: 60_000 });
    await store.assignmentsFor("u1", { tenantId: "t1" }); // warm the tenant entry
    await store.assignRole("u1", "editor"); // global write
    expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]);
  });

  test("cache keys cannot collide across subject/tenant boundaries", async () => {
    const inner = memoryPermissionStore();
    const store = cachedPermissionStore(inner, { ttlMs: 60_000 });
    await inner.assignRole("b\uFFFDc", "editor", { tenantId: "a" });
    expect((await store.assignmentsFor("b\uFFFDc", { tenantId: "a" })).roles).toEqual(["editor"]);
    expect((await store.assignmentsFor("c", { tenantId: "a\uFFFDb" })).roles).toEqual([]);
  });
});
