// [[INFRA-F138]] —— `scripts/audit-layout-tokens.mjs` 的**整脚本**回归面。
// -----------------------------------------------------------------------------
// 与 `tests/audit-layout-tokens.test.ts` 的分工（⛔ 别合并，两者测的不是一回事）：
//   · 那一份 `import { parseLayoutTokens, extractMediaWidths, evaluate, … }` 直接调判据
//     函数 ⇒ 覆盖**判据逻辑**，看不见 `main()` 的接线（它连 `main` 都没导入）。
//   · 本份 **spawn 整个脚本**（共享 harness `tests/lib/gate-fixture-root.ts`）⇒ 覆盖
//     入口守卫 / 读 token 文件 / 遍历扫描面 / 判据 → 输出 → 退出码的**整条接线**。
//     被测闸一行不改。
//
// 为什么选它（**爆炸半径**，⛔ 不是「挂载最深」也不是「最便宜」—— 它只 2 层
//   L4 `.husky/pre-commit` + L5 `gate-chain`，同轮候选里 `prop-naming` / `sort-tokens`
//   的 `P3=NAMED(1)` 都比它的 `DIRLIST(readdir=2,named=4)` 便宜）。三条都可核：
//   ① **它守的对象随包，而它自己不随包** —— `SCAN_TARGETS` 含 `src`，那里的 `@media`
//      经 `dist/style.css` 发给所有 consumer ⇒ 假绿代价落在**下游产品的布局**，本仓
//      任何别的闸都看不见（同型先例 = 第五十/五十九轮的 consumer-contract 与
//      exports-types-contract，也与棘轮自印那条排序原则同向）。
//   ② **owner 的三条断点裁定寄存在它的豁免表里** —— `docs/STATUS.md` §一 1 逐字写着
//      「三条的重开条件已各自写进 audit-layout-tokens.mjs 的豁免表，别在这里找」。
//      而 `S4` 是 shrink-only：「修完由闸自己宣布」。⇒ 闸假绿 ⇒ 那三条裁定**静默失去
//      执行力**，且没有任何东西会报（memory `derived-artifact-cannot-hold-decisions`
//      的反面：判定落在契约面上是对的，前提是那个契约面真的在跑）。
//   ③ **它是那条 SoT 的唯一机械保障** —— STATUS §SoT 归属表逐字把「layout 断点 / 容器
//      宽的数值」的真源定为 `variables.css` 两个段，并注明「`audit:layout-tokens` 机械
//      核」。它治的病（7 个互不相同的断点值并存、每处就地硬写）正是那种**无人会人眼
//      发现**的漂移 —— 与 `prop-naming` 不同，后者有 owner 人工 review 兜一层。
//
// 🔴 **harness 的 `realpathSync` 对本闸是承重的。** 本闸用**原始**入口守卫
//   （`resolve(argv[1]) === resolve(fileURLToPath(import.meta.url))`），属 [[INFRA-F138]]
//   记的「14 条非随包闸仍用原始守卫」那一档（⛔ 那不是缺陷 —— 它们不随包、只从仓库根跑）。
//   独立探针实测：fixture root 不 realpath ⇒ 全部场景 `EXIT=0` + 双流零字节。
//
// 🔴 **本轮发现并如实登记（⛔ 本文件不修，改闸是 [[INFRA-F138]] 逐字封的路 —— 报 owner）：
//   `S2-empty-scan` 这条 fail-closed 判据，在 CLI 这条真实执行路径上永远开不了火。**
//   机械成因：`TOKENS_FILE = 'src/tokens/variables.css'` 落在 `SCAN_TARGETS[0] = 'src'`
//   之下、扩展名又 ∈ `SCAN_EXTS` ⇒ 只要它存在（不存在则 `main()` 更早一步 fail closed
//   退出，`evaluate` 根本不跑），扫描面就 **≥ 1** ⇒ `scan.fileCount === 0` 恒假。
//   ⇒ 它是触发器 R 说的「死判据」，只是死在**配置层**而不是代码层：哪天 token 文件挪出
//   `src/`，它立刻活。下方 C 组那条自钉判据把这个前提钉住 —— 前提一变，本条边界自动
//   转红并把人指到这里。**判据逻辑那半由旧面覆盖**（它直接传 `scan: { fileCount: 0 }`）。
//   ⛔ 这是**如实登记的边界，不是待补 TODO**，与前几轮的 `EXEMPTIONS` 空表 /
//   `STRUCTURAL_L4_ONLY` / `KNOWN_COLLISIONS` 同型。
//   ✅ **上面这段不是推断，是实测**（注入 ④ = 把 `if (scan.fileCount === 0)` 摘成 `if (false)`）：
//   独立探针 15 个场景的 `EXIT` / 两流长度 / 判据码**一格未变**（与健康态逐字相同）⇒ 该出口
//   在 CLI 路径上从未被走到。**本面 0 红、而旧面 1 红** —— 这是十向注入里唯一方向相反的一向，
//   也是「⛔ 别合并这两份」最硬的凭据：旧面**不是**本面的子集。
//
// 🔴 **十向故障注入的读数（每向先用独立探针断言故障态成立，⛔ 探针不经共享 harness ——
//   否则注入 ⑩ 会把探针自己毁掉；探针假名 `qhalt`，与本文件的 `krivo` 刻意不同以免自污染）：**
//   ①守卫恒假 ⇒ 探针 **15 场景全 `EXIT=0` + 双流零字节**，本面 **32 红** ②只摘退出码 ⇒
//   10 个真违例 `EXIT 1→0` 而两流长度**逐字不变**（`410/139`·`500/126`·`523/246`·`467/213`·
//   `459/149`·`397/266` …），本面 **15 红** ③摘 S1 断点分母 ⇒ 「把 token 删了」当场变绿
//   （`E1/O410/E139` → `E0/O462/E0`），**2 红** ④见上，**0 红**（旧面 1 红）⑤摘 S3 ad-hoc
//   接线 ⇒ 四个真违例场景输出与健康态**逐字相同**（`E0/O575/E0`），**9 红** ⑥摘 S3 unparsed
//   fail-closed ⇒ **2 红** ⑦摘 S4 shrink-only ⇒ **3 红** ⑧摘「读不到 token 文件」的出口 ⇒
//   **退出码仍是 1 而红的原因全变了**（`O0/E67` 的一句 fail closed → `O443/E705` 的三条
//   S1/S3），**2 红** ⑨摘注释中和 ⇒ 假阳复发、`comment-only` 由绿转红，**2 红**
//   ⑩**harness 摘 `realpathSync`** ⇒ **32 红，与①同数 ⇒ 那行承重**，且探针 15 场景
//   **完全不受影响** = 闸本体没坏的凭据。
//   **旧面那 26 条纯函数用例对 ①②⑧⑩ 四向全部 26/26 全绿**（③⑤⑥⑦⑨ 各 1-2 红 ⇒ 判据
//   逻辑那半它确实覆盖）⇒ **差正好落在接线那一半。**
//
// ✅ **反过来，本面比前两轮多覆盖一整支：`S4` 的两个方向都造得出。**
//   `EXEMPTIONS` 现在是**非空表**（2 条 owner 逐条拍过的裁定），CLI 走
//   `evaluate({ tokens, scan })` 默认参数 ⇒ 用的就是真表 ⇒ 绿档 fixture **必须**自带
//   那两个豁免文件与那两个值，否则 `S4` 当场红。这既是覆盖、又让绿档成为**真表的漂移
//   钉**（同 `exports-types-contract` 绿档钉 `./web-components` 的既有范式）。谁改了那张
//   表，本面会红，并由 C 组那条自钉判据直接说出原因。
//
// ⛔ 污染面纪律（[[INFRA-F138]] 逐字）：fixture 假名统一用 `krivo`（落地前实测全仓 0
//   文件命中，含 30 个历轮 worktree）；散文与断言里提到别的闸一律**去 `.mjs` 后缀、
//   去 `audit:` 冒号**，否则量具会把那条闸误报成「已覆盖」（高报）或挪档（低报）。
//   本闸 stdout 只引用它自己 ⇒ 不需要「断言截短」那一招。
//   ⚠️ 豁免表里那两个 `react-pilot/src/demos/*-demo.css` 是**真实路径且必须逐字写对**
//   （`S4` 按 `(file, value)` 认），但它们不是闸、不进量具的比对面。
//
// ⚠️ **`豁免 : 2 条` 刻意不算非空过凭据** —— 它取自闸里硬编码的 `EXEMPTIONS.length`，
//   fixture 与真仓库印出来一模一样。四个凭据见 A 组，各出自一个不同的输入面。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterAll } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
  REPO_ROOT,
  type GateRun,
} from './lib/gate-fixture-root'
import {
  TOKENS_FILE,
  SCAN_TARGETS,
  SCAN_EXTS,
  SKIP_DIRS,
  CONTAINER_REPORT_MIN_PX,
  EXEMPTIONS,
  parseLayoutTokens,
} from '../scripts/audit-layout-tokens.mjs'

