import { describe, it, expect } from "vitest";
import { Runner, stripSlug } from "./runner.js";
import { State, StateStack } from "./state/stateStack.js";
import { ThreadStore } from "./state/threadStore.js";
import { getRuntimeContext } from "./asyncContext.js";
import { makeMockCtx } from "./__tests__/testHelpers.js";
function makeFrame() {
    return new State({ args: {}, locals: {}, step: 0 });
}
describe("Runner", () => {
    describe("step()", () => {
        it("executes callback and advances counter", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            let executed = false;
            await runner.step(0, async () => {
                executed = true;
            });
            expect(executed).toBe(true);
            expect(frame.step).toBe(1);
        });
        it("skips when counter is past the step", async () => {
            const frame = makeFrame();
            frame.step = 5;
            const runner = new Runner(makeMockCtx(), frame);
            let executed = false;
            await runner.step(0, async () => {
                executed = true;
            });
            expect(executed).toBe(false);
        });
        it("executes multiple steps in sequence", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            const order = [];
            await runner.step(0, async () => {
                order.push(0);
            });
            await runner.step(1, async () => {
                order.push(1);
            });
            await runner.step(2, async () => {
                order.push(2);
            });
            expect(order).toEqual([0, 1, 2]);
            expect(frame.step).toBe(3);
        });
        it("resumes from saved step counter", async () => {
            const frame = makeFrame();
            frame.step = 2;
            const runner = new Runner(makeMockCtx(), frame);
            const order = [];
            await runner.step(0, async () => {
                order.push(0);
            });
            await runner.step(1, async () => {
                order.push(1);
            });
            await runner.step(2, async () => {
                order.push(2);
            });
            expect(order).toEqual([2]);
            expect(frame.step).toBe(3);
        });
    });
    describe("halt propagation", () => {
        it("halts runner and skips subsequent steps", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            const order = [];
            await runner.step(0, async (runner) => {
                order.push(0);
                runner.halt("interrupt-data");
            });
            await runner.step(1, async () => {
                order.push(1);
            });
            expect(order).toEqual([0]);
            expect(runner.halted).toBe(true);
            expect(runner.haltResult).toBe("interrupt-data");
            expect(frame.step).toBe(0); // not advanced — step didn't complete
        });
        it("propagates halt from nested step", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            await runner.step(0, async (runner) => {
                await runner.step(0, async (runner) => {
                    runner.halt("deep-halt");
                });
                await runner.step(1, async () => {
                    throw new Error("should not run");
                });
            });
            expect(runner.halted).toBe(true);
            expect(runner.haltResult).toBe("deep-halt");
        });
        it("does not advance counter when callback halts", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            await runner.step(0, async (runner) => {
                runner.halt("stopped");
            });
            expect(frame.step).toBe(0); // NOT advanced
        });
    });
    describe("nested steps (substeps)", () => {
        it("tracks substep counters in frame.locals", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            await runner.step(0, async (runner) => {
                await runner.step(0, async () => { });
                await runner.step(1, async () => { });
            });
            expect(frame.step).toBe(1);
            expect(frame.locals.__substep_0).toBe(2); // 2 substeps completed
        });
        it("resumes nested substeps correctly", async () => {
            const frame = makeFrame();
            // Simulate: step 0 completed, inside step 1, substep 0 completed
            frame.step = 0;
            frame.locals.__substep_1 = 1; // substep 0 of step 1 done
            const runner = new Runner(makeMockCtx(), frame);
            const order = [];
            await runner.step(0, async (runner) => {
                // step 0 not skipped (counter is 0)
                await runner.step(0, async () => {
                    order.push("0.0");
                });
                await runner.step(1, async () => {
                    order.push("0.1");
                });
            });
            // step 0 runs because frame.step is 0
            expect(order).toEqual(["0.0", "0.1"]);
        });
        it("deeply nested steps produce correct keys", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            await runner.step(2, async (runner) => {
                await runner.step(1, async (runner) => {
                    await runner.step(3, async () => { });
                });
            });
            expect(frame.locals.__substep_2).toBe(2); // substep 1 completed
            expect(frame.locals["__substep_2.1"]).toBe(4); // substep 3 completed
        });
    });
    describe("ifElse()", () => {
        it("executes matching branch", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            let result = "";
            await runner.ifElse(0, [
                {
                    condition: () => false,
                    body: async () => {
                        result = "a";
                    },
                },
                {
                    condition: () => true,
                    body: async () => {
                        result = "b";
                    },
                },
            ]);
            expect(result).toBe("b");
            expect(frame.step).toBe(1);
        });
        it("executes else branch when no conditions match", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            let result = "";
            await runner.ifElse(0, [
                {
                    condition: () => false,
                    body: async () => {
                        result = "if";
                    },
                },
            ], async () => {
                result = "else";
            });
            expect(result).toBe("else");
        });
        it("does not re-evaluate conditions on resume", async () => {
            const frame = makeFrame();
            frame.locals.__condbranch_0 = 1; // branch 1 was selected
            frame.locals.__substep_0 = 0; // resume from start of branch body
            const runner = new Runner(makeMockCtx(), frame);
            let evalCount = 0;
            let result = "";
            await runner.ifElse(0, [
                {
                    condition: () => {
                        evalCount++;
                        return true;
                    },
                    body: async () => {
                        result = "a";
                    },
                },
                {
                    condition: () => {
                        evalCount++;
                        return true;
                    },
                    body: async () => {
                        result = "b";
                    },
                },
            ]);
            expect(evalCount).toBe(0); // conditions NOT re-evaluated
            expect(result).toBe("b"); // branch 1 executed
        });
        it("stores condbranch in nested context", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            await runner.step(3, async (runner) => {
                await runner.ifElse(0, [
                    { condition: () => true, body: async () => { } },
                ]);
            });
            // condbranch key should include the parent path
            expect(frame.locals["__condbranch_3.0"]).toBe(0);
        });
    });
    describe("loop()", () => {
        it("iterates over items", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            const collected = [];
            await runner.loop(0, ["a", "b", "c"], async (item) => {
                collected.push(item);
            });
            expect(collected).toEqual(["a", "b", "c"]);
            expect(frame.step).toBe(1);
        });
        it("resumes at saved iteration", async () => {
            const frame = makeFrame();
            frame.locals.__iteration_0 = 2; // already completed iterations 0, 1
            const runner = new Runner(makeMockCtx(), frame);
            const collected = [];
            await runner.loop(0, ["a", "b", "c"], async (item) => {
                collected.push(item);
            });
            expect(collected).toEqual(["c"]);
        });
        it("halts mid-iteration and preserves state", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            await runner.loop(0, ["a", "b", "c"], async (item, i, runner) => {
                if (item === "b") {
                    runner.halt("stopped");
                    return;
                }
            });
            expect(runner.halted).toBe(true);
            expect(frame.locals.__iteration_0).toBe(1); // completed iteration 0
        });
        it("resets substep tracking between iterations", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            await runner.loop(0, ["a", "b"], async (item, i, runner) => {
                await runner.step(0, async () => { });
            });
            // After completion, the iteration counter should reflect 2 iterations
            expect(frame.locals.__iteration_0).toBe(2);
        });
        it("iterates the keys of a plain object (Record)", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            const collected = [];
            await runner.loop(0, { alice: "approve", bob: "reject" }, async (key) => {
                collected.push(key);
            });
            expect(collected.sort()).toEqual(["alice", "bob"]);
        });
        it("does nothing when iterating an empty Record", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            const collected = [];
            await runner.loop(0, {}, async (key) => {
                collected.push(key);
            });
            expect(collected).toEqual([]);
            // The iteration counter is initialized to 0 and the step counter
            // still advances past this loop.
            expect(frame.locals.__iteration_0).toBe(0);
            expect(frame.step).toBe(1);
        });
        it("halts mid-iteration over a Record and preserves state", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            const collected = [];
            await runner.loop(0, { a: 1, b: 2, c: 3 }, async (key, _i, runner) => {
                collected.push(key);
                if (key === "b") {
                    runner.halt("stopped");
                }
            });
            expect(runner.halted).toBe(true);
            // Object.keys order is insertion order for string keys -> a, b, c
            expect(collected).toEqual(["a", "b"]);
            // Completed iteration 0 ("a") only. The halt happens inside iteration
            // 1, so the counter still points at iteration 1 for resumption.
            expect(frame.locals.__iteration_0).toBe(1);
        });
        it("does nothing when given null/undefined as the iterable", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            const collected = [];
            await runner.loop(0, null, async (item) => {
                collected.push(item);
            });
            expect(collected).toEqual([]);
            expect(frame.step).toBe(1);
        });
    });
    describe("whileLoop()", () => {
        it("loops while condition is true", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            let count = 0;
            await runner.whileLoop(0, () => count < 3, async () => {
                count++;
            });
            expect(count).toBe(3);
            expect(frame.step).toBe(1);
        });
        it("resumes at saved iteration", async () => {
            const frame = makeFrame();
            frame.locals.__iteration_0 = 2;
            const runner = new Runner(makeMockCtx(), frame);
            let count = 0;
            // condition must account for iterations already done
            await runner.whileLoop(0, () => count + 2 < 3, async () => {
                count++;
            });
            expect(count).toBe(1); // only 1 more iteration (iteration 2)
        });
        // The condition arrow generated by the TS builder is `async () => ${cond}`
        // (because the builder always emits `await` around function calls inside
        // expressions). The runtime must `await` the condition so a
        // Promise<boolean> is correctly consumed.
        it("awaits an async condition that returns Promise<boolean>", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            let count = 0;
            await runner.whileLoop(0, async () => {
                // Force the condition to genuinely return a Promise.
                await Promise.resolve();
                return count < 3;
            }, async () => {
                count++;
            });
            expect(count).toBe(3);
        });
    });
    describe("thread()", () => {
        it("calls setup and cleanup", async () => {
            const frame = makeFrame();
            const ctx = makeMockCtx();
            const calls = [];
            ctx.threads.create = () => {
                calls.push("create");
                return "tid";
            };
            ctx.threads.pushActive = () => {
                calls.push("push");
            };
            ctx.threads.popActive = () => {
                calls.push("pop");
            };
            const runner = new Runner(ctx, frame, { threads: ctx.threads });
            await runner.thread(0, "create", {}, async () => {
                calls.push("body");
            });
            expect(calls).toEqual(["create", "push", "body", "pop"]);
        });
        it("pops thread even on halt", async () => {
            const frame = makeFrame();
            const ctx = makeMockCtx();
            let popped = false;
            ctx.threads.popActive = () => {
                popped = true;
            };
            const runner = new Runner(ctx, frame, { threads: ctx.threads });
            await runner.thread(0, "create", {}, async (runner) => {
                runner.halt("interrupt");
            });
            expect(popped).toBe(true);
            expect(runner.halted).toBe(true);
        });
        it("fires onThreadStart and onThreadEnd with slug ids and label", async () => {
            const frame = makeFrame();
            const ctx = makeMockCtx();
            const events = [];
            ctx.callbacks = {
                onThreadStart: (data) => { events.push({ kind: "start", data }); },
                onThreadEnd: (data) => { events.push({ kind: "end", data }); },
            };
            // makeMockCtx returns "tid-1" from create(). Override get() to
            // surface a non-empty message list so we can assert the snapshot.
            ctx.threads.get = () => ({
                messages: [{ toJSON: () => ({ role: "user", content: "hi" }) }],
                parentId: null,
            });
            const runner = new Runner(ctx, frame, { threads: ctx.threads });
            await runner.thread(0, "create", { label: "coding task", summarize: true }, async () => {
                /* body */
            });
            expect(events.length).toBe(2);
            expect(events[0].kind).toBe("start");
            expect(events[0].data.threadId).toBe("ttid-1");
            expect(events[0].data.label).toBe("coding task");
            expect(events[0].data.threadType).toBe("thread");
            expect(events[0].data.isResumption).toBe(false);
            expect(events[1].kind).toBe("end");
            expect(events[1].data.threadId).toBe("ttid-1");
            expect(events[1].data.label).toBe("coding task");
            expect(events[1].data.eagerSummarize).toBe(true);
            expect(events[1].data.messages).toEqual([{ role: "user", content: "hi" }]);
        });
    });
    describe("handle()", () => {
        it("pushes and pops handler", async () => {
            const frame = makeFrame();
            const ctx = makeMockCtx();
            const runner = new Runner(ctx, frame);
            const handler = async () => ({ type: "approve" });
            expect(ctx.handlers.length).toBe(0);
            await runner.handle(0, handler, async () => {
                expect(ctx.handlers.length).toBe(1);
            });
            expect(ctx.handlers.length).toBe(0);
        });
        it("pops handler even on halt", async () => {
            const frame = makeFrame();
            const ctx = makeMockCtx();
            const runner = new Runner(ctx, frame);
            const handler = async () => ({ type: "approve" });
            await runner.handle(0, handler, async (runner) => {
                runner.halt("interrupt");
            });
            expect(ctx.handlers.length).toBe(0);
            expect(runner.halted).toBe(true);
        });
    });
    describe("branchStep()", () => {
        it("executes when counter not past", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            let executed = false;
            await runner.branchStep(0, "0_1", async () => {
                executed = true;
            });
            expect(executed).toBe(true);
        });
        it("executes when counter past but branch data exists", async () => {
            const frame = makeFrame();
            frame.step = 5;
            frame.newBranch("0_1"); // simulate that branch data was created in a previous run
            const runner = new Runner(makeMockCtx(), frame);
            let executed = false;
            await runner.branchStep(0, "0_1", async () => {
                executed = true;
            });
            expect(executed).toBe(true);
        });
        it("skips when counter past and no branch data", async () => {
            const frame = makeFrame();
            frame.step = 5;
            const runner = new Runner(makeMockCtx(), frame);
            let executed = false;
            await runner.branchStep(0, "0_1", async () => {
                executed = true;
            });
            expect(executed).toBe(false);
        });
    });
    describe("nested composition", () => {
        it("step inside ifElse inside loop inside handle", async () => {
            const frame = makeFrame();
            const ctx = makeMockCtx();
            const runner = new Runner(ctx, frame);
            const handler = async () => ({ type: "approve" });
            const trace = [];
            await runner.handle(0, handler, async (runner) => {
                await runner.loop(0, ["a", "b"], async (item, i, runner) => {
                    await runner.ifElse(0, [
                        {
                            condition: () => item === "a",
                            body: async (runner) => {
                                await runner.step(0, async () => {
                                    trace.push(`${item}-if`);
                                });
                            },
                        },
                    ], async (runner) => {
                        await runner.step(0, async () => {
                            trace.push(`${item}-else`);
                        });
                    });
                });
            });
            expect(trace).toEqual(["a-if", "b-else"]);
            expect(ctx.handlers.length).toBe(0);
        });
    });
    describe("variable naming", () => {
        it("produces expected variable names for nested structures", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame);
            // Step 0: simple
            await runner.step(0, async () => { });
            // Step 1: ifElse
            await runner.ifElse(1, [
                {
                    condition: () => true,
                    body: async (runner) => {
                        await runner.step(0, async () => { });
                        await runner.step(1, async () => { });
                    },
                },
            ]);
            // Step 2: loop
            await runner.loop(2, ["a", "b"], async (item, i, runner) => {
                await runner.step(0, async () => { });
            });
            expect(frame.step).toBe(3);
            expect(frame.locals.__condbranch_1).toBe(0); // ifElse at step 1, branch 0
            expect(frame.locals.__substep_1).toBe(2); // 2 substeps in ifElse
            expect(frame.locals.__iteration_2).toBe(2); // 2 loop iterations
        });
    });
    describe("runInScope seeds the ALS callsite slot", () => {
        it("populates moduleId / scopeName / stepPath for a single step", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame, {
                moduleId: "modX",
                scopeName: "fooScope",
                stack: new StateStack(),
                threads: new ThreadStore(),
            });
            let seen = null;
            await runner.step(1, async () => {
                seen = getRuntimeContext().callsite;
            });
            expect(seen).toEqual({
                moduleId: "modX",
                scopeName: "fooScope",
                stepPath: "1",
            });
        });
        it("updates stepPath for nested step IDs", async () => {
            const frame = makeFrame();
            const runner = new Runner(makeMockCtx(), frame, {
                moduleId: "modX",
                scopeName: "fooScope",
                stack: new StateStack(),
                threads: new ThreadStore(),
            });
            const paths = [];
            await runner.step(0, async () => {
                paths.push(getRuntimeContext().callsite.stepPath);
                await runner.ifElse(0, [
                    {
                        condition: () => true,
                        body: async (r) => {
                            await r.step(0, async () => {
                                paths.push(getRuntimeContext().callsite.stepPath);
                            });
                        },
                    },
                ]);
            });
            // Top-level step 0 (path = ["0"]) and nested step 0.0.0 inside
            // the ifElse branch (path = ["0", "0", "0", "0"]).
            expect(paths[0]).toBe("0");
            expect(paths[paths.length - 1]).toContain("0.0");
        });
    });
});
describe("stripSlug", () => {
    it("strips the leading t from canonical slugs", () => {
        expect(stripSlug("t1")).toBe("1");
        expect(stripSlug("t42")).toBe("42");
    });
    it("leaves non-slug strings untouched", () => {
        expect(stripSlug("hello")).toBe("hello");
        // Regression: prior to the tightened regex this returned "hello".
        expect(stripSlug("thello")).toBe("thello");
        expect(stripSlug("t")).toBe("t");
        expect(stripSlug("t1a")).toBe("t1a");
        expect(stripSlug("")).toBe("");
    });
});
