import { describe, it, expect, vi } from "vitest";
// Mock dependencies before importing the module under test
vi.mock("../../server/context", () => ({
    track: () => () => { },
}));
vi.mock("../../server/request-context.js", () => ({
    getRequestContext: () => undefined,
}));
vi.mock("../../server/fetchable-loader-store.js", () => ({
    getFetchableLoader: () => undefined,
}));
vi.mock("../../handle.js", () => ({
    isHandle: () => false,
}));
import { setupLoaderAccess, setupLoaderAccessSilent, } from "../loader-resolution";
/**
 * Create a minimal mock HandlerContext for testing loader resolution.
 */
function createMockContext() {
    const url = new URL("http://localhost/test");
    return {
        params: {},
        request: new Request(url.href),
        searchParams: new URLSearchParams(),
        pathname: "/test",
        url,
        env: {},
        var: {},
        get: () => undefined,
        set: () => { },
        header: () => { },
        status: () => { },
        html: () => new Response(""),
        json: () => new Response(""),
        text: () => new Response(""),
        redirect: () => new Response(""),
        notFound: () => {
            throw new Error("not implemented");
        },
        use: vi.fn(),
    };
}
/**
 * Create a mock LoaderDefinition with a given ID and function.
 */
function createLoader(id, fn) {
    return {
        __brand: "loader",
        $$id: id,
        fn,
    };
}
describe("loader cycle detection", () => {
    describe("setupLoaderAccess", () => {
        it("should detect direct circular dependency (A -> B -> A)", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            // loaderB depends on loaderA, loaderA depends on loaderB
            const loaderA = createLoader("loaderA", async (loaderCtx) => {
                const b = await loaderCtx.use(loaderB);
                return { a: true, b };
            });
            const loaderB = createLoader("loaderB", async (loaderCtx) => {
                const a = await loaderCtx.use(loaderA);
                return { b: true, a };
            });
            setupLoaderAccess(ctx, loaderPromises);
            // Start both loaders - the cycle should be detected and cause rejection
            const resultA = ctx.use(loaderA);
            const resultB = ctx.use(loaderB);
            // At least one of the promises should reject with a cycle error
            const results = await Promise.allSettled([resultA, resultB]);
            const rejections = results.filter((r) => r.status === "rejected");
            expect(rejections.length).toBeGreaterThanOrEqual(1);
            const error = rejections[0].reason;
            expect(error.message).toContain("Circular loader dependency detected");
            expect(error.message).toContain("loaderA");
            expect(error.message).toContain("loaderB");
        });
        it("should detect three-way circular dependency (A -> B -> C -> A)", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            const loaderA = createLoader("loaderA", async (loaderCtx) => {
                return await loaderCtx.use(loaderB);
            });
            const loaderB = createLoader("loaderB", async (loaderCtx) => {
                return await loaderCtx.use(loaderC);
            });
            const loaderC = createLoader("loaderC", async (loaderCtx) => {
                return await loaderCtx.use(loaderA);
            });
            setupLoaderAccess(ctx, loaderPromises);
            const resultA = ctx.use(loaderA);
            const results = await Promise.allSettled([resultA]);
            const rejection = results[0];
            expect(rejection.status).toBe("rejected");
            expect(rejection.reason.message).toContain("Circular loader dependency detected");
            // The cycle chain should include all three loaders
            expect(rejection.reason.message).toContain("loaderA");
            expect(rejection.reason.message).toContain("loaderB");
            expect(rejection.reason.message).toContain("loaderC");
        });
        it("should allow diamond dependencies without false positives", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            // Diamond: A -> B, A -> C, B -> D, C -> D
            // D is shared but no cycle exists
            const loaderD = createLoader("loaderD", async () => {
                return "D-data";
            });
            const loaderB = createLoader("loaderB", async (loaderCtx) => {
                const d = await loaderCtx.use(loaderD);
                return { b: true, d };
            });
            const loaderC = createLoader("loaderC", async (loaderCtx) => {
                const d = await loaderCtx.use(loaderD);
                return { c: true, d };
            });
            const loaderA = createLoader("loaderA", async (loaderCtx) => {
                const [b, c] = await Promise.all([
                    loaderCtx.use(loaderB),
                    loaderCtx.use(loaderC),
                ]);
                return { a: true, b, c };
            });
            setupLoaderAccess(ctx, loaderPromises);
            const result = await ctx.use(loaderA);
            expect(result).toEqual({
                a: true,
                b: { b: true, d: "D-data" },
                c: { c: true, d: "D-data" },
            });
        });
        it("should allow linear dependencies (A -> B -> C)", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            const loaderC = createLoader("loaderC", async () => {
                return "C-data";
            });
            const loaderB = createLoader("loaderB", async (loaderCtx) => {
                const c = await loaderCtx.use(loaderC);
                return { b: true, c };
            });
            const loaderA = createLoader("loaderA", async (loaderCtx) => {
                const b = await loaderCtx.use(loaderB);
                return { a: true, b };
            });
            setupLoaderAccess(ctx, loaderPromises);
            const result = await ctx.use(loaderA);
            expect(result).toEqual({
                a: true,
                b: { b: true, c: "C-data" },
            });
        });
        it("should still memoize loaders (same loader called twice returns same promise)", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            let callCount = 0;
            const loader = createLoader("memoized", async () => {
                callCount++;
                return "data";
            });
            setupLoaderAccess(ctx, loaderPromises);
            const p1 = ctx.use(loader);
            const p2 = ctx.use(loader);
            // Same promise reference
            expect(p1).toBe(p2);
            const result = await p1;
            expect(result).toBe("data");
            expect(callCount).toBe(1);
        });
        it("should include the full cycle path in the error message", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            const loaderA = createLoader("auth", async (loaderCtx) => {
                return await loaderCtx.use(loaderB);
            });
            const loaderB = createLoader("user", async (loaderCtx) => {
                return await loaderCtx.use(loaderA);
            });
            setupLoaderAccess(ctx, loaderPromises);
            const result = await Promise.allSettled([ctx.use(loaderA)]);
            const rejection = result[0];
            expect(rejection.status).toBe("rejected");
            // Should show the cycle chain: auth -> user -> auth
            expect(rejection.reason.message).toMatch(/Circular loader dependency detected: .+ -> .+ -> .+/);
        });
    });
    describe("setupLoaderAccessSilent", () => {
        it("should detect circular dependency in silent mode", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            const loaderA = createLoader("silentA", async (loaderCtx) => {
                return await loaderCtx.use(loaderB);
            });
            const loaderB = createLoader("silentB", async (loaderCtx) => {
                return await loaderCtx.use(loaderA);
            });
            setupLoaderAccessSilent(ctx, loaderPromises);
            const results = await Promise.allSettled([ctx.use(loaderA)]);
            const rejection = results[0];
            expect(rejection.status).toBe("rejected");
            expect(rejection.reason.message).toContain("Circular loader dependency detected");
            expect(rejection.reason.message).toContain("silentA");
            expect(rejection.reason.message).toContain("silentB");
        });
        it("should allow valid dependencies in silent mode", async () => {
            const ctx = createMockContext();
            const loaderPromises = new Map();
            const loaderB = createLoader("silentB", async () => "B-data");
            const loaderA = createLoader("silentA", async (loaderCtx) => {
                const b = await loaderCtx.use(loaderB);
                return { a: true, b };
            });
            setupLoaderAccessSilent(ctx, loaderPromises);
            const result = await ctx.use(loaderA);
            expect(result).toEqual({ a: true, b: "B-data" });
        });
    });
});
//# sourceMappingURL=loader-cycle-detection.test.js.map