import { describe, it, expect } from 'vitest'
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'

/**
 * [[INFRA-F138]] 量具 `scripts/gate-regression-face-inventory.mjs` 的回归面。
 *
 * ⚠️ 刻意走**整脚本 CLI** 形态而不是 import 纯函数 —— 那个量具本体就是在量
 * 「只 import 符号的测试覆盖不到接线」，给它配一份 I 类的面就是自证反例。
 * （该量具是 flat 顶层副作用脚本，本来也没有可 import 的导出。）
 *
 * 每条断言都钉**会变的量**：⛔ 不写「分类字段存在」这类结论相反时也照样通过的话。
 */
const REPO_ROOT = resolve(__dirname, '..')
const SCRIPT = resolve(REPO_ROOT, 'scripts/gate-regression-face-inventory.mjs')

function run(args: string[] = []): { status: number; stdout: string; stderr: string } {
  try {
    const stdout = execFileSync(process.execPath, [SCRIPT, ...args], { encoding: 'utf8', cwd: REPO_ROOT })
    return { status: 0, stdout, stderr: '' }
  } catch (e) {
    const err = e as { status: number; stdout?: string; stderr?: string }
    return { status: err.status, stdout: err.stdout ?? '', stderr: err.stderr ?? '' }
  }
}

type Row = {
  gate: string
  face: 'E-fx' | 'E' | 'I' | 'IL' | 'M' | 'NONE'
  blocking: boolean
  P1: string
  P2: string
  P3: string
  mounts: string[]
}
const report = (() => {
  const r = run(['--json'])
  expect(r.status).toBe(0)
  return JSON.parse(r.stdout) as {
    totalGates: number
    testFaceCount: number
    tally: Record<string, number>
    rows: Row[]
  }
})()
const byGate = new Map(report.rows.map((r) => [r.gate, r]))

describe('分母（fail-closed 的那一半 —— 这几条塌了，「零缺陷」报告就是假的）', () => {
  it('闸清单规模在数十条量级，不是 0 也不是 1', () => {
    // 落地当日实测 87。这里只钉下界：写死 87 会在每次新增闸时误红。
    expect(report.totalGates).toBeGreaterThan(50)
    expect(report.rows.length).toBe(report.totalGates)
  })

  it('tally 各类之和 == 闸总数（没有闸被静默漏分类）', () => {
    const sum = Object.values(report.tally).reduce((a, b) => a + b, 0)
    expect(sum).toBe(report.totalGates)
  })

  it('测试面在仓库本体量级（非空过钉 —— ⚠️ 在 worktree 里跑时这条没有鉴别力）', () => {
    // 🔴 落地当日的真缺陷：主仓把 ~30 个 git worktree 挂在 `.claude/worktrees/` 下，每个都是完整
    //    checkout ⇒ 遍历它们让测试面从 195 涨到 **5499**（5304 来自那些 worktree）。
    //    方向危险：外来/陈旧测试文件会让闸看起来**更有覆盖**（量具边界 #3 说的高报方向）。
    //    那次分类结果恰好没变 = 运气，不是判据。
    // ⚠️ 如实登记：**本条在 worktree 里跑等于空过** —— worktree 内部没有 `.claude/worktrees/`，
    //    数字恒在带内。真正的回归钉是下一条（源码级），本条只保证分母不是 0 也不是天文数字。
    expect(report.testFaceCount).toBeGreaterThan(100)
    expect(report.testFaceCount).toBeLessThan(1000)
  })

  it('SKIP_DIR 逐字含 `\\.claude`（上一条在 worktree 里空过，这条才是那个缺陷的回归钉）', () => {
    // 源码级断言：删掉 `.claude` 这一项，本条当场红，**与在哪棵树上跑无关**。
    const src = readFileSync(SCRIPT, 'utf8')
    const line = src.split('\n').find((l) => l.includes('const SKIP_DIR'))
    expect(line).toBeDefined()
    expect(line).toMatch(/\\\.claude/)
  })

  it('分类器有鉴别力：E 类与 NONE 类同时非空', () => {
    // 阴性对照：若分类器退化成恒定输出（全 NONE / 全 E），这条当场红。
    expect(report.tally['E-fx'] + report.tally['E']).toBeGreaterThan(0)
    expect(report.tally['NONE']).toBeGreaterThan(0)
  })
})

