// tests/audit-claude-design-reference-numbers.test.ts
// -----------------------------------------------------------------------------
// `audit:reference-numbering`（L4 pre-commit + L5 gitea-pr-checks）的**整脚本**回归面。
//
// 该闸 327 行、9 条判据（RN-R1…RN-R9）、**零导出** —— 量具
// `pnpm report:gate-regression-face` 记为「零判据覆盖 · P1/P2 双 PASS · P3 NAMED(5)」。
// 本文件是 [[INFRA-F138]] 用共享 helper 兑现的第二条，⛔ 闸本体一行没改。
//
// 它是 helper 复用成本的实测样本：第一条（`audit-doc-sync`）连 harness 一起写，
// 本条只写 fixture 与断言 —— 抽 helper 的账就是在这里回本的。
//
// fixture 是**三方自洽**的最小世界：producer 表 ⟷ registry.numbers ⟷ 项目远端实况。
// 每条致败用例只动其中一处，其余不变 ⇒ 红的原因可归因到那一处。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-claude-design-reference-numbers.mjs'
const REGISTRY = 'docs/internal/claude-design-reference-registry.json'
const PRODUCER = 'scripts/fake-producer.mjs'
const PROJ = 'proj-fixture-1'

afterEach(cleanupGateFixtures)

type Registry = {
  producers: Record<string, unknown>
  projects: unknown[]
  numbers: Record<string, unknown>
}

/** 三方自洽的绿档世界；`mutate` 只动一处以定位致败源。 */
function build(mutate?: (r: Registry) => void, producerRows?: [string, string][]) {
  const registry: Registry = {
    producers: {
      p1: { script: PRODUCER, exportName: 'REFERENCE_DOCS', targets: [PROJ], note: 'fixture' },
    },
    projects: [
      {
        id: PROJ,
        label: 'fixture project',
        verifiedAt: '2026-08-25',
        method: 'fixture — 非真实 list_files',
        observedNumbered: ['01-alpha.md', '02-beta.md', '03-curated.md'],
      },
    ],
    numbers: {
      '01': { owner: 'producer', file: '01-alpha.md', producers: ['p1'], addedAt: '2026-07-30' },
      '02': { owner: 'producer', file: '02-beta.md', producers: ['p1'], addedAt: '2026-07-30' },
      '03': {
        owner: 'curated-remote',
        file: '03-curated.md',
        project: PROJ,
        addedAt: '2026-07-30',
        note: '远端手工策展，仓库无真源',
      },
      '04': { owner: 'retired', note: '退役号，⛔ 不得复用' },
    },
  }
  mutate?.(registry)

  const rows = producerRows ?? [
    ['src/a.md', '01-alpha.md'],
    ['src/b.md', '02-beta.md'],
  ]

  return createGateFixture({
    gate: GATE,
    prefix: 'ref-numbering-fx',
    dirs: ['docs/internal'],
    files: {
      [REGISTRY]: JSON.stringify(registry, null, 2),
      [PRODUCER]: `export const REFERENCE_DOCS = ${JSON.stringify(rows)}\n`,
    },
  })
}

describe('audit:reference-numbering — 绿档 + 自印非空过', () => {
  it('producer / registry / 远端实况三方自洽 → exit 0，自印 fixture 自己的数', () => {
    const run = runGate(build(), GATE)
    // 真仓库是「登记 16 号 / 策展 1 / 退役 1」；fixture 是 4 / 1 / 1 且 producer 出 2 项。
    // 跑错了树这组数不可能对上 —— 非空过凭据。
    expectGateGreen(run, {
      contains: ['✓ reference 编号闸 PASS', '登记 4 号（producer 出 2 项 / 远端策展 1 / 退役 1）'],
    })
  })

  it('绿档同时印出「不能证」那一行（诚实边界没被删掉）', () => {
    const run = runGate(build(), GATE)
    expect(run.stdout).toContain('不能证')
    expect(run.stdout).toContain('此刻仍成立')
  })
})

