import { test } from 'node:test'
import assert from 'node:assert/strict'
import path from 'node:path'
import os from 'node:os'
import { access, mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
import { CacheManager } from '../../src/cache/index.js'

// 在临时目录构造一个最小书仓库，files 是 { 相对路径: 内容 }
async function makeRepo(files) {
  const root = await mkdtemp(path.join(os.tmpdir(), 'wnw-rebuild-'))
  for (const [rel, content] of Object.entries(files)) {
    const full = path.join(root, rel)
    await mkdir(path.dirname(full), { recursive: true })
    await writeFile(full, content, 'utf8')
  }
  return root
}

const 名册仅林晚 =
  '| 正名 | 别名 | 类型 | 首现章 |\n|------|------|------|--------|\n| 林晚 | 晚晚 | character | 1 |\n'

test('角色卡不在名册里也必须入 entities（upsert，不丢数据）', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/设定/名册.md': 名册仅林晚,
    '定稿/设定/角色/独行客.md':
      '---\n姓名: 独行客\n状态: 在世\n位置: 荒原\n境界: 元婴\n最后变更章: 5\n---\n## 设定\n来历不明。\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    await cache.ensureReady(root)
    const rows = await cache.query('SELECT * FROM entities WHERE id = ?', ['独行客'])
    assert.equal(rows.length, 1, '独行客有角色卡但不在名册，必须 upsert 入 entities，不能丢')
    assert.equal(rows[0].status, '在世')
    assert.equal(rows[0].realm, '元婴')
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('名册中的角色被角色卡补全字段（upsert 走 UPDATE 分支）', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/设定/名册.md': 名册仅林晚,
    '定稿/设定/角色/林晚.md':
      '---\n姓名: 林晚\n状态: 在世\n位置: 青云宗\n境界: 练气三层\n最后变更章: 1\n---\n## 设定\n外门弟子。\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    await cache.ensureReady(root)
    const rows = await cache.query('SELECT * FROM entities WHERE id = ?', ['林晚'])
    assert.equal(rows.length, 1, '林晚不应因 upsert 而重复')
    assert.equal(rows[0].realm, '练气三层', '角色卡的境界应补进名册建立的行')
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('履历引用不存在的章节 → 记 warning，不阻断重建（AC10）', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/设定/名册.md': 名册仅林晚,
    '定稿/正文/0001-开局.md': '---\n章号: 1\n标题: 开局\n卷: 1\n字数: 100\n章定位: 推进\n---\n正文。',
    '大纲/伏笔/伏笔-001-test.md':
      '---\n强度: 高\n状态: 进行\n开启章: 1\n---\n## 履历\n- 第999章：推进——引用不存在的章\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    const result = await cache.rebuildFromSource(root)
    assert.equal(result.ok, true, '履历章节不存在只警告，不阻断')
    assert.ok(
      result.warnings.some((w) => w.includes('999')),
      `应有引用 999 章的 warning，实际：${JSON.stringify(result.warnings)}`
    )
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('别名冲突 → 报 error 并拒绝重建（AC10）', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    // 「影」同时是林晚和神秘老者的别名 → 冲突
    '定稿/设定/名册.md':
      '| 正名 | 别名 | 类型 | 首现章 |\n|------|------|------|--------|\n' +
      '| 林晚 | 影 | character | 1 |\n| 神秘老者 | 影 | character | 1 |\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    const result = await cache.rebuildFromSource(root)
    assert.equal(result.ok, false, '别名冲突必须拒绝重建')
    assert.ok(
      result.errors.some((e) => e.includes('影')),
      `应有别名冲突 error，实际：${JSON.stringify(result.errors)}`
    )
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

// —— 批 2 回归：R5 / R6 / A13 ——

test('R5：名册中文类型入库归一成英文机器值（角色→character、地点→location）', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/设定/名册.md':
      '| 正名 | 别名 | 类型 | 首现章 |\n|---|---|---|---|\n| 江遥 | 小江 | 角色 | 1 |\n| 滨海市 |  | 地点 | 1 |\n| 旧英文 |  | character | 1 |\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    await cache.ensureReady(root)
    const typeOf = async (id) =>
      (await cache.query('SELECT type FROM entities WHERE id = ?', [id]))[0]?.type
    assert.equal(await typeOf('江遥'), 'character')
    assert.equal(await typeOf('滨海市'), 'location')
    assert.equal(await typeOf('旧英文'), 'character', '英文旧值恒等收纳，存量书无损')
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('R6：全角逗号/顿号分隔的别名全部入 entity_aliases 可解析', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/设定/名册.md':
      '| 正名 | 别名 | 类型 | 首现章 |\n|---|---|---|---|\n| 林晚 | 阿晚，晚儿、小晚 | 角色 | 1 |\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    await cache.ensureReady(root)
    const aliases = (await cache.query('SELECT alias FROM entity_aliases ORDER BY alias', []))
      .map((r) => r.alias)
    assert.deepEqual(aliases, ['小晚', '晚儿', '阿晚'].sort(), `全角分隔的三个别名都应入库：${aliases}`)
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('A13：名册缺「别名」列 → 实体照常入库，不炸整次重建', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/正文/0001-开局.md': '---\n章号: 1\n标题: 开局\n卷: 1\n字数: 100\n章定位: 推进\n---\n正文。',
    '定稿/设定/名册.md': '| 正名 | 类型 | 首现章 |\n|---|---|---|\n| 林晚 | 角色 | 1 |\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    const result = await cache.rebuildFromSource(root)
    assert.equal(result.ok, true, `缺别名列不应回滚重建：${JSON.stringify(result.errors)}`)
    assert.equal((await cache.query('SELECT COUNT(*) AS n FROM entities', []))[0].n, 1)
    assert.equal((await cache.query('SELECT COUNT(*) AS n FROM chapters', []))[0].n, 1, '章不应被连坐')
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('别名冲突 → ROLLBACK 不留半库（P1-1：已写 chapters 也回滚）', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/正文/0001-开局.md': '---\n章号: 1\n标题: 开局\n卷: 1\n字数: 100\n章定位: 推进\n---\n正文。',
    '定稿/正文/0002-承接.md': '---\n章号: 2\n标题: 承接\n卷: 1\n字数: 100\n章定位: 推进\n---\n正文。',
    // 别名冲突：scanEntities 在 chapters/threads/secrets 已 INSERT 后才返回失败
    '定稿/设定/名册.md':
      '| 正名 | 别名 | 类型 | 首现章 |\n|------|------|------|--------|\n' +
      '| 林晚 | 影 | character | 1 |\n| 老者 | 影 | character | 1 |\n',
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    const result = await cache.rebuildFromSource(root)
    assert.equal(result.ok, false, '别名冲突应拒绝重建')
    // 事务回滚且失败临时库不应被提升为可用缓存
    await assert.rejects(() => cache.query('SELECT COUNT(*) AS c FROM chapters'))
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('重建失败保留上一份可用缓存，不替换成空库', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/正文/0001-开局.md': '---\n章号: 1\n标题: 开局\n卷: 1\n字数: 100\n章定位: 推进\n---\n正文。',
    '定稿/设定/名册.md': 名册仅林晚,
  })
  const cache = new CacheManager(path.join(root, '.cache', 'index.db'))
  try {
    await cache.ensureReady(root)
    const before = await cache.query('SELECT COUNT(*) AS c FROM chapters')
    assert.equal(before[0].c, 1)

    await writeFile(
      path.join(root, '定稿/设定/名册.md'),
      '| 正名 | 别名 | 类型 | 首现章 |\n|------|------|------|--------|\n' +
        '| 林晚 | 影 | character | 1 |\n| 老者 | 影 | character | 1 |\n',
      'utf8'
    )
    const result = await cache.rebuildFromSource(root)
    assert.equal(result.ok, false, '坏源文件应让本次重建失败')
    const after = await cache.query('SELECT COUNT(*) AS c FROM chapters')
    assert.equal(after[0].c, 1, '失败重建不应把旧缓存替换成空库')
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})

test('重建失败可选择移除旧缓存，避免继续读取陈旧数据', async () => {
  const root = await makeRepo({
    'book.yaml': 'spec_version: "7.0"\n书名: 测试\n',
    '定稿/正文/0001-开局.md': '---\n章号: 1\n标题: 开局\n卷: 1\n字数: 100\n章定位: 推进\n---\n正文。',
    '定稿/设定/名册.md': 名册仅林晚,
  })
  const dbPath = path.join(root, '.cache', 'index.db')
  const cache = new CacheManager(dbPath)
  try {
    await cache.ensureReady(root)
    await writeFile(
      path.join(root, '定稿/设定/名册.md'),
      '| 正名 | 别名 | 类型 | 首现章 |\n|------|------|------|--------|\n' +
        '| 林晚 | 影 | character | 1 |\n| 老者 | 影 | character | 1 |\n',
      'utf8'
    )
    const result = await cache.rebuildFromSource(root, { keepExistingOnFailure: false })
    assert.equal(result.ok, false)
    await assert.rejects(() => cache.query('SELECT COUNT(*) AS c FROM chapters'))
    await assert.rejects(() => access(dbPath))
  } finally {
    await cache.close()
    await rm(root, { recursive: true, force: true })
  }
})
