import { describe, it, expect, vi } from "vitest";
import { AgencyCancelledError, RestoreSignal } from "./errors.js";
import { callHook, registerGlobalHook } from "./hooks.js";
import { State, StateStack } from "./state/stateStack.js";
function ctxWithStack(stack, tsCallbacks = {}, topLevelCallbacks = []) {
    const stateStack = new StateStack();
    stateStack.stack = stack;
    return { stateStack, callbacks: tsCallbacks, topLevelCallbacks };
}
function fakeCtx() {
    return {
        topLevelCallbacks: [],
        callbacks: {},
        stateStack: { collectScopedCallbacks: () => [] },
    };
}
describe("callHook", () => {
    it("fires scoped callbacks innermost → outermost", async () => {
        const calls = [];
        const outer = new State();
        outer.scopedCallbacks = [
            { name: "onNodeStart", fn: () => { calls.push("outer"); } },
        ];
        const inner = new State();
        inner.scopedCallbacks = [
            { name: "onNodeStart", fn: () => { calls.push("inner"); } },
        ];
        const ctx = ctxWithStack([outer, inner]);
        await callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } });
        expect(calls).toEqual(["inner", "outer"]);
    });
    it("fires TS-passed callback last", async () => {
        const calls = [];
        const inner = new State();
        inner.scopedCallbacks = [
            { name: "onNodeStart", fn: () => { calls.push("scoped"); } },
        ];
        const ctx = ctxWithStack([inner], { onNodeStart: () => { calls.push("ts"); } });
        await callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } });
        expect(calls).toEqual(["scoped", "ts"]);
    });
    it("ignores callback return values", async () => {
        const inner = new State();
        inner.scopedCallbacks = [
            { name: "onLLMCallEnd", fn: () => ["whatever"] },
        ];
        const ctx = ctxWithStack([inner]);
        const result = await callHook({
            ctx,
            name: "onLLMCallEnd",
            data: {},
        });
        expect(result).toBeUndefined();
    });
    it("catches and logs ordinary errors, continues firing others", async () => {
        const consoleErr = vi.spyOn(console, "error").mockImplementation(() => { });
        const calls = [];
        const inner = new State();
        inner.scopedCallbacks = [
            { name: "onNodeStart", fn: () => { throw new Error("boom"); } },
            { name: "onNodeStart", fn: () => { calls.push("after"); } },
        ];
        const ctx = ctxWithStack([inner]);
        await callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } });
        expect(calls).toEqual(["after"]);
        expect(consoleErr).toHaveBeenCalled();
        consoleErr.mockRestore();
    });
    it("rethrows RestoreSignal control-flow errors", async () => {
        const signal = new RestoreSignal({ id: 1 });
        const inner = new State();
        inner.scopedCallbacks = [
            { name: "onNodeStart", fn: () => { throw signal; } },
        ];
        const ctx = ctxWithStack([inner]);
        await expect(callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } })).rejects.toBe(signal);
    });
    it("rethrows AgencyCancelledError", async () => {
        const cancelled = new AgencyCancelledError("user cancelled");
        const inner = new State();
        inner.scopedCallbacks = [
            { name: "onNodeStart", fn: () => { throw cancelled; } },
        ];
        const ctx = ctxWithStack([inner]);
        await expect(callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } })).rejects.toBe(cancelled);
    });
    it("fires scoped → top-level → TS-passed (in that order)", async () => {
        const calls = [];
        const inner = new State();
        inner.scopedCallbacks = [
            { name: "onNodeStart", fn: () => { calls.push("scoped"); } },
        ];
        const ctx = ctxWithStack([inner], { onNodeStart: () => { calls.push("ts"); } }, [{ name: "onNodeStart", fn: () => { calls.push("topLevel"); } }]);
        await callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } });
        expect(calls).toEqual(["scoped", "topLevel", "ts"]);
    });
    it("filters topLevelCallbacks by name", async () => {
        const calls = [];
        const ctx = ctxWithStack([new State()], {}, [
            { name: "onNodeStart", fn: () => { calls.push("matching"); } },
            { name: "onNodeEnd", fn: () => { calls.push("other"); } },
        ]);
        await callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } });
        expect(calls).toEqual(["matching"]);
    });
    it("multiple distinct callbacks for the same event all fire in order", async () => {
        const calls = [];
        const frame = new State();
        frame.addScopedCallback("onNodeStart", () => { calls.push("a"); });
        frame.addScopedCallback("onNodeStart", () => { calls.push("b"); });
        frame.addScopedCallback("onNodeStart", () => { calls.push("c"); });
        const ctx = ctxWithStack([frame]);
        await callHook({ ctx, name: "onNodeStart", data: { nodeName: "x" } });
        expect(calls).toEqual(["a", "b", "c"]);
    });
    it("per-instance recursion guard skips re-entry of the same fn", async () => {
        let depth = 0;
        let maxDepth = 0;
        const ctxHolder = { ctx: undefined };
        const fn = async (data) => {
            depth++;
            maxDepth = Math.max(maxDepth, depth);
            if (depth < 5) {
                await callHook({ ctx: ctxHolder.ctx, name: "onNodeStart", data });
            }
            depth--;
        };
        const inner = new State();
        inner.scopedCallbacks = [{ name: "onNodeStart", fn }];
        ctxHolder.ctx = ctxWithStack([inner]);
        await callHook({ ctx: ctxHolder.ctx, name: "onNodeStart", data: { nodeName: "x" } });
        expect(maxDepth).toBe(1);
    });
});
describe("registerGlobalHook", () => {
    it("fires registered global hooks", async () => {
        const calls = [];
        registerGlobalHook("onEmit", (_data) => { calls.push("global"); });
        const ctx = fakeCtx();
        await callHook({ ctx, name: "onEmit", data: "hello" });
        expect(calls).toContain("global");
    });
});