const GATE = 'scripts/audit-layout-tokens.mjs'

// ── fixture 自己的读数：**纯 ASCII 手写的设计值**，⛔ 不用被测代码算 ──────────────
//    （用 `parseLayoutTokens()` 反算期望值 = 拿被测对象给自己出题，判据坏掉时两边一起坏。）
const FIX_BP_A = 321 // < CONTAINER_REPORT_MIN_PX ⇒ S5 阈值的**下侧**样本
const FIX_BP_B = 555 // 同上，下侧
const FIX_BP_C = 1999 // ≥ 阈值 ⇒ S5 上侧样本
const FIX_CONTAINER = 777
const FIX_ADHOC = 4321 // 不在 --bp-* 值集合里 ⇒ S3 开火
const FIX_COMMENT_ADHOC = 8642 // 只出现在 CSS 注释里 ⇒ 必须**不**开火

// 豁免表那两条的真实落点（`S4` 按 (file, value) 认，⛔ 必须逐字对上活源）
const EX_PROGRESS = 'react-pilot/src/demos/progress-demo.css'
const EX_BUTTON = 'react-pilot/src/demos/button-demo.css'
const EX_PROGRESS_PX = 900
const EX_BUTTON_PX = 980

const PANEL = 'src/components/krivo-panel.css'

