import { test } from 'node:test'
import assert from 'node:assert/strict'
import path from 'node:path'
import { promises as fs } from 'node:fs'
import { stageChapter } from '../../src/staging/index.js'
import { prepareChapterMaterials } from '../../src/prep/index.js'
import { assembleReviewInput } from '../../src/review/index.js'
import { mechanicalCheck } from '../../src/mechanical-check/index.js'
import { repoCtx } from '../commands/_helper.js'

// —— AC2 批内依赖：第 3 章预登记（新条目/新角色/时间线/信息差）→ 第 4 章备料/审稿输入/机检可见 ——

const 定稿章 = (num) =>
  `---\n章号: ${num}\n标题: 第${num}章\n卷: 1\n视角: 林晚\n书内时间: 春月初${num}\n字数: 100\n章定位: 推进\n钩子: 危机钩-强\n情绪定位: 铺垫\n---\n\n林晚在第${num}章遇到了新的麻烦，她收剑而立。`

const 角色卡 = `---\n姓名: 林晚\n别名:\n  - 晚晚\n状态: 在世\n位置: 青云宗\n境界: 练气三层\n最后变更章: 1\n---\n## 设定\n外门弟子。\n\n## 典型对话\n"本姑娘才不怕！"\n\n## 关系\n无。\n`

const bookFiles = () => ({
  'book.yaml':
    'spec_version: "7.0"\n书名: 叠加测试\n类型: 玄幻\n每章目标字数: 3000\n卷规模: 40\n连写批次大小: 8\n',
  '定稿/正文/0001-第1章.md': 定稿章(1),
  '定稿/正文/0002-第2章.md': 定稿章(2),
  '定稿/设定/名册.md': '| 正名 | 别名 | 类型 | 首现章 |\n|--|--|--|--|\n| 林晚 | 晚晚 | character | 1 |\n',
  '定稿/设定/角色/林晚.md': 角色卡,
  '定稿/设定/时间线/第01卷.md':
    '| 章 | 书内时间 | 一句话事件 | 在场 |\n|----|----|----|----|\n| 1 | 春月初一 | 遇袭 | 林晚 |\n| 2 | 春月初二 | 追查 | 林晚 |\n',
  '大纲/卷纲/第01卷.md': '# 第01卷\n追查古钟之谜。\n',
  '大纲/伏笔/伏笔-001-旧案.md': '---\n强度: 高\n状态: 进行\n开启章: 1\n最后推进章: 2\n---\n## 描述\n旧案。\n\n## 履历\n- 第1章：埋下\n',
})

async function stageChapter3(ctx) {
  await fs.mkdir(path.join(ctx.repoPath, '工作区'), { recursive: true })
  await fs.writeFile(
    path.join(ctx.repoPath, '工作区', '审稿.md'),
    '# 第 3 章审稿单\n\n> 完整两审模式。\n> 共 0 个问题：0 阻断。\n',
    'utf8'
  )
  const payload = {
    frontMatter: {
      章号: 3,
      标题: '闻钟',
      卷: 1,
      视角: '林晚',
      书内时间: '春月初三',
      字数: 120,
      章定位: '推进',
      钩子: '悬念钩-强',
      情绪定位: '铺垫',
      伏笔: ['埋下 伏笔-009', '推进 伏笔-001'],
    },
    body: '林晚夜里听见后山钟声，古钟长老现身拦路。她记下了钟声的方位，决定天亮再探。',
    summary: '林晚闻钟遇古钟长老。',
    threadCreates: [
      {
        id: '伏笔-009',
        短题: '古钟',
        frontMatter: { 强度: '中', 状态: '进行', 开启章: 3, 最后推进章: 3 },
        body: '## 描述\n古钟的来历。\n\n## 履历\n- 第3章：埋下\n',
      },
    ],
    threadUpdates: [{ id: '伏笔-001', updates: { 最后推进章: 3 }, history: '第3章：推进' }],
    rosterUpserts: [{ 正名: '古钟长老', 别名: '钟叟', 类型: 'character', 首现章: 3 }],
    characterUpdates: [{ name: '林晚', updates: { 最后变更章: 3, 境界: '练气四层' } }],
    timelineRows: [
      { volumeNum: 1, row: { 章: 3, 书内时间: '春月初三', 一句话事件: '闻钟遇长老', 在场: '林晚' } },
    ],
    secretWrites: [
      {
        id: '信息差-002-钟声',
        frontMatter: { 读者已知: false, 登记章: 3, 短题: '钟声有古怪', 知情人: ['古钟长老'], 关键词: ['古钟'] },
        content: '## 内容\n钟声是封印松动的征兆。\n',
      },
    ],
    commitLines: {},
    workspaceFiles: [],
  }
  const r = await stageChapter(ctx, { chapterNum: 3, payload })
  assert.equal(r.ok, true, r.error)
}

