// tests/audit-doc-de-mirror.test.ts
// -----------------------------------------------------------------------------
// `audit:doc-de-mirror`（L4 pre-commit + L5 gh-ci + L5 gate-chain 三挂）的**整脚本**回归面。
//
// 为什么是整脚本而不是 import 判据函数：该闸 77 行**零导出**、判据写在顶层，
// 结构上没有可 import 的符号。量具 `pnpm report:gate-regression-face` 此前把它记在
// 「零判据覆盖」里且标 `P1=FAIL(cwd)`。⛔ 闸本体一行没改。
//
// 🔴 **本文件推翻了一条被写进三处的封路理由**（2026-08-26 实测，[[INFRA-F138]]）：
//   entry 的分层、`tests/lib/gate-fixture-root.ts` 头注释、以及量具的 P1 列都表述成
//   「闸必须自己用 `import.meta.url` 取 root，`P1=FAIL(cwd)` 的走不了 fixture-root 这条路」。
//   而 `runGate` 本身就传 `cwd: root`（同一份 harness 文件 `:131`）—— 于是
//   `resolve(process.cwd(), rel)` 取路径的闸在 fixture 下读的**全是** fixture。
//   实测三向：① 正例读到 fixture 自己的读数 ② 产物落在 fixture 内 ③ 删掉输入 ⇒ ENOENT
//   且路径指向 fixture（**无 fallback 回真仓库**）。本文件是那条证伪的兑现之一。
//   ⇒ P1 的真实含义是「**拷**到别处会不会读别处」，而 cwd 类闸靠的是 harness 换 cwd，
//   两条路都通。真正的筛选条件是**闸的全部输入路径是否都随 cwd 走**（混用
//   `import.meta.url` 算 root 的同仓 import 要靠 `copyFiles` 兜，而那对 P1=PASS 的闸同样存在）。
//
// ⚠️ **本闸的绿档 stdout 不含任何 fixture 特有的数**（如实登记）：PASS 印的是
//   `✓ doc-de-mirror PASS: ${DURABLE_DOCS.length} 份…`，而 `DURABLE_DOCS` 是内联常量 ⇒
//   那个「8」在 fixture 与真仓库里**字面完全相同**，钉它证明不了非空过。
//   ⇒ 本文件的非空过凭据改用**红绿配对**：同一份 fixture 只差一个 `de-mirror-ok` 标记，
//   红档的 stderr 逐字点名 fixture 自己写的那句话（真仓库不可能有）。
//   跑错了树的话红档根本红不了（真仓库现在是绿的）⇒ 配对成立即证明两侧都读的 fixture。
//
// 覆盖：no-version / no-npm-version 两 mode + 豁免标记 + 版本正则边界 + 缺文件 fail-open
//       + 接线钉（逐条印出 + 计数 + 退出码）。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-doc-de-mirror.mjs'

// 闸的扫描面是写死的 8 条路径，fixture 必须用同样的路径名才进得了判据。
// （这些是**文档**路径，不是闸名 ⇒ 不触碰量具那条「fixture 里别写别的闸 basename」纪律。）
const GOAL = 'docs/PROJECT_GOAL.md'
const README = 'README.md'
const GETTING_STARTED = 'docs/GETTING_STARTED.md'
const AGENTS = 'AGENTS.md'
const PLUGIN = 'docs/PLUGIN.md'
const ONBOARD_HTML = 'playground/public/onboarding.html'
const ONBOARD_MD = 'docs/ONBOARDING_NEW_MACHINE.md'
const DESIGNING = 'docs/DESIGNING_WITH_TVU.md'

const PASS_MARK = '✓ doc-de-mirror PASS'
const FAIL_MARK = '❌ doc-de-mirror FAIL'

/** 干净的 8 份（都不含版本字面）—— 绿档基线。 */
function cleanDocs(): Record<string, string> {
  return {
    [GOAL]: '# 目标\n\n把设计库做成双向桥。版本线见 STATUS。\n',
    [README]: '# README\n\n安装见 GETTING_STARTED。\n',
    [GETTING_STARTED]: '# 上手\n\n先装依赖，再跑文档站。\n',
    [AGENTS]: '# AGENTS\n\n硬规则见下表。\n',
    [PLUGIN]: '# Plugin\n\n分发轨道与包版本相互独立。\n',
    [ONBOARD_HTML]: '<!doctype html>\n<h1>Onboarding</h1>\n<p>先读目标文档。</p>\n',
    [ONBOARD_MD]: '# 新机器\n\n装好 Node 与 pnpm。\n',
    [DESIGNING]: '# 用 TVU 设计\n\n组件取自已发布库。\n',
  }
}