const tokensCss = (opts: { bp?: boolean; container?: boolean } = {}): string => {
  const { bp = true, container = true } = opts
  const lines = [':root {']
  if (bp) {
    lines.push(`  --bp-krivo-a: ${FIX_BP_A}px;`)
    lines.push(`  --bp-krivo-b: ${FIX_BP_B}px;`)
    lines.push(`  --bp-krivo-c: ${FIX_BP_C}px;`)
  }
  if (container) lines.push(`  --container-krivo: ${FIX_CONTAINER}px;`)
  lines.push('}', '')
  return lines.join('\n')
}

const mediaBlock = (feature: 'max-width' | 'min-width', px: number, cls = 'krivo'): string =>
  `@media (${feature}: ${px}px) {\n  .${cls} { display: block; }\n}\n`

const PANEL_CSS = `.krivo { color: #000; }\n${mediaBlock('max-width', FIX_BP_B)}${mediaBlock('min-width', FIX_BP_C)}`

/** 健康 fixture：四份文件，两条豁免都命中，@media 值全 ∈ --bp-* 值集合。 */
const HEALTHY: Record<string, string> = {
  [TOKENS_FILE]: tokensCss(),
  [PANEL]: PANEL_CSS,
  [EX_PROGRESS]: mediaBlock('max-width', EX_PROGRESS_PX, 'krivo-progress'),
  [EX_BUTTON]: mediaBlock('max-width', EX_BUTTON_PX, 'krivo-button'),
}

const FIX_FILE_COUNT = Object.keys(HEALTHY).length // 4 —— 输入面①：磁盘上的文件存在性
const FIX_MEDIA_HITS = 4 // 输入面③：三份 CSS 里解析出的 @media 条数
const FIX_MEDIA_DISTINCT = 4
const FIX_S5_COUNT = 3 // 输入面④：另一条解析路径（≥600px 的字面量：1999 / 900 / 980）

/** 建 fixture + 跑整条闸。`overrides` 覆盖 / 新增 fixture 文件，`drop` 删掉某份。 */
function run(
  prefix: string,
  overrides: Record<string, string> = {},
  opts: { drop?: string[] } = {},
): GateRun {
  const files: Record<string, string> = { ...HEALTHY, ...overrides }
  for (const d of opts.drop ?? []) delete files[d]
  const root = createGateFixture({
    gate: GATE,
    prefix,
    files,
    // ⛔ 显式关掉 harness 默认的 `scripts/lib` 软链 —— 本闸只 import node 内建，不碰它。
    linkDirs: [],
  })
  return runGate(root, GATE)
}

/** 取「程序自己最后那句话」—— 终态事实，见 L 组。 */
function lastLine(s: string): string {
  const lines = s.replace(/\s+$/, '').split('\n')
  return lines[lines.length - 1]
}

// ── 真仓库读数：⛔ **不写死**，当场从活源算，否则真仓库一改反向钉就静默变空 ──────────
const realTokens = parseLayoutTokens(readFileSync(resolve(REPO_ROOT, TOKENS_FILE), 'utf-8'))
const REAL_BP_NAMES: string[] = [...realTokens.bp.keys()]
const REAL_BP_VALUES: number[] = [...realTokens.bp.values()]
const REAL_CONTAINER_NAMES: string[] = [...realTokens.container.keys()]