describe('audit:reference-numbering — RN-R1 registry 自身形状', () => {
  it('键不是两位数字 → RN-R1', () => {
    const run = runGate(build((r) => { r.numbers['9'] = { owner: 'retired', note: 'x' } }), GATE)
    expectGateRed(run, { marker: '✗ reference 编号闸 FAIL', checks: ['[RN-R1]', '不是两位数字编号'] })
  })

  it('owner 非法 → RN-R1', () => {
    const run = runGate(build((r) => { r.numbers['05'] = { owner: 'mine', file: '05-x.md' } }), GATE)
    expectGateRed(run, { checks: ['[RN-R1]', 'owner="mine"'] })
  })

  it('retired 却仍带 file → RN-R1（退役号不许挂文件）', () => {
    const run = runGate(build((r) => { r.numbers['04'] = { owner: 'retired', file: '04-x.md', note: 'x' } }), GATE)
    expectGateRed(run, { checks: ['[RN-R1]', 'retired 却还带 file'] })
  })

  it('retired 没写 note → RN-R1（为何退役必须留下理由）', () => {
    const run = runGate(build((r) => { r.numbers['04'] = { owner: 'retired' } }), GATE)
    expectGateRed(run, { checks: ['[RN-R1]', '没写 note'] })
  })

  it('file 前缀号与键不一致 → RN-R1', () => {
    const run = runGate(build((r) => {
      r.numbers['01'] = { owner: 'producer', file: '07-alpha.md', producers: ['p1'], addedAt: '2026-07-30' }
    }), GATE)
    expectGateRed(run, { checks: ['[RN-R1]', '前缀号与键不一致'] })
  })

  it('引用未登记的 producer → RN-R1', () => {
    const run = runGate(build((r) => {
      ;(r.numbers['01'] as Record<string, unknown>).producers = ['ghost']
    }), GATE)
    expectGateRed(run, { checks: ['[RN-R1]', '未登记的 producer'] })
  })
})

describe('audit:reference-numbering — RN-R2/R3 producer 表', () => {
  it('producer script 不存在 → RN-R2', () => {
    const run = runGate(build((r) => {
      ;(r.producers.p1 as Record<string, unknown>).script = 'scripts/gone-producer.mjs'
    }), GATE)
    expectGateRed(run, { checks: ['[RN-R2]', 'script 不存在'] })
  })

  it('producer 没导出数组 → RN-R2（导出名写错也算）', () => {
    const run = runGate(build((r) => {
      ;(r.producers.p1 as Record<string, unknown>).exportName = 'NOT_THERE'
    }), GATE)
    expectGateRed(run, { checks: ['[RN-R2]', '没导出数组'] })
  })

  it('producer 表内同号出现两次 → RN-R3', () => {
    const run = runGate(build(undefined, [
      ['src/a.md', '01-alpha.md'],
      ['src/a2.md', '01-dup.md'],
      ['src/b.md', '02-beta.md'],
    ]), GATE)
    expectGateRed(run, { checks: ['[RN-R3]', '表内编号 01 出现两次'] })
  })
})

