import { PromptPlan } from "./promptPlan.js";
// ---------------------------------------------------------------------------
// Core numbering invariants
// ---------------------------------------------------------------------------
describe("PromptPlan — main step numbering", () => {
    it("generates sequential main-step labels starting at 1", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("A", "Step A", () => "body-a")
            .step("B", "Step B", () => "body-b")
            .step("C", "Step C", () => "body-c")
            .done();
        expect(plan.labels.A).toBe("1");
        expect(plan.labels.B).toBe("2");
        expect(plan.labels.C).toBe("3");
    });
    it("supports startFrom: 0 for execution-plan style (Step 0, Step 1, …)", () => {
        const plan = new PromptPlan({ startFrom: 0 })
            .addPhase("p", "Phase")
            .step("COVERAGE", "Coverage", () => "")
            .step("ENRICH", "Enrich", () => "")
            .step("DIVERSITY", "Diversity", () => "")
            .step("EXECUTE", "Execute", () => "")
            .done();
        expect(plan.labels.COVERAGE).toBe("0");
        expect(plan.labels.ENRICH).toBe("1");
        expect(plan.labels.DIVERSITY).toBe("2");
        expect(plan.labels.EXECUTE).toBe("3");
    });
    it("main step labels are compile-time stable — identical to labelFor() with any params", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("X", "X", () => "")
            .step("Y", "Y", () => "")
            .done();
        const params = { flag: true };
        expect(plan.labels.X).toBe(plan.labelFor("X", params));
        expect(plan.labels.Y).toBe(plan.labelFor("Y", params));
    });
    it("auto-renumbers when a step is inserted — key contract of the abstraction", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("FIRST", "First", () => "")
            .step("NEW", "Newly inserted", () => "") // inserted before what was "2"
            .step("SECOND", "Second", () => "")
            .done();
        expect(plan.labels.FIRST).toBe("1");
        expect(plan.labels.NEW).toBe("2");
        expect(plan.labels.SECOND).toBe("3"); // auto-renumbered from "2" to "3"
    });
});
// ---------------------------------------------------------------------------
// Sub-step numbering
// ---------------------------------------------------------------------------
describe("PromptPlan — sub-step numbering", () => {
    it("assigns sequential sub-step labels within their parent (1.1, 1.2, 2.1, …)", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("READ", "Read", () => "")
            .subStep("PATHS", "Paths", () => "")
            .step("EXTRACT", "Extract", () => "")
            .subStep("TRACE", "Trace", () => "", { when: (p) => p.flag })
            .done();
        expect(plan.labels.READ).toBe("1");
        expect(plan.labels.PATHS).toBe("1.1");
        expect(plan.labels.EXTRACT).toBe("2");
        // TRACE is conditional — NOT in labels map
        expect(plan.labels.TRACE).toBeUndefined();
    });
    it("sub() does not advance the main counter", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("A", "A", () => "")
            .subStep("A1", "A1", () => "")
            .subStep("A2", "A2", () => "")
            .step("B", "B", () => "")
            .done();
        expect(plan.labels.A).toBe("1");
        expect(plan.labels.A1).toBe("1.1");
        expect(plan.labels.A2).toBe("1.2");
        expect(plan.labels.B).toBe("2");
    });
    it("conditional sub-step is absent from static labels map", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("A", "A", () => "")
            .subStep("COND", "Conditional", () => "", { when: (p) => p.flag })
            .done();
        expect(plan.labels.A).toBe("1");
        expect(plan.labels.COND).toBeUndefined();
    });
});
// ---------------------------------------------------------------------------
// labelFor() — dynamic labels
// ---------------------------------------------------------------------------
describe("PromptPlan — labelFor()", () => {
    const plan = new PromptPlan()
        .addPhase("p", "Phase")
        .step("READ", "Read", () => "")
        .subStep("PATHS", "Paths", () => "")
        .step("EXTRACT", "Extract", () => "")
        .subStep("TRACE", "Trace", () => "", { when: (p) => p.flag })
        .step("DRAFT", "Draft", () => "")
        .done();
    it("returns the static label for always-present main steps", () => {
        const p = { flag: true };
        expect(plan.labelFor("READ", p)).toBe("1");
        expect(plan.labelFor("EXTRACT", p)).toBe("2");
        expect(plan.labelFor("DRAFT", p)).toBe("3");
    });
    it("returns the static label for always-present sub-steps", () => {
        expect(plan.labelFor("PATHS", { flag: false })).toBe("1.1");
    });
    it("returns '2.1' for the conditional sub-step when condition is true", () => {
        expect(plan.labelFor("TRACE", { flag: true })).toBe("2.1");
    });
    it("returns null for the conditional sub-step when condition is false", () => {
        expect(plan.labelFor("TRACE", { flag: false })).toBeNull();
    });
    it("throws for an unknown key", () => {
        expect(() => plan.labelFor("UNKNOWN", { flag: true })).toThrow('PromptPlan: unknown step key "UNKNOWN"');
    });
});
// ---------------------------------------------------------------------------
// Conditional sub-step: re-numbering when sibling is absent
// ---------------------------------------------------------------------------
describe("PromptPlan — conditional sub-step re-numbering", () => {
    const plan = new PromptPlan()
        .addPhase("p", "Phase")
        .step("A", "A", () => "")
        .subStep("A_COND", "Conditional A", () => "", { when: (p) => p.flag })
        .subStep("A_ALWAYS", "Always present", () => "always")
        .done();
    it("when condition is true, A_COND=1.1 and A_ALWAYS=1.2", () => {
        expect(plan.labelFor("A_COND", { flag: true })).toBe("1.1");
        expect(plan.labelFor("A_ALWAYS", { flag: true })).toBe("1.2");
    });
    it("when condition is false, A_COND=null and A_ALWAYS re-numbers to 1.1", () => {
        expect(plan.labelFor("A_COND", { flag: false })).toBeNull();
        expect(plan.labelFor("A_ALWAYS", { flag: false })).toBe("1.1");
    });
});
// ---------------------------------------------------------------------------
// render() — output format
// ---------------------------------------------------------------------------
describe("PromptPlan — render()", () => {
    it("emits ### Step N: Title headers for hash format", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase", { stepFormat: "hash" })
            .step("A", "Do something", () => "body text")
            .done();
        const output = plan.render({ flag: true });
        expect(output).toContain("### Step 1: Do something");
        expect(output).toContain("body text");
    });
    it("emits **Step N — Title** headers for bold format", () => {
        const plan = new PromptPlan({ startFrom: 0 })
            .addPhase("p", "Phase", { stepFormat: "bold" })
            .step("A", "Coverage", () => "coverage body")
            .done();
        const output = plan.render({ flag: true });
        expect(output).toContain("**Step 0 — Coverage**");
        expect(output).toContain("coverage body");
    });
    it("emits the phase header when headerLevel is not hidden", () => {
        const plan = new PromptPlan()
            .addPhase("p", "My Phase Title", { headerLevel: "##" })
            .step("A", "Step A", () => "")
            .done();
        expect(plan.render({ flag: true })).toContain("## My Phase Title");
    });
    it("does NOT emit a phase header when headerLevel is hidden", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Hidden Phase", { headerLevel: "hidden" })
            .step("A", "Step A", () => "")
            .done();
        expect(plan.render({ flag: true })).not.toContain("Hidden Phase");
    });
    it("omits a conditional sub-step when condition is false", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("A", "A", () => "step-a-body")
            .subStep("COND", "Conditional", () => "cond-body", { when: (p) => p.flag })
            .done();
        const withFlag = plan.render({ flag: true });
        const withoutFlag = plan.render({ flag: false });
        expect(withFlag).toContain("cond-body");
        expect(withoutFlag).not.toContain("cond-body");
        expect(withoutFlag).not.toContain("Conditional");
    });
    it("re-numbers remaining sub-steps when a conditional one is absent", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("A", "A", () => "")
            .subStep("COND", "Conditional", () => "cond", { when: (p) => p.flag })
            .subStep("ALWAYS", "Always", () => "always")
            .done();
        // With flag: 1.1 = Conditional, 1.2 = Always
        expect(plan.render({ flag: true })).toContain("### Step 1.1: Conditional");
        expect(plan.render({ flag: true })).toContain("### Step 1.2: Always");
        // Without flag: conditional absent, Always re-numbers to 1.1
        expect(plan.render({ flag: false })).not.toContain("Conditional");
        expect(plan.render({ flag: false })).toContain("### Step 1.1: Always");
    });
    it("supports dynamic step titles via title functions", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase")
            .step("A", (p) => p.flag ? "Flag is on" : "Flag is off", () => "body")
            .done();
        expect(plan.render({ flag: true })).toContain("### Step 1: Flag is on");
        expect(plan.render({ flag: false })).toContain("### Step 1: Flag is off");
    });
    it("sections are separated by exactly one blank line", () => {
        const plan = new PromptPlan()
            .addPhase("p", "Phase", { headerLevel: "##" })
            .step("A", "Step A", () => "body-a")
            .step("B", "Step B", () => "body-b")
            .done();
        const output = plan.render({ flag: true });
        // Each pair of sections should have \n\n (exactly one blank line) between them
        expect(output).toContain("## Phase\n\n### Step 1");
        expect(output).toContain("body-a\n\n### Step 2");
    });
    it("reproduces the analysis-output step sequence", () => {
        const plan = new PromptPlan()
            .addPhase("analysis", "Your Task", { headerLevel: "##", stepFormat: "hash" })
            .step("READ", "Read the changed files", () => "read body")
            .subStep("PATHS", "Resolve endpoint paths", () => "paths body")
            .step("EXTRACT", "Extract endpoints", () => "extract body")
            .subStep("TRACE", "Trace callers", () => "trace body", { when: (p) => p.flag })
            .step("DRAFT", "Draft scenarios", () => "draft body")
            .step("CALL", "Call the tool", () => "call body")
            .done();
        expect(plan.labels.READ).toBe("1");
        expect(plan.labels.PATHS).toBe("1.1");
        expect(plan.labels.EXTRACT).toBe("2");
        expect(plan.labels.DRAFT).toBe("3");
        expect(plan.labels.CALL).toBe("4");
        // With flag: trace appears as 2.1
        const withTrace = plan.render({ flag: true });
        expect(withTrace).toContain("### Step 1: Read the changed files");
        expect(withTrace).toContain("### Step 1.1: Resolve endpoint paths");
        expect(withTrace).toContain("### Step 2: Extract endpoints");
        expect(withTrace).toContain("### Step 2.1: Trace callers");
        expect(withTrace).toContain("### Step 3: Draft scenarios");
        expect(withTrace).toContain("### Step 4: Call the tool");
        // Without flag: no trace step, sequence has no gap
        const noTrace = plan.render({ flag: false });
        expect(noTrace).not.toContain("trace body");
        expect(noTrace).not.toContain("Step 2.1");
        expect(noTrace).toContain("### Step 3: Draft scenarios");
    });
});
// ---------------------------------------------------------------------------
// preview() — developer review
// ---------------------------------------------------------------------------
describe("PromptPlan — preview()", () => {
    const plan = new PromptPlan()
        .addPhase("analysis", "Analysis Phase", { headerLevel: "##" })
        .step("READ", "Read", () => "")
        .subStep("PATHS", "Paths", () => "")
        .step("EXTRACT", "Extract", () => "")
        .subStep("TRACE", "Trace", () => "", {
        when: (p) => p.flag,
        whenDesc: "backend unmatched files present",
    })
        .step("DRAFT", "Draft", () => "")
        .done();
    it("reports correct activeStepCount and totalStepCount", () => {
        const withFlag = plan.preview({ flag: true });
        const withoutFlag = plan.preview({ flag: false });
        expect(withFlag.totalStepCount).toBe(5); // 3 main + 2 subs = 5
        expect(withFlag.activeStepCount).toBe(5); // all active
        expect(withoutFlag.totalStepCount).toBe(5);
        expect(withoutFlag.activeStepCount).toBe(4); // TRACE absent
    });
    it("marks the conditional sub-step inactive when condition is false", () => {
        const pv = plan.preview({ flag: false });
        const extractStep = pv.phases[0].steps.find(s => s.key === "EXTRACT");
        const traceSub = extractStep.subSteps.find(s => s.key === "TRACE");
        expect(traceSub.active).toBe(false);
        expect(traceSub.label).toBe("2.?");
        expect(traceSub.conditionDesc).toBe("backend unmatched files present");
    });
    it("marks the conditional sub-step active with correct label when condition is true", () => {
        const pv = plan.preview({ flag: true });
        const extractStep = pv.phases[0].steps.find(s => s.key === "EXTRACT");
        const traceSub = extractStep.subSteps.find(s => s.key === "TRACE");
        expect(traceSub.active).toBe(true);
        expect(traceSub.label).toBe("2.1");
    });
    it("includes all phase / step keys in the preview output", () => {
        const pv = plan.preview({ flag: true });
        expect(pv.phases).toHaveLength(1);
        expect(pv.phases[0].key).toBe("analysis");
        const stepKeys = pv.phases[0].steps.map(s => s.key);
        expect(stepKeys).toEqual(["READ", "EXTRACT", "DRAFT"]);
    });
});
// ---------------------------------------------------------------------------
// PhaseBuilder guard
// ---------------------------------------------------------------------------
describe("PhaseBuilder — error handling", () => {
    it("throws when subStep() is called before any step()", () => {
        expect(() => {
            new PromptPlan()
                .addPhase("p", "Phase")
                .subStep("ORPHAN", "Orphan", () => "");
        }).toThrow('subStep("ORPHAN") must follow a step() call in the same phase');
    });
    it("throws on duplicate step keys", () => {
        expect(() => {
            new PromptPlan()
                .addPhase("p", "Phase")
                .step("DUP", "First", () => "")
                .step("DUP", "Second", () => "");
        }).toThrow('PromptPlan: duplicate step key "DUP"');
    });
    it("throws on duplicate sub-step keys", () => {
        expect(() => {
            new PromptPlan()
                .addPhase("p", "Phase")
                .step("A", "A", () => "")
                .subStep("DUP", "First sub", () => "")
                .subStep("DUP", "Second sub", () => "");
        }).toThrow('PromptPlan: duplicate step key "DUP"');
    });
    it("throws when a sub-step key conflicts with a main step key", () => {
        expect(() => {
            new PromptPlan()
                .addPhase("p", "Phase")
                .step("SHARED", "Main", () => "")
                .subStep("SHARED", "Sub", () => "");
        }).toThrow('PromptPlan: duplicate step key "SHARED"');
    });
});