afterAll(cleanupGateFixtures)

// =============================================================================
describe('A 绿档 —— 非空过凭据取自四个不同输入面', () => {
  it('健康 fixture 绿，且自印的四个读数都是 fixture 自己的', () => {
    const r = run('krivo-healthy')
    expectGateGreen(r, {
      contains: [
        // 输入面①：fixture **磁盘上文件的存在性**（目录遍历的结果）
        `扫描面     : ${FIX_FILE_COUNT} 个文件`,
        // 输入面②：**token 文件的内容**（另一条读取路径：单文件 readFileSync + 正则）
        `断点 token : 3 个 — --bp-krivo-a=${FIX_BP_A}px --bp-krivo-b=${FIX_BP_B}px --bp-krivo-c=${FIX_BP_C}px`,
        `容器 token : 1 个 — --container-krivo=${FIX_CONTAINER}px`,
        // 输入面③：**另外三份 CSS 文件的内容**经 extractMediaWidths 解析出的
        `@media 命中 : ${FIX_MEDIA_HITS} 条 / ${FIX_MEDIA_DISTINCT} 个不同值`,
        // 输入面④：**同一批文件的另一条独立解析路径**（extractWidthLiterals）
        `S5 report-only：${FIX_S5_COUNT} 处`,
      ],
    })
  })

  it('绿档是真绿：stderr 零字节、stdout 无任何失败标记（常驻负向对照）', () => {
    const r = run('krivo-healthy-neg')
    expect(r.status).toBe(0)
    expect(r.stderr).toBe('')
    expect(r.stdout).not.toContain('✗')
    expect(r.stdout).not.toContain('违例')
    // must-not-hit 的正向锚点：闸确实跑到了最后一句
    expect(r.stdout).toContain('audit:layout-tokens ✓ 断点全部引用')
  })

  it('S5 那句诚实边界照常印出（⛔ 别据此宣称「容器宽已收口」）', () => {
    const r = run('krivo-healthy-scope')
    expect(r.stdout).toContain('不阻塞；容器宽未收口')
    expect(r.stdout).toContain(`≥ ${CONTAINER_REPORT_MIN_PX}px 的字面量`)
  })

  it('S5 逐条列出的三处都 ≥ 阈值，阈值下侧那两个断点确实被排除', () => {
    const r = run('krivo-healthy-s5')
    // 正向：三个上侧值逐条列出
    expect(r.stdout).toContain(`min-width: ${FIX_BP_C}px`)
    expect(r.stdout).toContain(`max-width: ${EX_PROGRESS_PX}px`)
    expect(r.stdout).toContain(`max-width: ${EX_BUTTON_PX}px`)
    // 负向：两个下侧值不该出现在 S5 段里（它们仍在 @media 命中里，见 A1）
    expect(r.stdout).not.toContain(`max-width: ${FIX_BP_B}px`)
    expect(r.stdout).not.toContain(`max-width: ${FIX_BP_A}px`)
  })
})

// =============================================================================
describe('B 反向钉 —— 绿档没有退回真仓库读', () => {
  it('反向钉本身是有意义的（fail-closed 自证：fixture 的名与值和真仓库无交集）', () => {
    // ⛔ 若哪天真仓库的 token 名或值恰好撞上 fixture 的设计值，下面那条反向钉会静默
    //    失去意义 ⇒ 本条先把前提钉住。
    for (const n of REAL_BP_NAMES) expect(n.startsWith('bp-krivo')).toBe(false)
    for (const n of REAL_CONTAINER_NAMES) expect(n).not.toBe('container-krivo')
    for (const v of REAL_BP_VALUES) expect([FIX_BP_A, FIX_BP_B, FIX_BP_C]).not.toContain(v)
    // 分母非空：三个真值都得真的量到了东西
    expect(REAL_BP_NAMES.length).toBeGreaterThan(0)
    expect(REAL_CONTAINER_NAMES.length).toBeGreaterThan(0)
    expect(REAL_BP_VALUES.length).toBeGreaterThan(0)
  })

  it('stdout 不含真仓库任何一个 bp / container token 的名值对', () => {
    const r = run('krivo-reverse')
    for (const [name, value] of realTokens.bp) expect(r.stdout).not.toContain(`--${name}=${value}px`)
    for (const [name, value] of realTokens.container) expect(r.stdout).not.toContain(`--${name}=${value}px`)
    // 正向锚点（must-not-hit 不许空过）：fixture 自己的名值对确实印出来了
    expect(r.stdout).toContain(`--bp-krivo-a=${FIX_BP_A}px`)
    expect(r.stdout).toContain(`--container-krivo=${FIX_CONTAINER}px`)
  })
})

