import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ArchitectAgent } from "../agents/architect.js";
import { parsePendingHooksMarkdown } from "../utils/story-markdown.js";
const ZERO_USAGE = {
    promptTokens: 0,
    completionTokens: 0,
    totalTokens: 0,
};
function buildAgent() {
    return new ArchitectAgent({
        client: {
            provider: "openai",
            apiFormat: "chat",
            stream: false,
            defaults: {
                temperature: 0.7,
                maxTokens: 4096,
                thinkingBudget: 0,
                maxTokensCap: null,
                extra: {},
            },
        },
        model: "test-model",
        projectRoot: process.cwd(),
    });
}
function baseBook() {
    return {
        id: "phase7-book",
        title: "Phase7测试书",
        platform: "other",
        genre: "urban",
        status: "active",
        targetChapters: 80,
        chapterWordCount: 2200,
        language: "zh",
        createdAt: "2026-04-15T00:00:00.000Z",
        updatedAt: "2026-04-15T00:00:00.000Z",
    };
}
const PHASE7_RESPONSE = [
    "=== SECTION: story_frame ===",
    "## 主题与基调",
    "这本书讲的是记忆与承诺。",
    "=== SECTION: volume_map ===",
    "## 各卷主题与情绪曲线",
    "三卷结构，第1卷 1-20 章。",
    "=== SECTION: rhythm_principles ===",
    "## 原则 1",
    "每 10 章一个高潮。",
    "=== SECTION: roles ===",
    "---ROLE---",
    "tier: major",
    "name: 林辞",
    "---CONTENT---",
    "## 核心标签",
    "沉默",
    "## 反差细节",
    "会给流浪狗留罐头",
    "## 人物小传",
    "过往。",
    "## 当前现状",
    "码头边做账房。",
    "## 关系网络",
    "与沈默是旧友。",
    "## 内在驱动",
    "查清真相。",
    "## 成长弧光",
    "从独行到托付。",
    "=== SECTION: book_rules ===",
    "---",
    "version: \"1.0\"",
    "---",
    "## 叙事视角",
    "第三人称。",
    "=== SECTION: current_state ===",
    "| 字段 | 值 |",
    "| --- | --- |",
    "| 当前章节 | 0 |",
    "",
    "=== SECTION: pending_hooks ===",
    "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 上游依赖 | 回收卷 | 核心 | 半衰期 | 备注 |",
    "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
    "| H01 | 1 | 主线 | 未开启 | 0 | 终章揭晓 | 终局 | 无 | 第3卷终章前 | 是 | 80 | 父亲笔记本 |",
    "| H02 | 3 | 谜团 | 未开启 | 0 | 第2卷揭开 | 中程 | [H01] | 第2卷中段 | 否 | 30 | 码头名单碎片 |",
    "| H03 | 7 | 小承诺 | 未开启 | 0 | 15章 | 近期 | 无 | 第1卷末 | 否 |  | 对妹妹的承诺 |",
].join("\n");
describe("ArchitectAgent — Phase 7 extended hook frontmatter", () => {
    let bookDir;
    beforeEach(async () => {
        bookDir = await mkdtemp(join(tmpdir(), "inkos-phase7-arch-"));
    });
    afterEach(async () => {
        await rm(bookDir, { recursive: true, force: true });
        vi.restoreAllMocks();
    });
    it("architect prompt instructs depends_on / pays_off_in_arc / core_hook / half_life", async () => {
        const agent = buildAgent();
        const chat = vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: PHASE7_RESPONSE, usage: ZERO_USAGE });
        await agent.generateFoundation(baseBook());
        const messages = chat.mock.calls[0]?.[0];
        const system = messages[0]?.content ?? "";
        // The zh prompt must document all four new columns with clear rules.
        expect(system).toContain("上游依赖");
        expect(system).toContain("回收卷");
        expect(system).toContain("核心");
        expect(system).toContain("半衰期");
        // Core-hook budget guidance: 3-7 per book.
        expect(system).toContain("3-7 条");
        // The extended table header must appear in the prompt example.
        expect(system).toContain("| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 上游依赖 | 回收卷 | 核心 | 半衰期 | 备注 |");
    });
    it("round-trips extended columns through parseSections into the ledger", async () => {
        const agent = buildAgent();
        vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: PHASE7_RESPONSE, usage: ZERO_USAGE });
        const result = await agent.generateFoundation(baseBook());
        // Phase 7 hotfix 1: rendered ledger now includes a 12th column `半衰期`
        // (half_life) so architect-supplied values persist through the projection
        // roundtrip and are read by hook-stale-detection. Hooks without an explicit
        // half_life render an empty cell (parser falls back to timing default).
        // Hotfix 2 adds a 13th `升级` (promoted) column — architect computes it
        // from core_hook / depends_on / cross_volume at seed time. H01 (core=是)
        // and H02 (depends_on=[H01]) both get promoted=是; H03 has no rule firing.
        expect(result.pendingHooks).toContain("| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 上游依赖 | 回收卷 | 核心 | 半衰期 | 升级 | 备注 |");
        expect(result.pendingHooks).toContain("| H01 | 1 | 主线 | 未开启 | 0 | 终章揭晓 | 终局 | 无 | 第3卷终章前 | 是 | 80 | 是 | 父亲笔记本 |");
        expect(result.pendingHooks).toContain("| H02 | 3 | 谜团 | 未开启 | 0 | 第2卷揭开 | 中程 | [H01] | 第2卷中段 | 否 | 30 | 是 | 码头名单碎片 |");
        // H03 omits half_life; cell renders empty. No rule fires so 升级=否.
        expect(result.pendingHooks).toContain("| H03 | 7 | 小承诺 | 未开启 | 0 | 15章 | 近期 | 无 | 第1卷末 | 否 |  | 否 | 对妹妹的承诺 |");
    });
    it("pending_hooks.md on disk carries the Phase 7 columns", async () => {
        const agent = buildAgent();
        vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: PHASE7_RESPONSE, usage: ZERO_USAGE });
        const result = await agent.generateFoundation(baseBook());
        await agent.writeFoundationFiles(bookDir, result, false, "zh");
        const disk = await readFile(join(bookDir, "story", "pending_hooks.md"), "utf-8");
        expect(disk).toContain("上游依赖");
        expect(disk).toContain("回收卷");
        expect(disk).toContain("核心");
        // Second row's depends_on column should be [H01].
        expect(disk).toMatch(/\| H02 \|.*\| \[H01\] \|/);
    });
    it("parsePendingHooksMarkdown reads the extended ledger shape", async () => {
        const agent = buildAgent();
        vi.spyOn(agent, "chat")
            .mockResolvedValue({ content: PHASE7_RESPONSE, usage: ZERO_USAGE });
        const result = await agent.generateFoundation(baseBook());
        const hooks = parsePendingHooksMarkdown(result.pendingHooks);
        expect(hooks).toHaveLength(3);
        const h01 = hooks.find((h) => h.hookId === "H01");
        expect(h01.coreHook).toBe(true);
        expect(h01.paysOffInArc).toBe("第3卷终章前");
        expect(h01.dependsOn ?? []).toEqual([]);
        const h02 = hooks.find((h) => h.hookId === "H02");
        expect(h02.coreHook).toBe(false);
        expect(h02.dependsOn).toEqual(["H01"]);
        expect(h02.paysOffInArc).toBe("第2卷中段");
        // Phase 7 hotfix 1: half_life survives the roundtrip.
        expect(h01.halfLifeChapters).toBe(80);
        expect(h02.halfLifeChapters).toBe(30);
        // H03 omitted half_life — falls back to undefined, not a default.
        const h03 = hooks.find((h) => h.hookId === "H03");
        expect(h03.halfLifeChapters).toBeUndefined();
    });
    it("legacy 8-column pending_hooks tables still parse without new fields (backward compat)", () => {
        const legacy = [
            "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 备注 |",
            "| --- | --- | --- | --- | --- | --- | --- | --- |",
            "| L01 | 1 | 主线 | 未开启 | 0 | 15章 | 中程 | 旧书 |",
        ].join("\n");
        const hooks = parsePendingHooksMarkdown(legacy);
        expect(hooks).toHaveLength(1);
        const hook = hooks[0];
        expect(hook.hookId).toBe("L01");
        expect(hook.coreHook).toBeUndefined();
        expect(hook.dependsOn).toBeUndefined();
        expect(hook.paysOffInArc).toBeUndefined();
        expect(hook.halfLifeChapters).toBeUndefined();
    });
});
//# sourceMappingURL=architect-phase7.test.js.map