import { describe, expect, test } from "bun:test";
import {
  CacheCoordinator,
  connectCacheInvalidation,
  RequestCache,
  TagCache,
  responseCache,
} from "../src/index.ts";

describe("@wrnexus/cache", () => {
  test("separates request, data, component, and page cache lifetimes", async () => {
    const coordinator = new CacheCoordinator({ ttlMs: 100, eventLimit: 10 });
    coordinator.data.set("user:1", { id: 1 }, { tags: ["users"] });
    coordinator.component.set("avatar:1", "html", { tags: ["users"] });
    coordinator.page.set("/users", "document", { tags: ["users"] });
    expect(coordinator.inspect().layers.data[0]?.key).toBe("user:1");
    expect(coordinator.invalidateTags(["users"])).toBe(3);
    expect(
      coordinator.inspect().recentEvents.some((event) => event.operation === "invalidate"),
    ).toBe(true);

    const request = new RequestCache();
    let calls = 0;
    const [first, second] = await Promise.all([
      request.getOrLoad("permissions", async () => ++calls),
      request.getOrLoad("permissions", async () => ++calls),
    ]);
    expect([first, second, calls]).toEqual([1, 1, 1]);
  });
  test("supports fresh, stale, and tag invalidation", async () => {
    let now = 0;
    const cache = new TagCache<number>({ ttlMs: 10, staleWhileRevalidateMs: 10, clock: () => now });
    cache.set("a", 1, { tags: ["users"] });
    expect(cache.lookup("a").state).toBe("fresh");
    now = 11;
    expect(cache.lookup("a").state).toBe("stale");
    expect(cache.invalidateTag("users")).toBe(1);
    expect(cache.lookup("a").state).toBe("miss");
  });

  test("deduplicates concurrent loaders", async () => {
    const cache = new TagCache<number>();
    let calls = 0;
    const loader = async () => ++calls;
    const [a, b] = await Promise.all([cache.getOrLoad("x", loader), cache.getOrLoad("x", loader)]);
    expect(a).toBe(1);
    expect(b).toBe(1);
    expect(calls).toBe(1);
  });

  test("provides exclusive per-key locks", async () => {
    const cache = new TagCache();
    const order: string[] = [];
    let release!: () => void;
    const first = cache.withLock("catalog", async () => {
      order.push("first:start");
      await new Promise<void>((resolve) => (release = resolve));
      order.push("first:end");
    });
    await Promise.resolve();
    const second = cache.withLock("catalog", () => order.push("second"));
    await Promise.resolve();
    expect(order).toEqual(["first:start"]);
    release();
    await Promise.all([first, second]);
    expect(order).toEqual(["first:start", "first:end", "second"]);
  });

  test("invalidation during a load prevents stale work from repopulating the cache", async () => {
    const cache = new TagCache<number>();
    let release!: (value: number) => void;
    const loading = cache.getOrLoad(
      "x",
      () => new Promise<number>((resolve) => (release = resolve)),
    );
    await Promise.resolve();
    cache.clear();
    release(7);
    expect(await loading).toBe(7);
    expect(cache.lookup("x").state).toBe("miss");
  });

  test("propagates tag, key, and clear invalidations across instances", async () => {
    const subscriptions = new Map<string, Set<(message: unknown) => void | Promise<void>>>();
    const bus = {
      async publish(topic: string, message: unknown) {
        await Promise.all(
          [...(subscriptions.get(topic) ?? [])].map((handler) => Promise.resolve(handler(message))),
        );
      },
      subscribe(topic: string, handler: (message: unknown) => void | Promise<void>) {
        const handlers = subscriptions.get(topic) ?? new Set();
        handlers.add(handler);
        subscriptions.set(topic, handlers);
        return () => handlers.delete(handler);
      },
    };
    const first = new TagCache<number>();
    const second = new TagCache<number>();
    const firstChannel = connectCacheInvalidation(first, bus, {
      namespace: "catalog",
      instanceId: "one",
    });
    const secondChannel = connectCacheInvalidation(second, bus, {
      namespace: "catalog",
      instanceId: "two",
    });
    for (const cache of [first, second]) cache.set("product:1", 1, { tags: ["products"] });

    expect(await firstChannel.invalidateTag("products")).toBe(1);
    expect(second.lookup("product:1").state).toBe("miss");
    first.set("one", 1);
    second.set("one", 1);
    await secondChannel.delete("one");
    expect(first.lookup("one").state).toBe("miss");
    first.set("all", 1);
    second.set("all", 1);
    await firstChannel.clear();
    expect(second.size).toBe(0);

    firstChannel.close();
    firstChannel.close();
    await expect(firstChannel.invalidateTag("products")).rejects.toThrow(
      "WRN-CACHE-INVALIDATION-CLOSED",
    );
    secondChannel.close();
  });

  test("response middleware does not call next twice for stale entries", async () => {
    let now = 0;
    const cache = new TagCache<any>({ ttlMs: 1, staleWhileRevalidateMs: 100, clock: () => now });
    const middleware = responseCache({ cache, ttlMs: 1, staleWhileRevalidateMs: 100 });
    let calls = 0;
    const ctx = {
      req: new Request("https://example.com/a"),
      url: new URL("https://example.com/a"),
    } as any;
    await middleware(ctx, async () => {
      calls++;
      return new Response("one");
    });
    now = 2;
    const stale = await middleware(ctx, async () => {
      calls++;
      return new Response("two");
    });
    expect(await stale.text()).toBe("one");
    expect(calls).toBe(1);
  });
});