test('AC2 备料叠加：第 4 章材料含批内近况/结尾/时间线/信息差', async () => {
  const { ctx, cleanup } = await repoCtx(null, bookFiles())
  try {
    await stageChapter3(ctx)
    const r = await prepareChapterMaterials(ctx, { chapterNum: 4 })
    assert.equal(r.ok, true, r.error)
    assert.match(r.content, /批内已暂存：第 3-3 章共 1 章/)
    assert.match(r.content, /### 第3章结尾（批内暂存）\n[\s\S]*天亮再探/)
    assert.match(r.content, /### 第2章结尾/) // 不足两章回定稿补
    assert.match(r.content, /- 3 闻钟遇长老（批内预登记）/)
    assert.match(r.content, /- 信息差-002-钟声（批内预登记）：知情人=古钟长老；关键词=古钟；内容：钟声是封印松动的征兆。/)
  } finally {
    await cleanup()
  }
})

test('AC2 审稿输入叠加：第 4 章相关条目/名册/角色/时间线/信息差看到第 3 章预登记', async () => {
  const { ctx, cleanup } = await repoCtx(null, bookFiles())
  try {
    await stageChapter3(ctx)
    const draftPath = path.join(ctx.repoPath, '工作区', '草稿-A.md')
    await fs.writeFile(
      draftPath,
      '---\n章号: 4\n标题: 探钟\n卷: 1\n字数: 60\n章定位: 推进\n钩子: 危机钩-强\n情绪定位: 铺垫\n伏笔:\n  - 推进 伏笔-009\n---\n林晚天亮后去探古钟，山道上又见古钟长老。',
      'utf8'
    )
    const r = await assembleReviewInput(ctx, { chapterNum: 4, draftPath })
    assert.equal(r.ok, true, r.error)
    const t = r.input.相关条目.find((x) => x.id === '伏笔-009')
    assert.ok(t, JSON.stringify(r.input.相关条目))
    assert.equal(t.批内预登记, true)
    assert.equal(t.开启章, 3)
    const old = r.input.相关条目.find((x) => x.id === '伏笔-001')
    assert.equal(old.最后推进章, 3, '批内推进要刷进相关条目')
    assert.ok(r.input.名册.some((x) => x.正名 === '古钟长老' && x.别名.includes('钟叟')))
    const 林晚 = r.input.相关角色.find((x) => x.正名 === '林晚')
    assert.ok(林晚, '草稿提到林晚应命中角色')
    assert.equal(林晚.境界, '练气四层', '批内角色变更要叠加')
    assert.ok(r.input.时间线片段.some((x) => String(x.事件).includes('批内预登记')))
    assert.ok(r.input.信息差候选.some((x) => x.id === '信息差-002-钟声'))
    assert.match(r.input.全书近况, /批内已暂存/)
  } finally {
    await cleanup()
  }
})

test('重审受影响章不倒灌：第 3 章自己的审稿输入看不到第 3 章预登记', async () => {
  const { ctx, cleanup } = await repoCtx(null, bookFiles())
  try {
    await stageChapter3(ctx)
    const draftPath = path.join(ctx.repoPath, '工作区', '草稿-A.md')
    await fs.writeFile(
      draftPath,
      '---\n章号: 3\n标题: 闻钟\n卷: 1\n字数: 40\n章定位: 推进\n钩子: 悬念钩-强\n情绪定位: 铺垫\n---\n林晚夜里听见后山钟声。',
      'utf8'
    )
    const r = await assembleReviewInput(ctx, { chapterNum: 3, draftPath })
    assert.equal(r.ok, true, r.error)
    assert.ok(!r.input.相关条目.some((x) => x.id === '伏笔-009'))
    assert.ok(!r.input.名册.some((x) => x.正名 === '古钟长老'))
    assert.doesNotMatch(r.input.全书近况, /批内已暂存/)
  } finally {
    await cleanup()
  }
})

test('AC2 机检叠加：推进批内新条目零误报、批内新角色不报新专名、批内信息差出候选', async () => {
  const { ctx, cleanup } = await repoCtx(null, bookFiles())
  try {
    await stageChapter3(ctx)
    const draftPath = path.join(ctx.repoPath, '工作区', '草稿-B.md')
    await fs.writeFile(
      draftPath,
      '---\n章号: 4\n标题: 探钟\n卷: 1\n字数: 3000\n章定位: 推进\n钩子: 危机钩-强\n情绪定位: 铺垫\n伏笔:\n  - 推进 伏笔-009\n---\n' +
        '古钟长老道：“你不该来。”林晚握紧短刀盯着古钟出神，山风吹动她的衣角。'.repeat(1) +
        '她沿着山道慢慢往上走，一步一步踩着碎石，心里盘算着夜里听到的那阵钟声到底从哪里来。'.repeat(1),
      'utf8'
    )
    const r = await mechanicalCheck(ctx, { chapterNum: 4, draftPath })
    assert.equal(r.ok, true, r.error)
    assert.deepEqual(
      r.issues.filter((i) => i.check === '条目变动'),
      [],
      JSON.stringify(r.issues)
    )
    assert.ok(!r.candidates.some((c) => c.type === '新专名' && c.value === '古钟长老'), JSON.stringify(r.candidates))
    assert.ok(r.candidates.some((c) => c.type === '信息差候选' && c.value === '信息差-002-钟声'), JSON.stringify(r.candidates))
  } finally {
    await cleanup()
  }
})

test('AC7 缓存不变量：批次进行中删缓存全量重建，备料输出不变', async () => {
  const { ctx, cleanup } = await repoCtx(null, bookFiles())
  try {
    await stageChapter3(ctx)
    const before = await prepareChapterMaterials(ctx, { chapterNum: 4 })
    assert.equal(before.ok, true, before.error)
    const rb = await ctx.cache.rebuildFromSource(ctx.repoPath)
    assert.equal(rb.ok, true, rb.errors?.join('；'))
    const after = await prepareChapterMaterials(ctx, { chapterNum: 4 })
    assert.equal(after.ok, true, after.error)
    assert.equal(after.content, before.content)
  } finally {
    await cleanup()
  }
})