// =============================================================================
describe('C 自钉判据 —— 本面的覆盖边界靠的是活源的哪几个事实', () => {
  it('真豁免表恰好是那两条（⇒ 绿档 fixture 必须自带这两个文件，这是 S4 覆盖的前提）', () => {
    // 这既是「本面能覆盖 S4 两个方向」的**凭据**，也是真表的漂移钉：
    // 谁改了那张表，本条当场红并把人指到文件头那段。
    expect(EXEMPTIONS).toHaveLength(2)
    expect(EXEMPTIONS.map((e: { file: string }) => e.file).sort()).toEqual([EX_BUTTON, EX_PROGRESS].sort())
    const byFile = Object.fromEntries(
      EXEMPTIONS.map((e: { file: string; value: number }) => [e.file, e.value]),
    )
    expect(byFile[EX_PROGRESS]).toBe(EX_PROGRESS_PX)
    expect(byFile[EX_BUTTON]).toBe(EX_BUTTON_PX)
  })

  it('🔴 S2 在 CLI 面上是死判据 —— token 文件落在扫描面之内 ⇒ fileCount 恒 ≥ 1', () => {
    // 这是文件头那条「S2 永远开不了火」的**凭据**，不是一句声称。
    // 前提一变（token 文件挪出 SCAN_TARGETS / 换扩展名），本条转红并把人指到文件头。
    const inScan = SCAN_TARGETS.some((t: string) => TOKENS_FILE.startsWith(`${t}/`))
    expect(inScan).toBe(true)
    expect(SCAN_EXTS.some((e: string) => TOKENS_FILE.endsWith(e))).toBe(true)
    // 且它所在的目录分量没有被 SKIP_DIRS 排掉（否则遍历会跳过它）
    for (const seg of TOKENS_FILE.split('/')) expect(SKIP_DIRS.has(seg)).toBe(false)
  })

  it('S5 阈值是正整数，且 fixture 的设计值确实落在它两侧（自钉设计前提）', () => {
    expect(Number.isInteger(CONTAINER_REPORT_MIN_PX)).toBe(true)
    // A4 的正负两侧断言全靠这个前提；它一变，A4 就不再是「阈值生效」的证据
    expect(FIX_BP_A).toBeLessThan(CONTAINER_REPORT_MIN_PX)
    expect(FIX_BP_B).toBeLessThan(CONTAINER_REPORT_MIN_PX)
    expect(FIX_BP_C).toBeGreaterThanOrEqual(CONTAINER_REPORT_MIN_PX)
    expect(EX_PROGRESS_PX).toBeGreaterThanOrEqual(CONTAINER_REPORT_MIN_PX)
  })

  it('fixture 的 ad-hoc 值确实不在自己的 --bp-* 值集合里（否则 G 组的红是假红）', () => {
    expect([FIX_BP_A, FIX_BP_B, FIX_BP_C]).not.toContain(FIX_ADHOC)
    expect([FIX_BP_A, FIX_BP_B, FIX_BP_C]).not.toContain(FIX_COMMENT_ADHOC)
    expect([EX_PROGRESS_PX, EX_BUTTON_PX]).not.toContain(FIX_ADHOC)
  })
})

// =============================================================================
describe('D main() 独有的接线 —— 旧面结构上覆盖不到（它连 main 都没导入）', () => {
  it('token 文件读不到 → fail closed exit 1，且 stdout 零字节（钉住它真的早退了）', () => {
    const r = run('krivo-no-tokens', {}, { drop: [TOKENS_FILE] })
    expect(r.status).toBe(1)
    expect(r.stderr).toContain(`读不到 ${TOKENS_FILE}`)
    expect(r.stderr).toContain('fail closed')
    // 这条路径在 evaluate 之前 ⇒ 覆盖面自印一个字都不该有
    expect(r.stdout).toBe('')
  })

  it('有真违例时退出码必须是 1（摘掉 process.exit(1) 这一档由本条抓）', () => {
    const r = run('krivo-exit-wired', { [PANEL]: mediaBlock('max-width', FIX_ADHOC) })
    expect(r.status).toBe(1)
    expect(r.stderr).toContain('S3-ad-hoc-breakpoint')
  })

  it('红档时覆盖面自印那半照常印完（判据红 ≠ 程序半途死掉）', () => {
    const r = run('krivo-red-still-prints', { [PANEL]: mediaBlock('max-width', FIX_ADHOC) })
    expect(r.stdout).toContain('audit:layout-tokens — 覆盖面')
    expect(r.stdout).toContain(`扫描面     : ${FIX_FILE_COUNT} 个文件`)
    expect(r.stdout).toContain('S5 report-only')
    // 而结论句**不该**出现（那是绿档专有）
    expect(r.stdout).not.toContain('✓ 断点全部引用')
  })
})