describe('audit:reference-numbering — RN-R4/R5/R6 producer ⟷ registry 双向', () => {
  it('producer 出了未登记的号 → RN-R4（加号必须先核号再登记）', () => {
    const run = runGate(build(undefined, [
      ['src/a.md', '01-alpha.md'],
      ['src/b.md', '02-beta.md'],
      ['src/c.md', '09-unregistered.md'],
    ]), GATE)
    expectGateRed(run, { checks: ['[RN-R4]', '09', '没登记'] })
  })

  it('producer 复用退役号 → RN-R6 且带出退役理由', () => {
    const run = runGate(build(undefined, [
      ['src/a.md', '01-alpha.md'],
      ['src/b.md', '02-beta.md'],
      ['src/d.md', '04-reused.md'],
    ]), GATE)
    expectGateRed(run, { checks: ['[RN-R6]', '复用了退役号 04', '⛔ 不得复用'] })
  })

  it('producer 撞远端策展号 → RN-R6（让号，别覆盖别人的文档）', () => {
    const run = runGate(build(undefined, [
      ['src/a.md', '01-alpha.md'],
      ['src/b.md', '02-beta.md'],
      ['src/c.md', '03-mine.md'],
    ]), GATE)
    expectGateRed(run, { checks: ['[RN-R6]', '撞上远端策展文档'] })
  })

  it('registry 声明由 p1 产出但 producer 表里没有 → RN-R5（删号要一并删登记）', () => {
    const run = runGate(build(undefined, [['src/a.md', '01-alpha.md']]), GATE)
    expectGateRed(run, { checks: ['[RN-R5]', '02', '该 producer 表里没有'] })
  })
})

describe('audit:reference-numbering — RN-R7 远端实况 ⟷ 登记', () => {
  it('远端实况有号但 registry 未登记 → RN-R7', () => {
    const run = runGate(build((r) => {
      ;(r.projects[0] as Record<string, unknown>).observedNumbered = [
        '01-alpha.md', '02-beta.md', '03-curated.md', '08-someone-else.md',
      ]
    }), GATE)
    expectGateRed(run, { checks: ['[RN-R7]', '08', '未登记'] })
  })

  it('策展号在远端已消失 → RN-R7（撤登记或更新实况）', () => {
    const run = runGate(build((r) => {
      ;(r.projects[0] as Record<string, unknown>).observedNumbered = ['01-alpha.md', '02-beta.md']
    }), GATE)
    expectGateRed(run, { checks: ['[RN-R7]', '最近一次 list_files 实况里没有它'] })
  })
})

describe('audit:reference-numbering — RN-R8 加号必须伴随核号', () => {
  it('addedAt 晚于该项目 verifiedAt → RN-R8（加号时没真去核过）', () => {
    const run = runGate(build((r) => {
      ;(r.numbers['01'] as Record<string, unknown>).addedAt = '2026-08-26'
    }), GATE)
    expectGateRed(run, { checks: ['[RN-R8]'] })
  })

  it('⛔ must-not-hit：addedAt 早于 verifiedAt 是正常的，不报', () => {
    const run = runGate(build((r) => {
      ;(r.numbers['01'] as Record<string, unknown>).addedAt = '2026-01-01'
    }), GATE)
    expectGateGreen(run)
  })
})

describe('audit:reference-numbering — 接线钉（判据 → 退出码 → 逐条点名）', () => {
  it('registry 缺失 → exit 1 并点名，⛔ 不是静默 exit 0', () => {
    const root = createGateFixture({ gate: GATE, prefix: 'ref-numbering-fx-noreg', dirs: ['docs/internal'] })
    const run = runGate(root, GATE)
    expectGateRed(run, { marker: '✗ 缺 registry' })
  })

  it('registry JSON 语法坏 → exit 1 并报解析失败（fail-closed）', () => {
    const root = build()
    // 直接把 registry 写坏，其余不动
    const { writeFileSync } = require('node:fs') as typeof import('node:fs')
    const { join } = require('node:path') as typeof import('node:path')
    writeFileSync(join(root, REGISTRY), '{ not json')
    const run = runGate(root, GATE)
    expectGateRed(run, { marker: 'registry JSON 解析失败' })
  })

  it('多条判据同时违例 → 计数正确且逐条印出（接线不吞 failures）', () => {
    const run = runGate(
      build((r) => {
        r.numbers['05'] = { owner: 'mine', file: '05-x.md' }
        r.numbers['06'] = { owner: 'retired' }
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('FAIL（2 条）')
    expect(run.stderr).toContain('owner="mine"')
    expect(run.stderr).toContain('没写 note')
  })
})
