// [[INFRA-F138]] —— `scripts/audit-report-scope-declaration.mjs` 的**整脚本**回归面。
// -----------------------------------------------------------------------------
// 本闸**只有这一个**测试面（它是 2026-09-15 新建的闸，⛔ 没有旧的「按符号 import」那一份）
// ⇒ 这里既覆盖判据逻辑（导出的两个纯函数），也覆盖 `main()` 的整条接线
//   （读目录 → 判据 → 输出 → 退出码），**被测闸一行不改**。
//
// 为什么这条闸值得一个整脚本面（爆炸半径）：
//   它是 §M-DISCIPLINE.SCOPE 验收项 1b 的**唯一**执行点。假绿 ⇒ 新写的审计报告不声明扫描面
//   ⇒ 下一个读它的人（含 AI）拿它的结论去做决策时**不知道它扫了多少**，而本仓已经为
//   「把部分覆盖读成全覆盖」付过多次代价（规则真源那段自己就登记了两个被推翻的旧读数）。
//
// ⛔ **本面覆盖不到什么（如实登记的边界，不是待补 TODO）**：
//   ① ⛔ 覆盖不到「那句声明写得**对不对**」—— 闸本身就只判标签在不在（见闸头注释边界 ①）。
//      一份写 `> **扫描面**：（待补）` 的报告在闸下是绿的，本面 D 组**钉住**这个现行行为，
//      ⛔ 那是登记、不是背书。
//   ② ⛔ 覆盖不到**真实 `docs/internal/_reports/` 目录的内容** —— 全部用 fixture。
//      真目录的合规由 pre-commit 每次 staged 命中时现跑。
//   ③ ⛔ 覆盖不到 pre-commit 的**触发条件**本身（那是 hook 的 `grep -qE`，不在本闸射程内）。
//
// ⛔ 污染面纪律（[[INFRA-F138]] 逐字）：fixture 假名统一用 `nrix`；散文与断言里提到别的闸
//   一律去 `.mjs` 后缀、去 `audit:` 冒号，否则量具会把那条闸误报成「已覆盖」。
//   本闸 stdout 只引用它自己 ⇒ 不需要「断言截短」那一招。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterAll } from 'vitest'
import { writeFileSync, mkdirSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'
import { firstBlockquote, declaresScope, SCOPE_LABEL_RE } from '../scripts/audit-report-scope-declaration.mjs'

const GATE = 'scripts/audit-report-scope-declaration.mjs'
const REPORTS = 'docs/internal/_reports'

const GOOD = '> **扫描面**：nrix 的 3 份样本（逐份实读）\n'
const TITLE = '# nrix 样本报告\n'

function fixture(files: Record<string, string> | null): string {
  const root = createGateFixture({ gate: GATE, prefix: 'nrix-scope' })
  if (files === null) {
    // 目录本身不建 ⇒ 测 fail-closed 的第一档
    return root
  }
  mkdirSync(join(root, REPORTS), { recursive: true })
  for (const [name, body] of Object.entries(files)) {
    writeFileSync(join(root, REPORTS, name), body)
  }
  return root
}

afterAll(() => cleanupGateFixtures())

describe('A 组 · 纯函数判据', () => {
  it('firstBlockquote 跳过标题与空行，只取第一段连续 `>`', () => {
    const src = `${TITLE}\n> 第一行\n> 第二行\n\n正文\n\n> 第二个块\n`
    const b = firstBlockquote(src)
    expect(b).toContain('第一行')
    expect(b).toContain('第二行')
    expect(b).not.toContain('第二个块')
  })

  it('标题之后不是 `>` ⇒ 返回 null（⛔ 不是空串）', () => {
    expect(firstBlockquote(`${TITLE}\n正文直接开始\n`)).toBeNull()
  })

  it('全角冒号与半角冒号都认', () => {
    expect(SCOPE_LABEL_RE.test('**扫描面**：x')).toBe(true)
    expect(SCOPE_LABEL_RE.test('**扫描面**: x')).toBe(true)
  })

  it('标签在【第二个】块里 ⇒ 判违例（射程就是首个块）', () => {
    const src = `${TITLE}\n> 别的话\n\n正文\n\n> **扫描面**：晚了\n`
    expect(declaresScope(src).ok).toBe(false)
    expect(declaresScope(src).reason).toBe('no-label')
  })

  it('没有 `>` 块与有块但没标签 ⇒ 两个不同的 reason（⛔ 不许并成一个）', () => {
    expect(declaresScope(`${TITLE}\n正文\n`).reason).toBe('no-blockquote')
    expect(declaresScope(`${TITLE}\n> 别的话\n`).reason).toBe('no-label')
  })
})

describe('B 组 · 整脚本绿档', () => {
  it('全部声明了扫描面 ⇒ EXIT=0，且自印的份数是 fixture 自己的', () => {
    const root = fixture({ 'a.md': TITLE + GOOD, 'b.md': TITLE + GOOD, 'c.md': TITLE + GOOD })
    const run = runGate(root, GATE)
    expectGateGreen(run, { contains: ['3 份报告全部声明了扫描面'] })
    // 绿档必须自带那句「⛔ 别读成守住了」—— 它是这条闸的部分覆盖登记，⛔ 不许悄悄删
    expect(run.stdout).toContain('只覆盖 1b 一条')
  })
})

describe('C 组 · 整脚本红档（每条违例都点名到文件）', () => {
  it('一份缺标签 ⇒ EXIT=1 且点名那一份', () => {
    const root = fixture({ 'ok.md': TITLE + GOOD, 'bad.md': `${TITLE}> 只说了缘起\n` })
    const run = runGate(root, GATE)
    expectGateRed(run, { checks: [`${REPORTS}/bad.md`, '没有 `**扫描面**：` 标签'] })
    expect(run.stderr).not.toContain(`${REPORTS}/ok.md`)
  })

  it('一份完全没有 `>` 块 ⇒ 报的是另一条理由（⛔ 两档不得合并）', () => {
    const root = fixture({ 'bare.md': `${TITLE}正文直接开始\n` })
    const run = runGate(root, GATE)
    expectGateRed(run, { checks: ['首个 `>` 引用块不存在'] })
  })

  it('红档必须印出修法与「形态由 owner 拍定」的出处', () => {
    const root = fixture({ 'bad.md': `${TITLE}> 只说了缘起\n` })
    const run = runGate(root, GATE)
    expect(run.stderr).toContain('> **扫描面**：')
    expect(run.stderr).toContain('owner 2026-09-15 拍定')
  })
})

describe('D 组 · 现行行为的钉子（⛔ 登记，不是背书）', () => {
  it('声明写成「（待补）」照样绿 —— 闸只判标签在不在', () => {
    const root = fixture({ 'a.md': `${TITLE}> **扫描面**：（待补）\n` })
    expectGateGreen(runGate(root, GATE))
  })
})

describe('E 组 · fail-closed（⛔ 空集不得读成全合规）', () => {
  it('目录不存在 ⇒ EXIT=1，⛔ 不是绿', () => {
    const root = fixture(null)
    const run = runGate(root, GATE)
    expectGateRed(run, { checks: ['目录不存在'] })
  })

  it('目录在但 0 份 .md ⇒ EXIT=1，且说明它不是「全合规」', () => {
    const root = fixture({})
    const run = runGate(root, GATE)
    expectGateRed(run, { checks: ['判定面为空', '输入面塌了'] })
  })

  it('只有非 .md 文件 ⇒ 同样 EXIT=1（⛔ 扩展名过滤后不得静默变空集）', () => {
    const root = fixture({})
    writeFileSync(join(root, REPORTS, 'note.txt'), 'nrix')
    const run = runGate(root, GATE)
    expectGateRed(run, { checks: ['判定面为空'] })
  })
})

describe('F 组 · 接线（摘掉接线必须炸，⛔ 不是只测判据）', () => {
  it('闸是从 REPO_ROOT 解析目录的 —— fixture root 下跑到的是 fixture 自己的那 2 份', () => {
    const root = fixture({ 'a.md': TITLE + GOOD, 'b.md': TITLE + GOOD })
    const run = runGate(root, GATE)
    // ⛔ 若接线错（读到真仓库的 8 份），这里会是 8 ⇒ 这条断言就是接线的钉子
    expectGateGreen(run, { contains: ['2 份报告'] })
  })
})

afterAll(() => {
  // createGateFixture 的 root 由 cleanupGateFixtures 统一清；这里只兜底本文件另建的临时物
  try {
    rmSync(join(process.cwd(), '.nrix-should-not-exist'), { recursive: true, force: true })
  } catch {
    /* 无副作用 */
  }
})