// =============================================================================
describe('E S1 分母 fail closed —— 防「把 token 删了让闸空转成假绿」', () => {
  it('解析不出任何 --bp-* → S1-no-breakpoint-tokens 红', () => {
    const r = run('krivo-s1-nobp', { [TOKENS_FILE]: tokensCss({ bp: false }) }, { drop: [PANEL] })
    expectGateRed(r, {
      checks: ['S1-no-breakpoint-tokens', TOKENS_FILE, '分母为 0', '闸会空转成假绿'],
    })
    // 单判据红：两条豁免仍命中 ⇒ 不该混进 S3 / S4
    expect(r.stderr).not.toContain('S3-')
    expect(r.stderr).not.toContain('S4-')
  })

  it('解析不出任何 --container-* → S1-no-container-tokens 红', () => {
    const r = run('krivo-s1-nocontainer', { [TOKENS_FILE]: tokensCss({ container: false }) })
    expectGateRed(r, { checks: ['S1-no-container-tokens', '分母 fail closed'] })
    // bp 还在 ⇒ 面板那两个值仍合规，不该有 S3
    expect(r.stderr).not.toContain('S3-')
  })

  it('两组 token 都没有 → 两条 S1 都报（不因为第一条就短路）', () => {
    const r = run(
      'krivo-s1-both',
      { [TOKENS_FILE]: tokensCss({ bp: false, container: false }) },
      { drop: [PANEL] },
    )
    expectGateRed(r, { checks: ['S1-no-breakpoint-tokens', 'S1-no-container-tokens'] })
    expect(r.stderr).toContain('2 条违例')
  })
})

// =============================================================================
describe('F S3 主判据 —— @media 宽度必须 ∈ --bp-* 值集合', () => {
  it('ad-hoc 断点 → 红，且点名 文件:行号 + 该值 + 当前值集合', () => {
    const r = run('krivo-s3-adhoc', { [PANEL]: mediaBlock('max-width', FIX_ADHOC) })
    expectGateRed(r, {
      checks: [
        'S3-ad-hoc-breakpoint',
        `${PANEL}:1`,
        `用了 ${FIX_ADHOC}px`,
        // 值集合按升序印出 —— 钉住它印的是 fixture 自己的集合
        `{${FIX_BP_A}, ${FIX_BP_B}, ${FIX_BP_C}}`,
        'AI 不自拟视觉值',
      ],
    })
  })

  it('min-width 与 max-width 同等受检', () => {
    const r = run('krivo-s3-minwidth', { [PANEL]: mediaBlock('min-width', FIX_ADHOC) })
    expectGateRed(r, { checks: ['S3-ad-hoc-breakpoint', `用了 ${FIX_ADHOC}px`] })
  })

  it('豁免按 (file, value) 认：同一个值换个文件 → 照样红', () => {
    // 900 在豁免表里，但只对 progress-demo.css 有效
    const r = run('krivo-s3-exempt-otherfile', { [PANEL]: mediaBlock('max-width', EX_PROGRESS_PX) })
    expectGateRed(r, { checks: ['S3-ad-hoc-breakpoint', PANEL, `用了 ${EX_PROGRESS_PX}px`] })
    // 正向锚点：真正命中豁免的那份没被连坐
    expect(r.stderr).not.toContain(`${EX_PROGRESS}:`)
  })

  it('豁免按 (file, value) 认：同一个文件换个值 → 照样红，且连带 S4 报旧值失联', () => {
    const r = run('krivo-s3-exempt-othervalue', {
      [EX_PROGRESS]: mediaBlock('max-width', FIX_ADHOC, 'krivo-progress'),
    })
    expectGateRed(r, { checks: ['S3-ad-hoc-breakpoint', EX_PROGRESS, `用了 ${FIX_ADHOC}px`] })
    expect(r.stderr).toContain('S4-stale-exemption')
  })
})

