import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import {
  SECTIONS, findMissingSections, stripFences,
} from '../scripts/audit-handoff-deliverable-sections.mjs'
import { isDeliverable } from '../scripts/audit-deliverable-open-questions.mjs'

const __dirname = dirname(fileURLToPath(import.meta.url))
const MARKER = '<!-- mockup-handoff -->'
const TRACE = 'Affordance: directional-vertical (intent: 展开列表)\nVocabulary scan: [Arrow/Down, Arrow/Up]\nChosen: Arrow/Down — 同几何\n'
// ↓ 2026-09-08 Q1 B1 批新增 4 段（覆盖 5 个 Acceptance 段 —— M36 与 M36.1 共用一段）
const B1 = `\n\n## derive from \`1:23\`\n\n## sectionInternalOverlap = 0\nsectionToSectionOverlap = 0\n\n## Buttons used: Button variants [primary]\n\n## reused file-local components\n| m | 2 | yes |`
const full = `${MARKER}\n\n## Reference probe\n| a |\n\n## clone source probe table\nN/A — 本轮无 clone()\n\n## CRUD self-check\nN/A — view-only\n\n## Affordance trace\n${TRACE}` + `\n\n## PRD examples scoped\nN/A — PRD 无 e.g. 列表\n\n## Cardinality self-check\nN/A — 单对象无跨页关系\n\n## IA 树\nN/A — 单 entity\n\n## Selection model\nN/A — 无选中态` + B1

