// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
 * DSL -> IR -> Engine integration tests.
 *
 * Compiles workflow DSL source to IR, then executes it through the engine,
 * verifying end-to-end runtime behavior.
 */

import {
    WorkflowIR,
    TaskDefinition,
    TaskContext,
    Template,
} from "workflow-model";
import {
    TaskRegistry,
    WorkflowEngine,
    WorkflowEvent,
    allBuiltinTasks,
} from "../src/index.js";
import {
    compile,
    compileFile,
    FileResolver,
    TaskSchemaInfo,
    CompileOptions,
} from "workflow-dsl";
import { isGenericTask } from "workflow-model";

// ---- Helpers ----

const VALIDATE: CompileOptions = { validate: true };

function makeRegistry(...tasks: TaskDefinition[]): TaskRegistry {
    const registry = new TaskRegistry();
    for (const t of tasks) {
        registry.register(t);
    }
    return registry;
}

function collectEvents(engine: WorkflowEngine): WorkflowEvent[] {
    const events: WorkflowEvent[] = [];
    engine.on((e) => events.push(e));
    return events;
}

/**
 * Build TaskSchemaInfo[] from TaskDefinition[], so the DSL compiler
 * can look up input/output schemas for each task.
 */
function taskSchemasFrom(tasks: TaskDefinition[]): TaskSchemaInfo[] {
    return tasks.map((t) => {
        if (isGenericTask(t)) {
            return {
                name: t.name,
                inputSchema: t.inputSchemaTemplate,
                outputSchema: t.outputSchemaTemplate,
                typeParameters: t.typeParameters,
            };
        }
        return {
            name: t.name,
            inputSchema: t.inputSchema,
            outputSchema: t.outputSchema,
        };
    });
}

/**
 * Compile DSL source to IR, asserting no errors.
 */
function compileOk(
    source: string,
    extraTasks: TaskDefinition[] = [],
    options: CompileOptions = VALIDATE,
): WorkflowIR {
    const schemas = taskSchemasFrom([...allBuiltinTasks, ...extraTasks]);
    const result = compile(source, schemas, options);
    if (result.errors.length > 0) {
        throw new Error(
            `Compile errors:\n${result.errors.map((e) => e.message).join("\n")}`,
        );
    }
    return result.ir!;
}

// Mock task definitions for testing

const webFetch: TaskDefinition = {
    name: "web.fetch",
    sideEffects: false,
    inputSchema: {
        type: "object",
        required: ["url"],
        properties: { url: { type: "string" } },
    },
    outputSchema: {
        type: "object",
        required: ["body"],
        properties: { body: { type: "string" } },
    },
    async execute(input: any) {
        return { kind: "ok", output: { body: `content of ${input.url}` } };
    },
};

function makeEngine(extraTasks: TaskDefinition[] = []): {
    eng: WorkflowEngine;
    events: WorkflowEvent[];
} {
    const reg = makeRegistry(...allBuiltinTasks, ...extraTasks);
    const eng = new WorkflowEngine(reg);
    const events = collectEvents(eng);
    return { eng, events };
}

/**
 * BranchArm scopes are nested. Walk every node in the IR (including arm
 * sub-scopes and loop bodies) and return the first node ID matching the
 * predicate.
 */
function findNodeIdDeep(
    ir: WorkflowIR,
    predicate: (id: string) => boolean,
): string | undefined {
    const visit = (nodes: Record<string, any>): string | undefined => {
        for (const [id, node] of Object.entries(nodes)) {
            if (predicate(id)) return id;
            if (node?.kind === "branch") {
                for (const arm of Object.values(node.cases ?? {})) {
                    const found = visit((arm as any).scope?.nodes ?? {});
                    if (found) return found;
                }
                if (node.default?.scope?.nodes) {
                    const found = visit(node.default.scope.nodes);
                    if (found) return found;
                }
            } else if (node?.kind === "loop" && node.body?.nodes) {
                const found = visit(node.body.nodes);
                if (found) return found;
            }
        }
        return undefined;
    };
    return visit(ir.workflows[ir.entry].nodes);
}

// ---- Tests ----