// =============================================================================
describe('G S3 fail closed on input shape —— 认不出的形态判红，不是放行', () => {
  it('range syntax 的 @media → S3-unparsed-media-width 红', () => {
    const r = run('krivo-s3-range', {
      [PANEL]: `@media (${FIX_BP_A}px <= width <= ${FIX_BP_B}px) {\n  .krivo { color: red; }\n}\n`,
    })
    expectGateRed(r, {
      checks: ['S3-unparsed-media-width', `${PANEL}:1`, '本闸认不出的宽度形态', 'fail closed'],
    })
  })

  it('即使值都在集合内，认不出的形态照样红（fail closed ≠ 查表放行）', () => {
    // FIX_BP_A / FIX_BP_B 都是合法档位，但写法本闸解析不了
    const r = run('krivo-s3-range-legalvalues', {
      [PANEL]: `@media (width >= ${FIX_BP_B}px) {\n  .krivo { color: red; }\n}\n`,
    })
    expectGateRed(r, { checks: ['S3-unparsed-media-width'] })
    // 正向锚点：它红的原因确实是形态，不是值
    expect(r.stderr).not.toContain('S3-ad-hoc-breakpoint')
  })
})

// =============================================================================
describe('H CSS 注释中和 —— 解析修正，⛔ 不是判据放宽', () => {
  it('注释里的 @media ad-hoc 值不开火（否则就是 2026-08-11 记的那个假阳）', () => {
    const r = run('krivo-comment-only', {
      [PANEL]: `/* 说明：本块必须排在 @media (max-width: ${FIX_COMMENT_ADHOC}px) 之后 */\n.krivo { color: #000; }\n`,
    })
    expectGateGreen(r, { contains: [`扫描面     : ${FIX_FILE_COUNT} 个文件`] })
    expect(r.stdout).not.toContain(String(FIX_COMMENT_ADHOC))
  })

  it('阴性对照：注释旁的**真**声明照收（中和没把真声明一起吞掉）', () => {
    const r = run('krivo-comment-plus-real', {
      [PANEL]:
        `/* 历史：这里曾用 @media (max-width: ${FIX_COMMENT_ADHOC}px) */\n` +
        mediaBlock('max-width', FIX_ADHOC),
    })
    expectGateRed(r, { checks: ['S3-ad-hoc-breakpoint', `用了 ${FIX_ADHOC}px`] })
    // 注释里那个值仍然一次都没被算进来
    expect(`${r.stdout}\n${r.stderr}`).not.toContain(String(FIX_COMMENT_ADHOC))
  })

  it('注释中和保留换行 ⇒ 真声明的行号不漂', () => {
    const r = run('krivo-comment-lineno', {
      [PANEL]: `/* krivo\n   多行注释\n   第三行 */\n${mediaBlock('max-width', FIX_ADHOC)}`,
    })
    expectGateRed(r, { checks: [`${PANEL}:4`] })
  })

  it('S5 那条解析路径同样中和注释（2026-08-12 记的「S3 修了 S5 漏了」）', () => {
    const r = run('krivo-comment-s5', {
      [PANEL]: `/* 曾经写过 max-width: ${FIX_COMMENT_ADHOC}px 在这里 */\n.krivo { color: #000; }\n`,
    })
    // 注释里那个 ≥600px 的字面量不该被 S5 计入 ⇒ 处数与健康档一致减去面板原有的 1 处
    expect(r.stdout).toContain(`S5 report-only：${FIX_S5_COUNT - 1} 处`)
    expect(r.stdout).not.toContain(String(FIX_COMMENT_ADHOC))
  })
})

// =============================================================================
describe('I S4 豁免表 shrink-only —— 修完由闸自己宣布', () => {
  it('一条豁免不再命中 → S4-stale-exemption 红，点名文件 / 值 / addedAt 并要求删行', () => {
    const r = run('krivo-s4-stale-one', {}, { drop: [EX_PROGRESS] })
    const addedAt = EXEMPTIONS.find((e: { file: string }) => e.file === EX_PROGRESS)!.addedAt
    expectGateRed(r, {
      checks: [
        'S4-stale-exemption',
        `${EX_PROGRESS} 的 ${EX_PROGRESS_PX}px 已不再命中`,
        `addedAt ${addedAt}`,
        '请删掉该行',
        '表只许缩不许长',
      ],
    })
    // 正向锚点：另一条还命中着，不该被连坐
    expect(r.stderr).not.toContain(`${EX_BUTTON} 的 ${EX_BUTTON_PX}px`)
  })

  it('两条都不再命中 → 报 2 条', () => {
    const r = run('krivo-s4-stale-both', {}, { drop: [EX_PROGRESS, EX_BUTTON] })
    expectGateRed(r, { checks: ['2 条违例', EX_PROGRESS, EX_BUTTON] })
  })

  it('两条都命中时自印「命中 2」，且不报任何 S4', () => {
    const r = run('krivo-s4-allhit')
    expect(r.stdout).toContain(`豁免       : ${EXEMPTIONS.length} 条（命中 ${EXEMPTIONS.length}）`)
    expect(r.stderr).not.toContain('S4-')
  })
})