function build(overrides: Record<string, string> = {}, opts: { only?: string[] } = {}) {
  const base = cleanDocs()
  const files = opts.only
    ? Object.fromEntries(opts.only.map((k) => [k, base[k]]))
    : base
  return createGateFixture({
    gate: GATE,
    prefix: 'de-mirror-fx',
    files: { ...files, ...overrides },
  })
}

afterEach(cleanupGateFixtures)

describe('audit:doc-de-mirror — 绿档基线', () => {
  it('8 份耐久文档均无版本字面 → exit 0', () => {
    const run = runGate(build(), GATE)
    // ⚠️ 「8 份」是闸的内联常量，不是 fixture 的读数 —— 非空过凭据在下面的红绿配对，不在这一句。
    expectGateGreen(run, { contains: [PASS_MARK, '8 份耐久文档无未标记的版本镜像'] })
  })
})

describe('audit:doc-de-mirror — 判据 (1) no-version mode', () => {
  // 🔑 红绿配对 = 本文件的非空过凭据：两侧同一份 fixture，只差行内那个豁免标记。
  const OFFENDING = '本项目当前处于 v9.7 收口阶段。'

  it('PROJECT_GOAL 出现未标记的版本字面 → 红，且逐字点名 fixture 自己写的那一行', () => {
    const run = runGate(build({ [GOAL]: `# 目标\n\n${OFFENDING}\n` }), GATE)
    expectGateRed(run, {
      marker: FAIL_MARK,
      // `docs/PROJECT_GOAL.md:3` + 这句话在真仓库里不存在 ⇒ 证明读的是 fixture
      checks: [`${GOAL}:3`, OFFENDING, '1 处'],
    })
  })

  it('同一句话只加行内 `de-mirror-ok` → 绿（致败源就是那个标记的有无，不是文件形态）', () => {
    const run = runGate(
      build({ [GOAL]: `# 目标\n\n${OFFENDING} <!-- de-mirror-ok: 历史里程碑 -->\n` }),
      GATE,
    )
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('README / AGENTS / GETTING_STARTED 也在扫描面（不是只扫第一份）', () => {
    const run = runGate(
      build({
        [README]: '# README\n\n见 v3.1 的说明。\n',
        [AGENTS]: '# AGENTS\n\nv4.2 起改用新链路。\n',
        [GETTING_STARTED]: '# 上手\n\n本文对应 v5.6。\n',
      }),
      GATE,
    )
    expectGateRed(run, {
      marker: FAIL_MARK,
      checks: [`${README}:3`, `${AGENTS}:3`, `${GETTING_STARTED}:3`, '3 处'],
    })
  })

  it('2026-08-11 扩面的三份真的在扫描面 —— 含唯一的非 .md 那份（发到公网的上手页）', () => {
    const run = runGate(
      build({
        [ONBOARD_HTML]: '<!doctype html>\n<p>当前版本 v8.4</p>\n',
        [ONBOARD_MD]: '# 新机器\n\n请安装 v8.4 对应的依赖。\n',
        [DESIGNING]: '# 用 TVU 设计\n\n本指南对应 v8.4。\n',
      }),
      GATE,
    )
    expectGateRed(run, {
      marker: FAIL_MARK,
      checks: [`${ONBOARD_HTML}:2`, `${ONBOARD_MD}:3`, `${DESIGNING}:3`, '3 处'],
    })
  })
})

describe('audit:doc-de-mirror — 判据 (2) no-npm-version mode（PLUGIN.md 是另一条版本线的真源）', () => {
  it('⛔ must-not-hit：PLUGIN 写自己的 plugin 版本、同一行无 npm 语境 → 放行', () => {
    const run = runGate(build({ [PLUGIN]: '# Plugin\n\nplugin v2.1 起支持新入口。\n' }), GATE)
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('同一行出现「npm 包」+ 版本字面 → 红（镜像了 npm 那条版本线）', () => {
    const run = runGate(build({ [PLUGIN]: '# Plugin\n\n配套 npm 包 v6.3 一起发。\n' }), GATE)
    expectGateRed(run, { marker: FAIL_MARK, checks: [`${PLUGIN}:3`, '配套 npm 包 v6.3 一起发'] })
  })

  it('npm 语境的另一个分支（安装命令）同样命中 → 红（不是只认「npm 包」一个词）', () => {
    const run = runGate(
      build({ [PLUGIN]: '# Plugin\n\n先 pnpm add @ux-team/tvu-design-system 装到 v6.4。\n' }),
      GATE,
    )
    expectGateRed(run, { marker: FAIL_MARK, checks: [`${PLUGIN}:3`] })
  })

  it('⛔ must-not-hit：npm 语境词与版本字面**不在同一行** → 放行（判据是行内共现）', () => {
    const run = runGate(
      build({ [PLUGIN]: '# Plugin\n\n本页讲 npm 包的接入。\n\nplugin v2.2 是本轨道的版本。\n' }),
      GATE,
    )
    expectGateGreen(run, { contains: [PASS_MARK] })
  })
})

describe('audit:doc-de-mirror — 版本正则的边界', () => {
  it('⛔ must-not-hit：`v1` 无小数点 → 不算版本字面', () => {
    const run = runGate(build({ [GOAL]: '# 目标\n\n代号 v1 的那一代已退役。\n' }), GATE)
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('⛔ must-not-hit：`\\b` 词边界 —— 紧贴在字母数字后面的 `v1.0` 不命中', () => {
    const run = runGate(build({ [GOAL]: '# 目标\n\n构建号 rev1.0 与版本线无关。\n' }), GATE)
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('大写 `V2.0` 照样命中（正则带 i flag，不是只认小写）', () => {
    const run = runGate(build({ [GOAL]: '# 目标\n\n参见 V2.0 的说明。\n' }), GATE)
    expectGateRed(run, { marker: FAIL_MARK, checks: [`${GOAL}:3`, 'V2.0'] })
  })

  it('三段式 `v0.10.1` 命中（`\\d+\\.\\d+` 前缀即可，不要求恰好两段）', () => {
    const run = runGate(build({ [GOAL]: '# 目标\n\n那次修复在 v0.10.1。\n' }), GATE)
    expectGateRed(run, { marker: FAIL_MARK, checks: [`${GOAL}:3`, 'v0.10.1'] })
  })

  it('如实登记的宽松边界：裸写 `de-mirror-ok` 也放行（判据不要求注释语法）', () => {
    const run = runGate(build({ [GOAL]: '# 目标\n\n见 v7.7 —— de-mirror-ok\n' }), GATE)
    expectGateGreen(run, { contains: [PASS_MARK] })
  })
})

describe('audit:doc-de-mirror — 接线钉（判据 → 计数 → 退出码 → 逐条输出）', () => {
  it('一份耐久文档都不存在 → exit 0 且照常印 PASS（`existsSync` 跳过，⛔ 不崩溃、不静默红）', () => {
    const root = createGateFixture({ gate: GATE, prefix: 'de-mirror-fx-empty' })
    const run = runGate(root, GATE)
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('同一份文档内多行违规 → 逐行各记一条，计数不去重到文件级', () => {
    const run = runGate(
      build({ [GOAL]: '# 目标\n\n见 v1.1。\n中间一行没有版本。\n又见 v1.2。\n' }),
      GATE,
    )
    expectGateRed(run, {
      marker: FAIL_MARK,
      checks: [`${GOAL}:3`, `${GOAL}:5`, '2 处'],
    })
  })

  it('跨多份文档同时违规 → violations 全部逐条印出、计数正确（接线不吞 findings）', () => {
    const run = runGate(
      build({
        [GOAL]: '# 目标\n\n见 v1.1。\n',
        [README]: '# README\n\n见 v1.2。\n',
        [PLUGIN]: '# Plugin\n\nnpm 包 v1.3 同步发。\n',
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('3 处')
    expect(run.stderr).toContain(`${GOAL}:3`)
    expect(run.stderr).toContain(`${README}:3`)
    expect(run.stderr).toContain(`${PLUGIN}:3`)
  })

  it('违规走 stderr、PASS 走 stdout（挂 CI 时两条流的用途不同）', () => {
    const red = runGate(build({ [GOAL]: '# 目标\n\n见 v1.1。\n' }), GATE)
    expect(red.stderr).toContain(FAIL_MARK)
    expect(red.stdout).not.toContain(PASS_MARK)

    const green = runGate(build(), GATE)
    expect(green.stdout).toContain(PASS_MARK)
  })
})