// INFRA-F123：四条 L1 规则的产物落在同一份 handoff .md 上 ⇒ 一个存在性闸关四条
// （M35/R13 那条 2026-08-21 并入 —— 它的封路理由「产物落在对话侧」被规则自己的 §Acceptance 推翻）。
describe('audit-handoff-deliverable-sections', () => {
  it('判据字符串逐字取自规则原文，未发明格式', () => {
    expect(SECTIONS.map(s => s.phrase)).toEqual([
      // ↓ 2026-09-07 Q1 第 2 步第一批：新增三段，判据串同样逐字取自各自规则 §Acceptance 首条
      'Reference probe', 'clone source probe table', 'CRUD self-check',
      'PRD examples scoped', 'Cardinality self-check', 'IA 树', 'Affordance:',
      // ↓ 2026-09-08 Q1 B1 批：4 条判据覆盖 5 段（M36 / M36.1 逐字要求同一个 handoff 段）
      'derive from', 'sectionInternalOverlap', 'Buttons used:', 'reused file-local components',
    ])
  })

  // ↓ 2026-09-08：`altPhrases` 的正反对照。这个字段是为 §M21.1 补的 ——
  //   该规则首条逐字是两个引号短语的「或」，而在此之前本闸无法表达 phrase 层的 OR。
  it('altPhrases：写规则给的另一支（from-scratch）同样放行 —— 判据是「两个短语至少写一个」', () => {
    const alt = full.replace('## derive from `1:23`', '## from-scratch with rationale: 无同类件可 derive')
    expect(findMissingSections(alt)).toHaveLength(0)
  })

  it('altPhrases：两支都没写 → 只点名 M21.1（不连坐）', () => {
    const neither = full.replace('## derive from `1:23`', '## 起手声明：抄了隔壁那个')
    expect(findMissingSections(neither).map(m => m.id)).toEqual(['M21.1'])
  })

  it('阴性对照：altPhrases 缺省的 6 条行为逐字不变 —— 补这个字段没动既有判据', () => {
    // 既有 SECTIONS 一条都不该带 altPhrases；带了就说明有人顺手给别的规则开了 OR
    expect(SECTIONS.filter(s => s.altPhrases).map(s => s.id)).toEqual(['M21.1'])
  })

  it('I2：两个数缺后一个 → 只点名那一行（照 IA-FIRST 的不连坐语义）', () => {
    const md = full.replace('sectionToSectionOverlap = 0', '')
    expect(findMissingSections(md).map(m => m.id)).toEqual(['I2:sectionToSectionOverlap'])
  })

  it('M36 / M36.1 共用一条判据 —— 缺那一段只报一次，⛔ 不报两条', () => {
    const md = full.replace('## reused file-local components', '## 复用的本地组件')
    expect(findMissingSections(md).map(m => m.id)).toEqual(['M36-REUSE'])
  })

  it('三段齐全 → 零缺失', () => {
    expect(findMissingSections(full)).toHaveLength(0)
  })

  it('缺一段 → 只点名那一段（不连坐）', () => {
    const missing = findMissingSections(full.replace('CRUD self-check', 'CRUD 自查'))
    expect(missing.map(m => m.id)).toEqual(['CRUD'])
  })

  it('全缺 → 每条都点名（11 条判据，⛔ 不连坐、不合并）', () => {
    expect(findMissingSections(`${MARKER}\n\n## 1. 现状\n`).map(m => m.id))
      .toEqual(['M37', 'M42', 'CRUD', 'PRD-EG', 'CARDINALITY', 'IA-FIRST', 'M35',
        'M21.1', 'I2', 'M32', 'M36-REUSE'])
  })

  // ↓ M35 / R13 那条的第二层：规则要的是**逐字 3 行**，不是一个段落标题
  it('M35：3 行 trace 齐全 → 零缺失', () => {
    expect(findMissingSections(full).filter(m => m.id.startsWith('M35'))).toHaveLength(0)
  })

  it('M35：锚点在但缺 Vocabulary scan → 只点名那一行（不连坐、不掩盖是哪行）', () => {
    const md = full.replace(/Vocabulary scan: .*\n/, '')
    expect(findMissingSections(md).map(m => m.id)).toEqual(['M35:Vocabulary scan:'])
  })

  it('M35：锚点在但缺 Chosen → 点名 Chosen 那一行', () => {
    const md = full.replace(/Chosen: .*\n/, '')
    expect(findMissingSections(md).map(m => m.id)).toEqual(['M35:Chosen:'])
  })

  it('M35：N/A 档整段豁免第二层 —— 这是规则自带的 skip 形态，也是本闸最便宜的逃生口', () => {
    const na = full.replace(TRACE, 'Affordance: N/A — 本轮未新增 affordance 类元素\n')
    expect(findMissingSections(na)).toHaveLength(0)
  })

  it('阴性对照：N/A 写在围栏里**不算** —— 否则模板的说明文字会让第二层对每份派生文档永久豁免', () => {
    const fencedNa = full.replace(TRACE, 'Affordance: <category>\n```\nAffordance: N/A — 说明文字\n```\n')
    // 锚点仍在（第一行未围栏），N/A 被 stripFences 剥掉 ⇒ 第二层照常要求另两行
    expect(findMissingSections(fencedNa).map(m => m.id))
      .toEqual(['M35:Vocabulary scan:', 'M35:Chosen:'])
  })

  it('N/A 写法算「段落存在」—— 存在性闸的天花板，防伪那层不在这条闸', () => {
    const na = `${MARKER}\nclone source probe table: N/A — 本轮无 clone()\nReference probe\nCRUD self-check: N/A — view-only\nPRD examples scoped: N/A — PRD 无 e.g. 列表\nCardinality self-check: N/A — 单对象\nIA 树: N/A — 单 entity\nSelection model: N/A — 无选中态\nAffordance: N/A — 无 affordance 元素\nderive from: N/A — 本轮未新建 component\nsectionInternalOverlap = 0\nsectionToSectionOverlap = 0\nButtons used: 无 — 本轮不含按钮\nreused file-local components: 无\n`
    expect(findMissingSections(na)).toHaveLength(0)
  })

  it('fenced code block 里的短语不算数 —— 不能靠代码块蒙混', () => {
    const body = `Reference probe\nclone source probe table\nCRUD self-check\nPRD examples scoped\nCardinality self-check\nIA 树\nSelection model\n${TRACE}derive from x\nsectionInternalOverlap\nsectionToSectionOverlap\nButtons used:\nreused file-local components\n`
    const fenced = `${MARKER}\n\n\`\`\`\n${body}\`\`\`\n`
    expect(findMissingSections(fenced).map(m => m.id)).toEqual(['M37', 'M42', 'CRUD', 'PRD-EG', 'CARDINALITY', 'IA-FIRST', 'M35',
      'M21.1', 'I2', 'M32', 'M36-REUSE'])
    // 阴性对照：同样的文本不放进围栏时应当全部命中，证明差别来自 stripFences 而非拼写
    expect(findMissingSections(fenced.replace(/```/g, ''))).toHaveLength(0)
  })

  it('stripFences 只吃围栏、不吃正文', () => {
    expect(stripFences('a\n```\nx\n```\nb')).toContain('a')
    expect(stripFences('a\n```\nx\n```\nb')).not.toContain('x')
  })

  it('扫描面 = isDeliverable（非交付物 .md 不进判据）', () => {
    expect(isDeliverable('# 普通文档\n', 'README.md')).toBe(false)
    expect(isDeliverable(MARKER, 'anywhere.md')).toBe(true)
    expect(isDeliverable('# x', 'docs/handoffs/2026-08-14-x-handoff.md')).toBe(true)
  })

  it('consumer handoff 模板自身合规（本仓扫描面里唯一的存量文件）', () => {
    const tpl = readFileSync(
      resolve(__dirname, '..', 'templates', 'consumer-product', 'docs', 'handoffs', '_handoff.template.md'),
      'utf8',
    )
    expect(isDeliverable(tpl, 'templates/consumer-product/docs/handoffs/_handoff.template.md')).toBe(true)
    expect(findMissingSections(tpl)).toHaveLength(0)
  })
})