// =============================================================================
describe('J S5 report-only —— 有内容也不参与阻塞', () => {
  it('S5 报了 3 处而其余全合规 ⇒ 仍绿 exit 0', () => {
    const r = run('krivo-s5-nonblocking')
    expect(r.status).toBe(0)
    expect(r.stdout).toContain(`S5 report-only：${FIX_S5_COUNT} 处`)
    expect(r.stdout).toContain('audit:layout-tokens ✓')
  })

  it('S5 的处数随文件内容变（钉住它真的在数，不是印死值）', () => {
    const r = run('krivo-s5-more', {
      [PANEL]: `${PANEL_CSS}.krivo-x { max-width: ${FIX_BP_C}px; }\n`,
    })
    expect(r.stdout).toContain(`S5 report-only：${FIX_S5_COUNT + 1} 处`)
    expect(r.status).toBe(0)
  })
})

// =============================================================================
describe('K 覆盖面自印 —— 它自己每次运行印出来的那几行是判据的一部分', () => {
  it('扫描面那行把 targets 与扩展名一起印出（⇒ 读者能自己核覆盖边界）', () => {
    const r = run('krivo-scope-line')
    expect(r.stdout).toContain(`（${SCAN_TARGETS.join(', ')} · ${SCAN_EXTS.join('/')}）`)
  })

  it('扫描面计数随文件增减而变（钉住它真的在遍历）', () => {
    const r = run('krivo-scope-count', { 'src/components/krivo-extra.css': '.krivo-e { color: #111; }\n' })
    expect(r.stdout).toContain(`扫描面     : ${FIX_FILE_COUNT + 1} 个文件`)
  })

  it('SKIP_DIRS 里的目录不进扫描面（拿 dist-wc 做样本）', () => {
    const skipped = [...SKIP_DIRS].find((d: string) => d !== '.git')!
    const r = run('krivo-scope-skip', { [`src/${skipped}/krivo-skipped.css`]: mediaBlock('max-width', FIX_ADHOC) })
    // 该文件里有 ad-hoc 断点；若它被扫到，闸会红
    expect(r.status).toBe(0)
    expect(r.stdout).toContain(`扫描面     : ${FIX_FILE_COUNT} 个文件`)
  })
})

// =============================================================================
describe('L 终态事实 —— 防「碰巧崩在同一个退出码上」的假红', () => {
  // 判据 = **闸自己的最后一句话必须是输出的最后一行**。
  // 崩溃同样是非零退出码，只断言 status 的红是假红（harness 头注释判据 2 逐字预言过）。
  it('绿档：stdout 末行恰好是闸的收尾句', () => {
    const r = run('krivo-terminal-green')
    expect(lastLine(r.stdout)).toBe(
      `audit:layout-tokens ✓ 断点全部引用 --bp-* 值集合（含 ${EXEMPTIONS.length} 条具名豁免）`,
    )
  })

  it('红档：stderr 末行是闸自己印的违例行，不是崩溃栈', () => {
    const r = run('krivo-terminal-red', { [PANEL]: mediaBlock('max-width', FIX_ADHOC) })
    expect(r.status).toBe(1)
    expect(lastLine(r.stderr)).toContain('改用已有档位')
    // 崩溃栈尾会是 `Node.js vXX.Y.Z` 这类；这条把它排除掉
    expect(lastLine(r.stderr)).not.toMatch(/^Node\.js v/)
  })

  it('token 文件缺失那条早退路径：stderr 末行就是它自己那句 fail closed', () => {
    const r = run('krivo-terminal-notokens', {}, { drop: [TOKENS_FILE] })
    expect(r.status).toBe(1)
    expect(lastLine(r.stderr)).toBe(
      `audit:layout-tokens ✗ 读不到 ${TOKENS_FILE} —— fail closed。`,
    )
  })
})