describe('地面真值（每条都在本轮逐条亲验过，来源写在断言里）', () => {
  it('audit-status-consistency = E-fx —— 2026-08-24 第二十八轮建的整脚本 fixture-root harness', () => {
    expect(byGate.get('scripts/audit-status-consistency.mjs')?.face).toBe('E-fx')
  })

  it('audit-doc-shape = E-fx —— 2026-08-28 兑现（它曾是 [[INFRA-F138]] 的立项实证对象）', () => {
    const r = byGate.get('scripts/audit-doc-shape.mjs')
    // 🔴 本条 2026-08-28 改口径，与上面 `audit-doc-sync` 那条同型。原文钉的是 `I`，理由是
    //    2026-08-24 拿这条闸做的两次单侧注入：改判据逻辑 ⇒ 旧面 1 failed/19 passed；**摘接线**
    //    ⇒ 闸对真违例（顶部 3496 B > 3000）改印 `✓ PASS` + exit 0，而全量 vitest 1993 passed。
    //    **那个理由仍然成立**（它正是 F138 立项的实证），变的是事实：
    //    `tests/audit-doc-shape-cli.test.ts` 已用 `tests/lib/gate-fixture-root.ts` 补上整脚本
    //    回归面（32 条用例）。八向故障注入实证 —— 守卫恒假 / harness 摘 realpathSync 各 29 红，
    //    只摘退出码 12 红（两流长度逐字不变），而旧面对这三向 **20/20 全绿**。
    //    ⇒ 它现在真的是 E-fx，继续钉 `I` 会（正确地）红。
    // P1/P2/blocking 三格仍钉着：它们是「这条路走不走得通」的判据，与覆盖状态无关。
    expect(r?.face).toBe('E-fx')
    expect(r?.blocking).toBe(true)
    expect(r?.P1).toBe('PASS')
    expect(r?.P2).toBe('PASS')
  })

  it('audit-doc-sync = E-fx —— 2026-08-25 用共享 harness 兑现（此前是「零改动 fixture-root 可行」的样板）', () => {
    const r = byGate.get('scripts/audit-doc-sync.mjs')
    // 🔴 本条 2026-08-25 改口径。原文钉的是「零覆盖类组（NONE ∪ M）」，理由是本文件把闸名
    //    写成字面量、量具因此正确地把它从 NONE 归到 M。**那个理由仍然成立**，变的是事实：
    //    `tests/audit-doc-sync.test.ts` 已用 `tests/lib/gate-fixture-root.ts` 补上整脚本
    //    回归面（13 条用例），故障注入实证 —— 摘掉该闸接线后**全量 2147 个测试里只有那 7 条转红**。
    //    ⇒ 它现在真的是 E-fx，钉零覆盖类组会（正确地）红。
    // P1/P2 两格仍钉着：它们是「这条路走不走得通」的判据，与覆盖状态无关，退化了要立刻知道。
    expect(r?.face).toBe('E-fx')
    expect(r?.blocking).toBe(true)
    expect(r?.P1).toBe('PASS')
    expect(r?.P2).toBe('PASS')
  })

  it('audit-claude-design-reference-numbers = E-fx —— 同一 harness 的第二条（复用成本的证据）', () => {
    const r = byGate.get('scripts/audit-claude-design-reference-numbers.mjs')
    expect(r?.face).toBe('E-fx')
    expect(r?.blocking).toBe(true)
  })

  it('audit-gate-mount-declaration 是 blocking —— 退出码正则修好前它被误判成 report-only', () => {
    // 判据 ③ 的回归钉：`process.exit(failed ? 1 : 0)` 必须被认成非零出口。
    expect(byGate.get('scripts/audit-gate-mount-declaration.mjs')?.blocking).toBe(true)
  })

  it('translation-completeness 的 P2 = proc（⚠️ 2026-08-27 订正：原写 cred，那是假阳）', () => {
    // 🔴 本条原文是「P2 带 cred —— 它要 Figma 凭据，fixture 成本反超」，**结论对不上事实**：
    //    该闸 import 只有 ajv / child_process / fs / path / gate-output-contract，`process.env`
    //    零真引用。旧判据命中的是**映射表变量名** `FIGMA_TOKEN_TO_CODE_TOKEN`（:365-366）。
    //    ⇒ 它的真依赖一直是 `proc`（execSync），从来不是凭据。
    //    留痕的原因：这条断言当时是绿的，而它**断言的理由是假的** —— 正是
    //    memory `review-result-vs-rationale` / `entry-restatement-is-secondhand` 那一类。
    expect(byGate.get('figma-sync/audit-translation-completeness.mjs')?.P2).toBe('proc')
  })

  it('audit-render-coverage-gaps 是 report-only —— 阴性对照，防 blocking 判据变成恒真', () => {
    expect(byGate.get('scripts/audit-render-coverage-gaps.mjs')?.blocking).toBe(false)
  })
})