describe("DSL -> Engine integration", () => {
    // ---- Literals and constants ----

    describe("literals and constants", () => {
        it("returns a string literal", async () => {
            const ir = compileOk(`
                workflow hello(): string { return "hello world"; }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });

            expect(result.success).toBe(true);
            expect(result.output).toBe("hello world");
        });

        it("returns a number literal", async () => {
            const ir = compileOk(`
                workflow answer(): number { return 42; }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });

            expect(result.success).toBe(true);
            expect(result.output).toBe(42);
        });

        it("returns a boolean literal", async () => {
            const ir = compileOk(`
                workflow flag(): boolean { return true; }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });

            expect(result.success).toBe(true);
            expect(result.output).toBe(true);
        });

        it("returns an input parameter", async () => {
            const ir = compileOk(`
                workflow echo(msg: string): string { return msg; }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { msg: "ping" } });

            expect(result.success).toBe(true);
            expect(result.output).toBe("ping");
        });

        it("returns an object literal with mixed refs", async () => {
            const ir = compileOk(`
                workflow pack(name: string, age: integer): unknown {
                    return { name: name, age: age, active: true };
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { name: "Alice", age: 30 },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual({
                name: "Alice",
                age: 30,
                active: true,
            });
        });
    });

    // ---- Task calls ----

    describe("task calls", () => {
        it("calls a task and returns its output", async () => {
            const { eng } = makeEngine([webFetch]);
            const ir = compileOk(
                `
                workflow fetch(url: string): unknown {
                    const result = web.fetch(url);
                    return result.body;
                }
            `,
                [webFetch],
            );
            const result = await eng.run(ir, {
                input: { url: "https://example.com" },
            });

            expect(result.success).toBe(true);
            expect(result.output).toBe("content of https://example.com");
        });

        it("chains two tasks sequentially", async () => {
            const { eng } = makeEngine([webFetch]);
            const ir = compileOk(
                `
                workflow fetchAndFormat(url: string): unknown {
                    const page = web.fetch(url);
                    const msg = text.template("Got: {{body}}", { body: page.body });
                    return msg;
                }
            `,
                [webFetch],
            );
            const result = await eng.run(ir, {
                input: { url: "https://example.com" },
            });

            expect(result.success).toBe(true);
            expect(result.output).toBe("Got: content of https://example.com");
        });

        it("accesses a dotted property path", async () => {
            const multiPropTask: TaskDefinition = {
                name: "data.get",
                sideEffects: false,
                inputSchema: {
                    type: "object",
                    required: ["key"],
                    properties: { key: { type: "string" } },
                },
                outputSchema: {
                    type: "object",
                    required: ["name", "value"],
                    properties: {
                        name: { type: "string" },
                        value: { type: "number" },
                    },
                },
                async execute(input: any) {
                    return {
                        kind: "ok",
                        output: { name: input.key, value: 99 },
                    };
                },
            };

            const { eng } = makeEngine([multiPropTask]);
            const ir = compileOk(
                `
                workflow getVal(key: string): unknown {
                    const data = data.get(key);
                    return data.value;
                }
            `,
                [multiPropTask],
            );
            const result = await eng.run(ir, { input: { key: "test" } });

            expect(result.success).toBe(true);
            expect(result.output).toBe(99);
        });
    });

    // ---- Template literals ----

    describe("template literals", () => {
        it("interpolates variables into a template", async () => {
            const ir = compileOk(`
                workflow greet(name: string): unknown {
                    const msg = text.template("Hello {{name}}!", { name: name });
                    return msg;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { name: "World" } });

            expect(result.success).toBe(true);
            expect(result.output).toBe("Hello World!");
        });
    });

    // ---- Binary operators ----

    describe("binary operators", () => {
        it("adds two numbers", async () => {
            const ir = compileOk(`
                workflow add(a: number, b: number): unknown {
                    const sum = a + b;
                    return sum;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { a: 3, b: 4 } });

            expect(result.success).toBe(true);
            expect(result.output).toBe(7);
        });

        it("subtracts two numbers", async () => {
            const ir = compileOk(`
                workflow sub(a: number, b: number): unknown {
                    const diff = a - b;
                    return diff;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { a: 10, b: 3 } });

            expect(result.success).toBe(true);
            expect(result.output).toBe(7);
        });

        it("multiplies two numbers", async () => {
            const ir = compileOk(`
                workflow mul(a: number, b: number): unknown {
                    const prod = a * b;
                    return prod;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { a: 6, b: 7 } });

            expect(result.success).toBe(true);
            expect(result.output).toBe(42);
        });

        it("divides two numbers", async () => {
            const ir = compileOk(`
                workflow div(a: number, b: number): unknown {
                    const quot = a / b;
                    return quot;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { a: 20, b: 4 } });

            expect(result.success).toBe(true);
            expect(result.output).toBe(5);
        });

        it("computes modulo", async () => {
            const ir = compileOk(`
                workflow mod(a: number, b: number): unknown {
                    const rem = a % b;
                    return rem;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { a: 17, b: 5 } });

            expect(result.success).toBe(true);
            expect(result.output).toBe(2);
        });

        it("compares with ===", async () => {
            const ir = compileOk(`
                workflow eq(a: number, b: number): unknown {
                    const same = a === b;
                    return same;
                }
            `);
            const { eng } = makeEngine();

            let result = await eng.run(ir, { input: { a: 5, b: 5 } });
            expect(result.output).toBe(true);

            result = await eng.run(ir, { input: { a: 5, b: 6 } });
            expect(result.output).toBe(false);
        });

        it("compares with !==", async () => {
            const ir = compileOk(`
                workflow neq(a: number, b: number): unknown {
                    const diff = a !== b;
                    return diff;
                }
            `);
            const { eng } = makeEngine();

            let result = await eng.run(ir, { input: { a: 1, b: 2 } });
            expect(result.output).toBe(true);

            result = await eng.run(ir, { input: { a: 1, b: 1 } });
            expect(result.output).toBe(false);
        });

        it("compares with > and <", async () => {
            const ir = compileOk(`
                workflow cmp(a: number, b: number): unknown {
                    const gt = a > b;
                    return gt;
                }
            `);
            const { eng } = makeEngine();

            let result = await eng.run(ir, { input: { a: 5, b: 3 } });
            expect(result.output).toBe(true);

            result = await eng.run(ir, { input: { a: 3, b: 5 } });
            expect(result.output).toBe(false);
        });

        it("computes boolean && and ||", async () => {
            const irAnd = compileOk(`
                workflow both(a: boolean, b: boolean): unknown {
                    const r = a && b;
                    return r;
                }
            `);
            const irOr = compileOk(`
                workflow either(a: boolean, b: boolean): unknown {
                    const r = a || b;
                    return r;
                }
            `);
            const { eng } = makeEngine();

            let result = await eng.run(irAnd, {
                input: { a: true, b: false },
            });
            expect(result.output).toBe(false);

            result = await eng.run(irAnd, { input: { a: true, b: true } });
            expect(result.output).toBe(true);

            result = await eng.run(irOr, { input: { a: false, b: false } });
            expect(result.output).toBe(false);

            result = await eng.run(irOr, { input: { a: false, b: true } });
            expect(result.output).toBe(true);
        });

        it("short-circuits && (rhs not executed when lhs is false)", async () => {
            const ir = compileOk(`
                workflow sc(a: boolean, b: boolean): unknown {
                    const r = a && b;
                    return r;
                }
            `);
            // Find the RHS-evaluation and short-circuit arm node IDs
            const rhsNode = findNodeIdDeep(ir, (id) =>
                id.startsWith("and_rhs"),
            )!;
            const shortNode = findNodeIdDeep(ir, (id) =>
                id.startsWith("and_short"),
            )!;
            expect(rhsNode).toBeDefined();
            expect(shortNode).toBeDefined();

            const { eng, events } = makeEngine();

            // false && _ => short-circuit arm taken, rhs skipped
            events.length = 0;
            await eng.run(ir, { input: { a: false, b: true } });
            const executedIds = events
                .filter((e) => e.type === "nodeStarted")
                .map((e) => (e as any).nodeId);
            expect(executedIds).toContain(shortNode);
            expect(executedIds).not.toContain(rhsNode);

            // true && _ => rhs arm taken, short-circuit skipped
            events.length = 0;
            await eng.run(ir, { input: { a: true, b: false } });
            const executedIds2 = events
                .filter((e) => e.type === "nodeStarted")
                .map((e) => (e as any).nodeId);
            expect(executedIds2).toContain(rhsNode);
            expect(executedIds2).not.toContain(shortNode);
        });

        it("short-circuits || (rhs not executed when lhs is true)", async () => {
            const ir = compileOk(`
                workflow sc(a: boolean, b: boolean): unknown {
                    const r = a || b;
                    return r;
                }
            `);
            const rhsNode = findNodeIdDeep(ir, (id) =>
                id.startsWith("or_rhs"),
            )!;
            const shortNode = findNodeIdDeep(ir, (id) =>
                id.startsWith("or_short"),
            )!;
            expect(rhsNode).toBeDefined();
            expect(shortNode).toBeDefined();

            const { eng, events } = makeEngine();

            // true || _ => short-circuit arm taken, rhs skipped
            events.length = 0;
            await eng.run(ir, { input: { a: true, b: false } });
            const executedIds = events
                .filter((e) => e.type === "nodeStarted")
                .map((e) => (e as any).nodeId);
            expect(executedIds).toContain(shortNode);
            expect(executedIds).not.toContain(rhsNode);

            // false || _ => rhs arm taken, short-circuit skipped
            events.length = 0;
            await eng.run(ir, { input: { a: false, b: true } });
            const executedIds2 = events
                .filter((e) => e.type === "nodeStarted")
                .map((e) => (e as any).nodeId);
            expect(executedIds2).toContain(rhsNode);
            expect(executedIds2).not.toContain(shortNode);
        });

        it("respects operator precedence (a + b * c)", async () => {
            const ir = compileOk(`
                workflow calc(a: number, b: number, c: number): unknown {
                    const r = a + b * c;
                    return r;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { a: 2, b: 3, c: 4 },
            });

            // 2 + (3 * 4) = 14
            expect(result.success).toBe(true);
            expect(result.output).toBe(14);
        });
    });

    // ---- Unary operators ----

    describe("unary operators", () => {
        it("negates a boolean with !", async () => {
            const ir = compileOk(`
                workflow neg(flag: boolean): unknown {
                    const r = !flag;
                    return r;
                }
            `);
            const { eng } = makeEngine();

            let result = await eng.run(ir, { input: { flag: true } });
            expect(result.output).toBe(false);

            result = await eng.run(ir, { input: { flag: false } });
            expect(result.output).toBe(true);
        });

        it("negates a number with -", async () => {
            const ir = compileOk(`
                workflow neg(x: number): unknown {
                    const r = -x;
                    return r;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 42 } });

            expect(result.success).toBe(true);
            expect(result.output).toBe(-42);
        });
    });

    // ---- Branching (if/else, switch, ternary) ----

    describe("branching", () => {
        it("if/else takes the correct branch", async () => {
            // Branches that both bind to the same name produce valid runtime
            // IR but the static dominator analysis rejects them, so we
            // compile and run without validation.
            const ir = compileOk(
                `
                workflow check(x: number): unknown {
                    const big = x > 10;
                    if (big) {
                        const r = text.template("big: {{x}}", { x: x });
                        return r;
                    } else {
                        const r = text.template("small: {{x}}", { x: x });
                        return r;
                    }
                }
            `,
            );
            const { eng } = makeEngine();

            let result = await eng.run(ir, {
                input: { x: 20 },
            });
            expect(result.success).toBe(true);
            expect(result.output).toBe("big: 20");

            result = await eng.run(ir, {
                input: { x: 5 },
            });
            expect(result.success).toBe(true);
            expect(result.output).toBe("small: 5");
        });

        it("ternary expression picks the right value", async () => {
            const ir = compileOk(`
                workflow pick(flag: boolean): unknown {
                    const r = flag ? "yes" : "no";
                    return r;
                }
            `);
            const { eng } = makeEngine();

            let result = await eng.run(ir, { input: { flag: true } });
            expect(result.success).toBe(true);
            expect(result.output).toBe("yes");

            result = await eng.run(ir, { input: { flag: false } });
            expect(result.success).toBe(true);
            expect(result.output).toBe("no");
        });

        it("switch dispatches to the correct case", async () => {
            // Use task calls in switch arms (literal-only returns from
            // branches are not yet supported by the emitter).
            // All arms use the same binding name so the output template
            // resolves correctly regardless of which arm ran.
            const ir = compileOk(
                `
                workflow route(cmd: string): unknown {
                    switch (cmd) {
                        case "hello":
                            const r = text.template("greeting", {});
                            return r;
                        case "bye":
                            const r = text.template("farewell", {});
                            return r;
                        default:
                            const r = text.template("unknown", {});
                            return r;
                    }
                }
            `,
            );
            const { eng } = makeEngine();

            let result = await eng.run(ir, {
                input: { cmd: "hello" },
            });
            expect(result.success).toBe(true);
            expect(result.output).toBe("greeting");

            result = await eng.run(ir, {
                input: { cmd: "bye" },
            });
            expect(result.success).toBe(true);
            expect(result.output).toBe("farewell");

            result = await eng.run(ir, {
                input: { cmd: "other" },
            });
            expect(result.success).toBe(true);
            expect(result.output).toBe("unknown");
        });

        it("switch dispatches on integer discriminant", async () => {
            const ir = compileOk(`
                workflow route(code: integer): unknown {
                    switch (code) {
                        case 1:
                            const r = text.template("one", {});
                            return r;
                        case 2:
                            const r = text.template("two", {});
                            return r;
                        case 3:
                            const r = text.template("three", {});
                            return r;
                        default:
                            const r = text.template("other", {});
                            return r;
                    }
                }
            `);
            const { eng } = makeEngine();

            for (const [code, expected] of [
                [1, "one"],
                [2, "two"],
                [3, "three"],
                [99, "other"],
            ] as const) {
                const result = await eng.run(ir, { input: { code } });
                expect(result.success).toBe(true);
                expect(result.output).toBe(expected);
            }
        });

        it("switch executes only the matching arm's side effects", async () => {
            // Use a side-effect task per arm and verify only the selected
            // arm runs. Guards against any future regression where all
            // arms (or the wrong arm) would be evaluated.
            const calls: string[] = [];
            const makeArmTask = (name: string): TaskDefinition => ({
                name,
                sideEffects: false,
                inputSchema: {
                    type: "object",
                    required: ["tag"],
                    properties: { tag: { type: "string" } },
                },
                outputSchema: { type: "string" },
                async execute(input: any) {
                    calls.push(name);
                    return { kind: "ok", output: `${name}:${input.tag}` };
                },
            });
            const armA = makeArmTask("arm.a");
            const armB = makeArmTask("arm.b");
            const armC = makeArmTask("arm.c");
            const extras = [armA, armB, armC];
            const ir = compileOk(
                `
                workflow route(cmd: string): unknown {
                    switch (cmd) {
                        case "a":
                            const r = arm.a("x");
                            return r;
                        case "b":
                            const r = arm.b("x");
                            return r;
                        default:
                            const r = arm.c("x");
                            return r;
                    }
                }
            `,
                extras,
            );
            const { eng } = makeEngine(extras);

            let result = await eng.run(ir, { input: { cmd: "b" } });
            expect(result.success).toBe(true);
            expect(result.output).toBe("arm.b:x");
            expect(calls).toEqual(["arm.b"]);

            calls.length = 0;
            result = await eng.run(ir, { input: { cmd: "a" } });
            expect(result.success).toBe(true);
            expect(calls).toEqual(["arm.a"]);

            calls.length = 0;
            result = await eng.run(ir, { input: { cmd: "zzz" } });
            expect(result.success).toBe(true);
            expect(calls).toEqual(["arm.c"]);
        });

        it("switch nested inside map dispatches per-iteration", async () => {
            const ir = compileOk(`
                workflow labelAll(cmds: string[]): unknown {
                    const results = map(cmds, (cmd) => {
                        switch (cmd) {
                            case "hi":
                                const r = text.template("greeting", {});
                                return r;
                            case "bye":
                                const r = text.template("farewell", {});
                                return r;
                            default:
                                const r = text.template("unknown", {});
                                return r;
                        }
                    });
                    return results;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { cmds: ["hi", "bye", "other", "hi"] },
            });
            expect(result.success).toBe(true);
            expect(result.output).toEqual([
                "greeting",
                "farewell",
                "unknown",
                "greeting",
            ]);
        });

        it("if/else with continuation after branch", async () => {
            const ir = compileOk(`
                workflow classify(x: number): unknown {
                    const big = x > 10;
                    if (big) {
                        const label = text.template("big", {});
                    } else {
                        const label = text.template("small", {});
                    }
                    const result = text.template("done", {});
                    return result;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 5 } });

            expect(result.success).toBe(true);
            expect(result.output).toBe("done");
        });
    });

    // ---- Throw ----

    describe("throw", () => {
        it("throw causes workflow failure", async () => {
            const ir = compileOk(`
                workflow fail(): unknown {
                    throw "something went wrong";
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });

            expect(result.success).toBe(false);
            expect(result.error?.message).toContain("something went wrong");
        });

        it("throw propagates the message string to RunResult.error", async () => {
            const ir = compileOk(`
                workflow fail(): unknown {
                    throw "explicit error message";
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });

            expect(result.success).toBe(false);
            expect(result.error).toBeDefined();
            expect(result.error!.message).toBe("explicit error message");
        });

        it("throw inside if branch propagates message", async () => {
            const ir = compileOk(`
                workflow failIf(x: boolean): unknown {
                    if (x) {
                        throw "branch error";
                    }
                    return "ok";
                }
            `);
            const { eng } = makeEngine();

            const failing = await eng.run(ir, { input: { x: true } });
            expect(failing.success).toBe(false);
            expect(failing.error!.message).toBe("branch error");

            const passing = await eng.run(ir, { input: { x: false } });
            expect(passing.success).toBe(true);
        });
    });

    // ---- Map ----

    describe("map", () => {
        it("maps over a collection", async () => {
            const { eng } = makeEngine([webFetch]);
            const ir = compileOk(
                `
                workflow fetchAll(urls: string[]): unknown {
                    const results = map(urls, (url) => {
                        const page = web.fetch(url);
                        return page.body;
                    });
                    return results;
                }
            `,
                [webFetch],
            );
            const result = await eng.run(ir, {
                input: { urls: ["a.com", "b.com", "c.com"] },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([
                "content of a.com",
                "content of b.com",
                "content of c.com",
            ]);
        });

        it("maps over an empty collection", async () => {
            const { eng } = makeEngine([webFetch]);
            const ir = compileOk(
                `
                workflow fetchAll(urls: string[]): unknown {
                    const results = map(urls, (url) => {
                        const page = web.fetch(url);
                        return page;
                    });
                    return results;
                }
            `,
                [webFetch],
            );
            const result = await eng.run(ir, { input: { urls: [] } });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([]);
        });

        it("map body can reference outer-scope params", async () => {
            const ir = compileOk(`
                workflow addToAll(items: number[], offset: number): unknown {
                    const results = map(items, (item) => {
                        const sum = item + offset;
                        return sum;
                    });
                    return results;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { items: [1, 2, 3], offset: 10 },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([11, 12, 13]);
        });
    });

    // ---- Filter ----

    describe("filter", () => {
        it("filters a collection by predicate", async () => {
            const ir = compileOk(`
                workflow positives(nums: number[]): unknown {
                    const results = filter(nums, (n) => {
                        const ok = n > 0;
                        return ok;
                    });
                    return results;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { nums: [3, -1, 5, -2, 0, 7] },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([3, 5, 7]);
        });

        it("filter with no matches returns empty array", async () => {
            const ir = compileOk(`
                workflow none(nums: number[]): unknown {
                    const results = filter(nums, (n) => {
                        const ok = n > 100;
                        return ok;
                    });
                    return results;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { nums: [1, 2, 3] },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([]);
        });

        it("filter body can reference outer-scope params", async () => {
            const ir = compileOk(`
                workflow above(nums: number[], threshold: number): unknown {
                    const results = filter(nums, (n) => {
                        const ok = n > threshold;
                        return ok;
                    });
                    return results;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { nums: [1, 5, 10, 15, 20], threshold: 8 },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([10, 15, 20]);
        });
    });

    // ---- Parallel (fork) ----

    describe("parallel", () => {
        it("runs branches concurrently and collects results", async () => {
            const callOrder: string[] = [];
            const taskA: TaskDefinition = {
                ...webFetch,
                name: "task.a",
                async execute() {
                    callOrder.push("a");
                    return { kind: "ok", output: { body: "result-a" } };
                },
            };
            const taskB: TaskDefinition = {
                ...webFetch,
                name: "task.b",
                async execute() {
                    callOrder.push("b");
                    return { kind: "ok", output: { body: "result-b" } };
                },
            };

            const { eng } = makeEngine([taskA, taskB]);
            const ir = compileOk(
                `
                workflow both(): unknown {
                    const results = parallel(
                        () => {
                            const a = task.a("");
                            return a.body;
                        },
                        () => {
                            const b = task.b("");
                            return b.body;
                        }
                    );
                    return results;
                }
            `,
                [taskA, taskB],
            );
            const result = await eng.run(ir, { input: {} });

            expect(result.success).toBe(true);
            // Both branches ran
            expect(callOrder).toContain("a");
            expect(callOrder).toContain("b");
            // Output is an object with branch keys
            const output = result.output as Record<string, unknown>;
            expect(output.branch_0).toBe("result-a");
            expect(output.branch_1).toBe("result-b");
        });
    });

    // ---- ParallelMap (forkMap) ----

    describe("parallelMap", () => {
        it("maps over a collection in parallel", async () => {
            const { eng } = makeEngine([webFetch]);
            const ir = compileOk(
                `
                workflow fetchParallel(urls: string[]): unknown {
                    const results = parallelMap(urls, (url) => {
                        const page = web.fetch(url);
                        return page.body;
                    });
                    return results;
                }
            `,
                [webFetch],
            );
            const result = await eng.run(ir, {
                input: { urls: ["a.com", "b.com"] },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([
                "content of a.com",
                "content of b.com",
            ]);
        });
    });

    // ---- Attempts ----

    describe("attempts", () => {
        const ATTEMPTS_SOURCE = `
            workflow fetchWithAttempts(url: string): unknown {
                return attempts(3, () => {
                    const result = web.fetch(url);
                    return result.body;
                });
            }
        `;

        function compileAttempts(): WorkflowIR {
            return compileOk(ATTEMPTS_SOURCE, [webFetch]);
        }

        function attemptsEngine(execute: TaskDefinition["execute"]) {
            const task: TaskDefinition = { ...webFetch, execute };
            const builtins = allBuiltinTasks.filter(
                (t) => t.name !== "web.fetch",
            );
            const reg = makeRegistry(...builtins, task);
            const eng = new WorkflowEngine(reg);
            const events = collectEvents(eng);
            return { eng, events };
        }

        it("succeeds on first attempt without retrying", async () => {
            let callCount = 0;
            const { eng, events } = attemptsEngine(async (input: any) => {
                callCount++;
                return { kind: "ok", output: { body: `page: ${input.url}` } };
            });

            const ir = compileAttempts();
            const result = await eng.run(ir, {
                input: { url: "https://example.com" },
            });

            expect(result.success).toBe(true);
            expect(callCount).toBe(1);
            expect(result.output).toBe("page: https://example.com");

            const iterEvents = events.filter(
                (e) => e.type === "loopIterationStarted",
            );
            expect(iterEvents.length).toBe(1);
        });

        it("retries on failure then succeeds", async () => {
            let callCount = 0;
            const { eng, events } = attemptsEngine(async (input: any) => {
                callCount++;
                if (callCount <= 2) {
                    return {
                        kind: "fail",
                        error: { message: `Attempt ${callCount} failed` },
                    };
                }
                return {
                    kind: "ok",
                    output: { body: `page: ${input.url}` },
                };
            });

            const ir = compileAttempts();
            const result = await eng.run(ir, {
                input: { url: "https://flaky.example.com" },
            });

            expect(result.success).toBe(true);
            expect(callCount).toBe(3);
            expect(result.output).toBe("page: https://flaky.example.com");

            const iterEvents = events.filter(
                (e) => e.type === "loopIterationStarted",
            );
            expect(iterEvents.length).toBe(3);
        });

        it("exhausts attempts and fails", async () => {
            let callCount = 0;
            const { eng } = attemptsEngine(async () => {
                callCount++;
                return {
                    kind: "fail",
                    error: { message: `Attempt ${callCount} failed` },
                };
            });

            const ir = compileAttempts();
            const result = await eng.run(ir, {
                input: { url: "https://down.example.com" },
            });

            expect(result.success).toBe(false);
            expect(callCount).toBe(3);
        });
    });

    // ---- Composition: multiple features together ----

    describe("composition", () => {
        it("map + filter pipeline", async () => {
            const ir = compileOk(`
                workflow pipeline(nums: number[]): unknown {
                    const doubled = map(nums, (n) => {
                        const r = n * 2;
                        return r;
                    });
                    const big = filter(doubled, (n) => {
                        const ok = n > 5;
                        return ok;
                    });
                    return big;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { nums: [1, 2, 3, 4, 5] },
            });

            // doubled: [2, 4, 6, 8, 10], filtered > 5: [6, 8, 10]
            expect(result.success).toBe(true);
            expect(result.output).toEqual([6, 8, 10]);
        });

        it("if/else with arithmetic", async () => {
            const ir = compileOk(
                `
                workflow absVal(x: number): unknown {
                    const negative = x < 0;
                    if (negative) {
                        const r = -x;
                        return r;
                    } else {
                        return x;
                    }
                }
            `,
            );
            const { eng } = makeEngine();

            let result = await eng.run(ir, {
                input: { x: -5 },
            });
            expect(result.output).toBe(5);

            result = await eng.run(ir, {
                input: { x: 3 },
            });
            expect(result.output).toBe(3);
        });

        it("task call + binary op + ternary", async () => {
            const { eng } = makeEngine([webFetch]);
            const ir = compileOk(
                `
                workflow checkPage(url: string, minLen: number): unknown {
                    const page = web.fetch(url);
                    const ok = minLen > 5;
                    const status = ok ? "long enough" : "too short";
                    return status;
                }
            `,
                [webFetch],
            );

            const result = await eng.run(ir, {
                input: { url: "test.com", minLen: 10 },
            });
            expect(result.success).toBe(true);
            expect(result.output).toBe("long enough");

            const result2 = await eng.run(ir, {
                input: { url: "test.com", minLen: 3 },
            });
            expect(result2.success).toBe(true);
            expect(result2.output).toBe("too short");
        });

        it("map with task calls inside body", async () => {
            const { eng } = makeEngine([webFetch]);
            const ir = compileOk(
                `
                workflow fetchAll(urls: string[]): unknown {
                    const pages = map(urls, (url) => {
                        const page = web.fetch(url);
                        const msg = text.template("Page: {{body}}", { body: page.body });
                        return msg;
                    });
                    return pages;
                }
            `,
                [webFetch],
            );
            const result = await eng.run(ir, {
                input: { urls: ["a.com", "b.com"] },
            });

            expect(result.success).toBe(true);
            expect(result.output).toEqual([
                "Page: content of a.com",
                "Page: content of b.com",
            ]);
        });
    });

    // ---- Additional gap coverage ----

    // NOTE: The following tests are commented out because they expose real bugs
    // in the DSL compiler that should be tracked and fixed separately.

    describe("nested control flow", () => {
        it("filter + map pipeline", async () => {
            const ir = compileOk(`
                workflow pipeline(nums: number[]): unknown {
                    const pos = filter(nums, (n) => {
                        const ok = n > 0;
                        return ok;
                    });
                    const doubled = map(pos, (n) => {
                        const r = n * 2;
                        return r;
                    });
                    return doubled;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { nums: [-3, 1, -1, 4, 0, 2] },
            });
            expect(result.success).toBe(true);
            expect(result.output).toEqual([2, 8, 4]);
        });

        it("ternary inside map body (branch in forkMap scope)", async () => {
            const ir = compileOk(`
                workflow test(nums: number[]): unknown {
                    const result = map(nums, (n) => {
                        const r = n > 10 ? "big" : "small";
                        return r;
                    });
                    return result;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, {
                input: { nums: [5, 15, 20, 3] },
            });
            expect(result.success).toBe(true);
            expect(result.output).toEqual(["small", "big", "big", "small"]);
        });
    });

    describe("sub-workflow composition", () => {
        it("calls a helper workflow and binds its output", async () => {
            const ir = compileOk(`
                workflow double(n: number): number {
                    const r = n * 2;
                    return r;
                }
                export workflow main(x: number): number {
                    const y = double(x);
                    return y;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 21 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(42);
        });

        it("supports defaulted argument", async () => {
            const ir = compileOk(`
                workflow add(a: number, b: number = 10): number {
                    const r = a + b;
                    return r;
                }
                export workflow main(x: number): number {
                    const y = add(x);
                    return y;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 5 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(15);
        });

        it("supports defaulted argument with path access on earlier param", async () => {
            const ir = compileOk(`
                workflow add(opts: { a: number, b: number }, extra: number = opts.a): number {
                    const r = opts.a + opts.b + extra;
                    return r;
                }
                export workflow main(o: { a: number, b: number }): number {
                    const y = add(o);
                    return y;
                }
            `);
            const { eng } = makeEngine();
            // o = { a: 3, b: 4 }; extra defaults to o.a = 3; result = 3+4+3 = 10
            const result = await eng.run(ir, { input: { o: { a: 3, b: 4 } } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(10);
        });

        it("propagates sub-workflow output through caller pipeline", async () => {
            const ir = compileOk(`
                workflow inc(n: number): number {
                    const r = n + 1;
                    return r;
                }
                export workflow main(x: number): number {
                    const a = inc(x);
                    const b = inc(a);
                    const c = inc(b);
                    return c;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 0 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(3);
        });

        it("supports nested sub-workflow calls", async () => {
            const ir = compileOk(`
                workflow leaf(n: number): number {
                    const r = n * 10;
                    return r;
                }
                workflow middle(n: number): number {
                    const r = leaf(n);
                    return r;
                }
                export workflow main(x: number): number {
                    const y = middle(x);
                    return y;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 7 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(70);
        });

        it("sub-workflow has its own scope (no caller binding leakage)", async () => {
            // 'r' is bound inside both workflows; the callee's 'r' must
            // not be visible to or shadow the caller's 'r'.
            const ir = compileOk(`
                workflow helper(n: number): number {
                    const r = n + 100;
                    return r;
                }
                export workflow main(x: number): number {
                    const r = helper(x);
                    const out = r + 1;
                    return out;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 5 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(106);
        });

        it("sub-workflow failure propagates when caller has no onError", async () => {
            const failTask: TaskDefinition = {
                name: "fail.always",
                sideEffects: false,
                inputSchema: { type: "object" },
                outputSchema: { type: "object" },
                async execute() {
                    return {
                        kind: "fail",
                        error: { message: "boom" },
                    };
                },
            };
            const ir = compileOk(
                `
                workflow helper(x: unknown): unknown {
                    const y = fail.always({});
                    return y;
                }
                export workflow main(x: unknown): unknown {
                    const r = helper(x);
                    return r;
                }
            `,
                [failTask],
            );
            const { eng } = makeEngine([failTask]);
            const result = await eng.run(ir, { input: { x: 1 } });
            expect(result.success).toBe(false);
            expect(result.error?.message).toMatch(/boom/);
        });

        it("caller onError recovers from sub-workflow failure (manual IR)", async () => {
            // DSL does not yet expose onError syntax on workflow call sites,
            // so this test constructs the IR by hand to exercise the engine
            // recovery path.
            //
            // Graph: callHelper --[onError]--> recover (binds fallback=99, terminal)
            //                   --[next]-----> done    (terminal, binds helperResult=1, unreachable)
            //
            // helper always throws. onError fires, recover runs math.add(0,99),
            // binds "fallback". Main output reads "fallback". Proves recovery ran.
            const failTask: TaskDefinition = {
                name: "fail.always",
                sideEffects: false,
                inputSchema: { type: "object" },
                outputSchema: { type: "object" },
                async execute() {
                    return { kind: "fail", error: { message: "sub failed" } };
                },
            };

            const ir: WorkflowIR = {
                kind: "workflow",
                version: "1",
                entry: "main",
                workflows: {
                    helper: {
                        inputSchema: { type: "object" },
                        outputSchema: { type: "object" },
                        entry: "doFail",
                        output: { $from: "scope", name: "unused" } as Template,
                        nodes: {
                            doFail: {
                                kind: "task",
                                task: "fail.always",
                                inputSchema: { type: "object" },
                                outputSchema: { type: "object" },
                                inputs: {},
                                bind: "unused",
                            },
                        },
                    },
                    main: {
                        inputSchema: { type: "object" },
                        outputSchema: { type: "number" },
                        entry: "callHelper",
                        output: { $from: "scope", name: "result" } as Template,
                        nodes: {
                            callHelper: {
                                kind: "workflowCall",
                                workflowRef: { name: "helper" },
                                inputSchema: { type: "object" },
                                outputSchema: { type: "object" },
                                inputs: {},
                                // no bind: we don't use the success output
                                next: "done",
                                onError: "recover",
                            },
                            recover: {
                                // onError path: return 99 as fallback
                                kind: "task",
                                task: "math.add",
                                inputSchema: {
                                    type: "object",
                                    required: ["left", "right"],
                                    properties: {
                                        left: { type: "number" },
                                        right: { type: "number" },
                                    },
                                },
                                outputSchema: { type: "number" },
                                inputs: { left: 0, right: 99 },
                                bind: "result",
                                // no next → terminal
                            },
                            done: {
                                // success path: return 1 (unreachable when helper fails)
                                kind: "task",
                                task: "math.add",
                                inputSchema: {
                                    type: "object",
                                    required: ["left", "right"],
                                    properties: {
                                        left: { type: "number" },
                                        right: { type: "number" },
                                    },
                                },
                                outputSchema: { type: "number" },
                                inputs: { left: 0, right: 1 },
                                bind: "result",
                                // no next → terminal
                            },
                        },
                    },
                },
            };

            const { eng } = makeEngine([failTask]);
            const result = await eng.run(ir, { input: {} });
            // recover node ran (not done): output is 99, not 1
            expect(result.error).toBeUndefined();
            expect(result.success).toBe(true);
            expect(result.output).toBe(99);
        });

        it("same helper called multiple times has isolated bindings", async () => {
            const ir = compileOk(`
                workflow add5(n: number): number {
                    const r = n + 5;
                    return r;
                }
                export workflow main(x: number): number {
                    const a = add5(x);
                    const b = add5(x);
                    const sum = a + b;
                    return sum;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 10 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(30);
        });

        it("concurrent run() on same engine is rejected", async () => {
            const ir = compileOk(`
                export workflow main(x: number): number {
                    const r = x + 1;
                    return r;
                }
            `);
            const { eng } = makeEngine();
            const p1 = eng.run(ir, { input: { x: 1 } });
            const r2 = await eng.run(ir, { input: { x: 2 } });
            // The second call should fail-fast with the re-entrancy
            // guard message (since the first is still in-flight).
            // Outcome depends on timing — accept either ordering.
            const r1 = await p1;
            const failed = [r1, r2].filter((r) => !r.success);
            const succeeded = [r1, r2].filter((r) => r.success);
            expect(succeeded.length).toBeGreaterThanOrEqual(1);
            // At least one must succeed (whichever ran first).
            // The other may succeed too if both ran sequentially —
            // accepted because Promise scheduling can serialize them.
            // If one fails, the message must be the re-entrancy guard.
            for (const f of failed) {
                expect(f.error?.message).toMatch(/not re-entrant/i);
            }
        });

        it("sub-workflow timeout aborts and surfaces timeout error", async () => {
            const slowTask: TaskDefinition = {
                name: "task.slow",
                sideEffects: false,
                inputSchema: { type: "object" },
                outputSchema: { type: "object" },
                async execute(_input: any, ctx: TaskContext) {
                    return new Promise((resolve, reject) => {
                        const onAbort = () => reject(new Error("aborted"));
                        if (ctx.signal.aborted) {
                            onAbort();
                            return;
                        }
                        ctx.signal.addEventListener("abort", onAbort, {
                            once: true,
                        });
                        // Never resolves on its own.
                    });
                },
            };
            // Build IR manually since the DSL doesn't currently expose
            // a per-call timeoutMs syntax for workflowCall nodes; patch
            // the emitted node directly.
            const ir = compileOk(
                `
                workflow slow(x: unknown): unknown {
                    const y = task.slow({});
                    return y;
                }
                export workflow main(x: unknown): unknown {
                    const r = slow(x);
                    return r;
                }
            `,
                [slowTask],
            );
            // Locate the workflowCall node in main and inject timeout.
            const mainBody = ir.workflows[ir.entry];
            const call = Object.values(mainBody.nodes).find(
                (n: any) => n.kind === "workflowCall",
            ) as any;
            expect(call).toBeDefined();
            call.timeoutMs = 50;
            const { eng } = makeEngine([slowTask]);
            const result = await eng.run(ir, { input: { x: 1 } });
            expect(result.success).toBe(false);
            expect(result.error?.message).toMatch(/timed out/i);
        });

        it("sub-workflow returns complex object", async () => {
            const ir = compileOk(`
                workflow pack(n: number): unknown {
                    const r = { name: "x", count: n };
                    return r;
                }
                export workflow main(x: number): unknown {
                    const y = pack(x);
                    return y;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 5 } });
            expect(result.success).toBe(true);
            expect(result.output).toEqual({ name: "x", count: 5 });
        });

        it("sub-workflow nodeStarted/nodeCompleted events carry workflowCall nodeId", async () => {
            const ir = compileOk(`
                workflow inc(n: number): number {
                    const r = n + 1;
                    return r;
                }
                export workflow main(x: number): number {
                    const y = inc(x);
                    return y;
                }
            `);
            const { eng, events } = makeEngine();
            const result = await eng.run(ir, { input: { x: 10 } });
            expect(result.success).toBe(true);
            // Find the workflowCall node id in the entry body.
            const mainBody = ir.workflows[ir.entry];
            const call = Object.entries(mainBody.nodes).find(
                ([, n]) => (n as any).kind === "workflowCall",
            )!;
            const callNodeId = call[0];
            const started = events.find(
                (e) =>
                    e.type === "nodeStarted" &&
                    (e as any).nodeId === callNodeId,
            );
            const completed = events.find(
                (e) =>
                    e.type === "nodeCompleted" &&
                    (e as any).nodeId === callNodeId,
            );
            expect(started).toBeDefined();
            expect(completed).toBeDefined();
            expect((completed as any).output).toBe(11);
        });
    });

    // ---- Cross-file imports ----

    class MemoryResolver implements FileResolver {
        constructor(private files: Record<string, string>) {}
        resolve(spec: string, importerAbsPath: string): string {
            const parts = importerAbsPath.split("/").slice(0, -1);
            for (const segment of spec.split("/")) {
                if (segment === "." || segment === "") continue;
                if (segment === "..") parts.pop();
                else parts.push(segment);
            }
            return parts.join("/");
        }
        read(absPath: string): string {
            const src = this.files[absPath];
            if (src === undefined) throw new Error(`No such file: ${absPath}`);
            return src;
        }
    }

    function compileFileOk(
        entryPath: string,
        files: Record<string, string>,
        extraTasks: TaskDefinition[] = [],
    ): WorkflowIR {
        const schemas = taskSchemasFrom([...allBuiltinTasks, ...extraTasks]);
        const resolver = new MemoryResolver(files);
        const result = compileFile(entryPath, schemas, {
            resolver,
            validate: true,
        });
        if (result.errors.length > 0) {
            throw new Error(
                `Compile errors:\n${result.errors.map((e) => e.message).join("\n")}`,
            );
        }
        return result.ir!;
    }

    describe("cross-file imports", () => {
        it("executes an imported helper workflow", async () => {
            const ir = compileFileOk("/p/main.wf", {
                "/p/helper.wf": `
                    export workflow double(n: number): number {
                        const r = n * 2;
                        return r;
                    }
                `,
                "/p/main.wf": `
                    import { double } from "./helper.wf";
                    export workflow main(x: number): number {
                        const y = double(x);
                        return y;
                    }
                `,
            });
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 7 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(14);
        });

        it("executes an imported helper via alias", async () => {
            const ir = compileFileOk("/p/main.wf", {
                "/p/lib.wf": `
                    export workflow triple(n: number): number {
                        const r = n * 3;
                        return r;
                    }
                `,
                "/p/main.wf": `
                    import { triple as mul3 } from "./lib.wf";
                    export workflow main(x: number): number {
                        const y = mul3(x);
                        return y;
                    }
                `,
            });
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: { x: 4 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(12);
        });

        it("chains two imported helpers", async () => {
            const ir = compileFileOk("/p/main.wf", {
                "/p/math.wf": `
                    export workflow inc(n: number): number {
                        const r = n + 1;
                        return r;
                    }
                    export workflow double(n: number): number {
                        const r = n * 2;
                        return r;
                    }
                `,
                "/p/main.wf": `
                    import { inc, double } from "./math.wf";
                    export workflow main(x: number): number {
                        const a = inc(x);
                        const b = double(a);
                        return b;
                    }
                `,
            });
            const { eng } = makeEngine();
            // inc(3) = 4, double(4) = 8
            const result = await eng.run(ir, { input: { x: 3 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(8);
        });

        it("private workflows with same name in different files execute independently", async () => {
            const ir = compileFileOk("/p/main.wf", {
                "/p/helper.wf": `
                    workflow scale(x: number): number {
                        const r = x * 10;
                        return r;
                    }
                    export workflow process(n: number): number {
                        const r = scale(n);
                        return r;
                    }
                `,
                "/p/main.wf": `
                    import { process } from "./helper.wf";
                    workflow scale(x: number): number {
                        const r = x + 1;
                        return r;
                    }
                    export workflow main(x: number): number {
                        const a = scale(x);
                        const b = process(x);
                        return a + b;
                    }
                `,
            });
            const { eng } = makeEngine();
            // main.scale(3) = 4, helper.scale(3) = 30, result = 4 + 30 = 34
            const result = await eng.run(ir, { input: { x: 3 } });
            expect(result.success).toBe(true);
            expect(result.output).toBe(34);
        });
    });

    // ---- Destructuring ----

    describe("destructuring", () => {
        it("destructures an array literal and returns first element", async () => {
            const ir = compileOk(`
                workflow main(): unknown {
                    const [a, b] = ["hello", "world"];
                    return a;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });
            expect(result.success).toBe(true);
            expect(result.output).toBe("hello");
        });

        it("destructures an array literal and returns second element", async () => {
            const ir = compileOk(`
                workflow main(): unknown {
                    const [a, b] = ["hello", "world"];
                    return b;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });
            expect(result.success).toBe(true);
            expect(result.output).toBe("world");
        });

        it("destructures a number array and uses elements in arithmetic", async () => {
            const ir = compileOk(`
                workflow main(): number {
                    const [x, y] = [3, 4];
                    const r = x + y;
                    return r;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });
            expect(result.success).toBe(true);
            expect(result.output).toBe(7);
        });
    });

    // ---- Named-record call syntax ----

    describe("named-record call syntax", () => {
        it("calls a workflow with named-record object literal", async () => {
            const ir = compileOk(`
                workflow add(a: number, b: number): number {
                    const r = a + b;
                    return r;
                }
                export workflow main(): number {
                    const r = add({ a: 3, b: 4 });
                    return r;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });
            expect(result.success).toBe(true);
            expect(result.output).toBe(7);
        });

        it("named-record with default for omitted key", async () => {
            const ir = compileOk(`
                workflow addWithDefault(a: number, b: number = 10): number {
                    const r = a + b;
                    return r;
                }
                export workflow main(): number {
                    const r = addWithDefault({ a: 5 });
                    return r;
                }
            `);
            const { eng } = makeEngine();
            const result = await eng.run(ir, { input: {} });
            expect(result.success).toBe(true);
            expect(result.output).toBe(15);
        });
    });
});