describe('P2 的 cred 判据（2026-08-27 收窄的回归钉）', () => {
  // 🔴 缘起：旧判据 `/…|GITEA|_TOKEN\b/` 直接扫源码文本，而 `_TOKEN\b` 撞的是**设计系统仓库
  //    的核心领域词 "token"** ⇒ 实测 5 条假阳。P2 是决定「哪些闸能零改动喂 fixture」的筛子，
  //    它报假 = 把可做的工作面写成封路（同款代价 2026-08-26 已为「P3=TREE 是封路」付过一次）。
  //
  // ⚠️ 本组的反向钉（must-not-hit）**单独存在时会空过** —— 把 usesCred 整个删成恒 false，
  //    5 条 must-not-hit 全绿。所以下面第一条是**正向锚点**，它先立住，反向钉才有意义。

  it('正向锚点：6 条真打 Figma API 的 audit-mockup-* 必须仍判 cred（一条不少）', () => {
    // 逐条亲验过：都有行首 `import { requireFigmaToken } from './lib/figma-env.mjs'`
    // ＋对 Figma REST 的真 `await fetch(…)`。它们正是 stub 投资针对的那一批。
    // ⛔ 这里刻意不写 REST host / 请求头名的字面量：`audit:figma-env-single-source` 的 S6
    //    把「含那两个标记的 js 族文件」整个收进分母并要求 import 那个 parser（注释里也算，
    //    fail-closed 是刻意的）—— 本文件写上就会（正确地）被它判红。
    const mustBeCred = [
      'scripts/audit-mockup-bilingual-spacing.mjs',
      'scripts/audit-mockup-binding-fidelity.mjs',
      'scripts/audit-mockup-conformance.mjs',
      'scripts/audit-mockup-geometry-consistency.mjs',
      'scripts/audit-mockup-library-origin.mjs',
      'scripts/audit-mockup-overlap.mjs',
    ]
    for (const g of mustBeCred) {
      expect(byGate.get(g), `${g} 不在闸清单里了`).toBeDefined()
      expect(byGate.get(g)?.P2, `${g} 掉出 cred = 判据收窄过头，造出假阴`).toMatch(/cred/)
    }
  })

  it('must-not-hit：5 条零凭据零网络的闸不得再判 cred（每条注明假阳来源）', () => {
    const falsePositives: Array<[string, string]> = [
      ['scripts/audit-stale-anchors.mjs', "ENV_ALLOWLIST 里的字符串常量 'FIGMA_TOKEN'"],
      ['scripts/audit-style-contract.mjs', '正则常量名 CLEAN_TOKEN 撞 _TOKEN\\b'],
      ['scripts/audit-figma-env-single-source.mjs', '整行注释 + 给违规者的修法提示文案(:232)'],
      ['figma-sync/audit-translation-completeness.mjs', '映射表变量名 FIGMA_TOKEN_TO_CODE_TOKEN'],
      ['figma-sync/audit-component-attributes.mjs', '同上，写在正则字面量里(:88)'],
    ]
    for (const [g, why] of falsePositives) {
      expect(byGate.get(g), `${g} 不在闸清单里了`).toBeDefined()
      expect(byGate.get(g)?.P2, `${g} 仍被判 cred —— 假阳来源：${why}`).not.toMatch(/cred/)
    }
  })

  it('源码级：figma-env import 判据必须行首锚定（否则那条元闸的提示文案会再假阳）', () => {
    // 上一条已经用真实闸钉住了这个行为，但它依赖那条元闸的现状文本。
    // 这一条钉源码：删掉 `^\s*import\s` 锚定，本条当场红，与仓库内容无关。
    const src = readFileSync(SCRIPT, 'utf8')
    const line = src.split('\n').find((l) => l.includes("figma-env\\.mjs['\"]") && l.includes('.test('))
    expect(line, '找不到 figma-env import 判据行').toBeDefined()
    expect(line).toMatch(/\^\\s\*import\\s/)
  })

  it('源码级：cred 判据的输入必须是剥过整行注释的源码（不是 src 原文）', () => {
    // 那条元闸的注释里逐字写着 `process.env.FIGMA_TOKEN`（它在讲自己抓什么形态）。
    // 不剥整行注释就会把它判成 cred。⛔ 只剥「整行就是注释」的行 —— 少剥 = 宁留假阳不造假阴。
    const src = readFileSync(SCRIPT, 'utf8')
    expect(src).toMatch(/const codeOnly = src\.split\('\\n'\)\.filter/)
    // usesCred 的每一条都必须喂 codeOnly，⛔ 不许有一条漏喂 src 原文
    const block = src.slice(src.indexOf('const usesCred ='), src.indexOf('const readdirN'))
    expect(block).not.toBe('')
    const tests = block.match(/\.test\(([A-Za-z]+)\)/g) ?? []
    // 现为 3 条判据（第 4 条「REST 请求头字面量」落地时删掉了 —— 它被 S6 证明结构上不可达，
    // 理由写在量具那段注释里）。钉下界，新增判据不会误红。
    expect(tests.length).toBeGreaterThanOrEqual(3)
    expect(tests.every((t) => t === '.test(codeOnly)'), `有判据喂了 src 原文：${tests.join(' ')}`).toBe(true)
  })
})

describe('§共享 harness 形态（2026-08-25 新判据的回归钉）', () => {
  // 🔴 缘起：分类是**逐测试文件**的词法扫描，而 [[INFRA-F138]] 逐字要求的第一步是把 harness
  //    抽成共享 helper —— 抽完 spawn/copyFileSync 就都不在测试文件里了。实测：用 helper 给两条闸
  //    补完整脚本回归面后，它们只从 NONE 挪到 M（零覆盖类组内部移动），补上的接线覆盖**读数上不可见**。
  //    ⇒ entry 的第一步与量具原判据直接冲突，本组钉住修好的那一条。

  it('源码级：`SHARED_HARNESS_RE` 在场且逐字认 `gate-fixture-root`（删掉判据当场红，与在哪棵树跑无关）', () => {
    const src = readFileSync(SCRIPT, 'utf8')
    const line = src.split('\n').find((l) => l.includes('const SHARED_HARNESS_RE'))
    expect(line).toBeDefined()
    expect(line).toContain('gate-fixture-root')
  })

  it('⛔ 窄形态：`runGate` 没有被加进通用 EXEC_API_RE（否则自写同名函数的测试会搭便车）', () => {
    const src = readFileSync(SCRIPT, 'utf8')
    const line = src.split('\n').find((l) => l.includes('const EXEC_API_RE'))
    expect(line).toBeDefined()
    expect(line).not.toContain('runGate')
  })

  it('阴性对照：自带 execFileSync 的 audit-status-consistency 仍是 E-fx（新形态没改动旧路径）', () => {
    expect(byGate.get('scripts/audit-status-consistency.mjs')?.face).toBe('E-fx')
  })
})

describe('npm key 取词边界（判据 ① 的回归钉）', () => {
  it('audit-product-code 不因 audit:consumer ⊂ audit:consumer-contract 而被误认覆盖', () => {
    // `audit:consumer` 是 audit-product-code 的 key；子串匹配会让它命中任何提到
    // `audit:consumer-contract` 的测试，从而虚报覆盖。
    const r = byGate.get('scripts/audit-product-code.mjs')
    expect(r).toBeDefined()
    expect(r!.face).not.toBe('M')
  })
})

describe('本量具自己不是闸', () => {
  it('有零覆盖闸在场时仍 exit 0（report-only，不阻断任何人）', () => {
    // ⛔ 标题原写死「34 条」—— 那是落地当刻的快照，2026-08-25 已是别的数。断言本就不依赖它。
    const zero = report.rows.filter((r) => r.face === 'NONE' || r.face === 'M')
    expect(zero.length).toBeGreaterThan(0) // 前提：确实有零覆盖项，否则下一句无意义
    expect(run().status).toBe(0)
  })

  it('未知参数 fail closed（`pnpm run x -- --foo` 会把 `--` 本身也透传进来）', () => {
    expect(run(['--bogus']).status).toBe(1)
    expect(run(['--']).status).toBe(1)
  })

  it('已知参数不 fail', () => {
    expect(run(['--zero-only']).status).toBe(0)
  })

  it('人读输出印出「接线有覆盖 N / 总数」这个会变的量，不是一句恒真的自我陈述', () => {
    const out = run().stdout
    const m = out.match(/接线有覆盖 : (\d+) \/ (\d+)/)
    expect(m).not.toBeNull()
    const [, covered, total] = m!.map(Number)
    expect(total).toBe(report.totalGates)
    expect(covered).toBe(report.tally['E-fx'] + report.tally['E'])
    // 落地当刻 5/87。若哪天真的补齐到全覆盖，这条会红并要求订正 —— 那是想要的。
    expect(covered).toBeLessThan(total)
  })
})
