// tests/audit-icon-naming-rules.test.ts
// -----------------------------------------------------------------------------
// `audit:icon-category-enum` + `audit:icon-leaf-kebab`（L4 pre-commit + L5 gate-chain）
// 的**整脚本**回归面 —— 两个 npm key 共用一个脚本 `scripts/audit-icon-naming-rules.mjs`，
// mode 由 `process.argv[2]` 选。
//
// 为什么是整脚本而不是 import 判据函数：该闸 83 行**零导出**、判据写在顶层，
// 结构上没有可 import 的符号。量具 `pnpm report:gate-regression-face` 把它记为
// 「零判据覆盖 · P1/P2 双 PASS」。本文件是那条判定的兑现（[[INFRA-F138]]）。
// ⛔ 闸本体一行没改。
//
// ⚠️ 本文件是 harness 加 `args` 参数后的第一个消费者：**mode 是这条闸的挂载形态**，
// 不传参跑它只会走 usage 分支 exit 2，测到的不是判据（见用例「接线钉 · usage」）。
//
// 覆盖：两个 mode 各自的绿档非空过 + 致败用例 + **交叉 must-not-hit**
// （一个 mode 不许报另一个 mode 的违例）+ §3.2 负数叶子豁免 + 候选池形态 + 3 条接线钉。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-icon-naming-rules.mjs'
const GENERATED = 'src/icons/generated'

afterEach(cleanupGateFixtures)

/** 造一份形似 `src/icons/generated/*.ts` 的注册表（闸只认 `name: '<cat>/<leaf>'` 这个词法形态）。 */
function registry(names: string[]): string {
  return `export const icons = [\n${names
    .map((n) => `  { component: Icon, name: '${n}' },`)
    .join('\n')}\n]\n`
}

type Overrides = {
  /** 覆盖 `generated/a.ts` 的名字表，默认三条全合法 */
  names?: string[]
  /** 额外/覆盖的 fixture 文件 */
  files?: Record<string, string>
}

function build(o: Overrides = {}) {
  const names = o.names ?? ['action/add', 'navigation/arrow-down', 'others/rotation--90']
  return createGateFixture({
    gate: GATE,
    prefix: 'icon-naming-fx',
    dirs: [GENERATED],
    files: {
      [`${GENERATED}/a.ts`]: registry(names),
      ...(o.files ?? {}),
    },
  })
}

describe('audit:icon-naming-rules — 绿档 + 自印非空过', () => {
  it('category-enum：三条全合法 → exit 0，且自印的是 fixture 自己的读数（≠ 真仓库 ⇒ 非空过）', () => {
    const run = runGate(build(), GATE, ['category-enum'])
    // 真仓库是几百条 registry name，fixture 恰好 3 条 —— 跑错了树这个数不可能对上。
    expectGateGreen(run, {
      contains: [
        'audit:icon-category-enum: 3 registry names checked',
        'category-enum (18 approved)',
        'OK — all registry names conform.',
      ],
    })
  })

  it('leaf-kebab：同一份 fixture → exit 0 且自印 fixture 自己的数', () => {
    const run = runGate(build(), GATE, ['leaf-kebab'])
    expectGateGreen(run, {
      contains: ['audit:icon-leaf-kebab: 3 registry names checked', 'OK — all registry names conform.'],
    })
  })
})

describe('audit:icon-naming-rules — 判据 (1) category-enum', () => {
  it('category 拼错（navigaton）→ 红且点名那个 category 与 name', () => {
    const run = runGate(build({ names: ['navigaton/arrow-down'] }), GATE, ['category-enum'])
    expectGateRed(run, {
      marker: '1 violation(s):',
      checks: ['navigaton/arrow-down', 'category "navigaton" ∉ approved enum'],
    })
  })

  it('⛔ must-not-hit：18 个 enum 成员逐个都不报（判据不是白名单写窄了）', () => {
    const all = [
      'action', 'navigation', 'status', 'rating', 'brand', 'time', 'theme', 'media',
      'file', 'network', 'input', 'output', 'indicator', 'communication', 'user',
      'setting', 'feature', 'others',
    ].map((c) => `${c}/x`)
    const run = runGate(build({ names: all }), GATE, ['category-enum'])
    expectGateGreen(run, { contains: ['18 registry names checked', 'OK — all registry names conform.'] })
  })

  it('⛔ must-not-hit：category-enum 模式**不**报 leaf 违例（两 mode 不越界）', () => {
    const run = runGate(build({ names: ['action/Close_Big'] }), GATE, ['category-enum'])
    expectGateGreen(run, { contains: ['1 registry names checked', 'OK'] })
  })
})

describe('audit:icon-naming-rules — 判据 (2) leaf-kebab', () => {
  it('leaf 含下划线 + 大写 → 红且点名 leaf', () => {
    const run = runGate(build({ names: ['action/Close_Big'] }), GATE, ['leaf-kebab'])
    expectGateRed(run, {
      marker: '1 violation(s):',
      checks: ['action/Close_Big', 'leaf "Close_Big" not lower-kebab'],
    })
  })

  it('leaf 含空格 → 红（rename 时最常见的那种）', () => {
    const run = runGate(build({ names: ['action/close big'] }), GATE, ['leaf-kebab'])
    expectGateRed(run, { checks: ['leaf "close big" not lower-kebab'] })
  })

  it('§3.2 负数叶子 `rotation--90` 豁免 → 绿（阴性对照：双连字符不是笔误）', () => {
    const run = runGate(build({ names: ['others/rotation--90'] }), GATE, ['leaf-kebab'])
    expectGateGreen(run, { contains: ['1 registry names checked', 'OK'] })
  })

  it('但 `rotation--ab`（双连字符后不是数字）→ 红（豁免只开给数字，没开成通配）', () => {
    const run = runGate(build({ names: ['others/rotation--ab'] }), GATE, ['leaf-kebab'])
    expectGateRed(run, { checks: ['leaf "rotation--ab" not lower-kebab'] })
  })

  it('⛔ must-not-hit：leaf-kebab 模式**不**报 category 违例（两 mode 不越界）', () => {
    const run = runGate(build({ names: ['navigaton/arrow-down'] }), GATE, ['leaf-kebab'])
    expectGateGreen(run, { contains: ['1 registry names checked', 'OK'] })
  })
})

describe('audit:icon-naming-rules — 候选池形态', () => {
  it('⛔ must-not-hit：不含 `/` 的 name 不进候选池（正则要求 cat/leaf 两段）', () => {
    const run = runGate(
      build({ files: { [`${GENERATED}/a.ts`]: registry(['action/add']).replace("'action/add'", "'BareName'") } }),
      GATE,
      ['category-enum'],
    )
    // 0 条被收 ⇒ 分母是 0 而不是把 'BareName' 当 category 报出来
    expectGateGreen(run, { contains: ['0 registry names checked'] })
  })

  it('非 .ts 文件不被扫（`.md` 里的同形态字面量不算注册表）', () => {
    const run = runGate(
      build({ names: ['action/add'], files: { [`${GENERATED}/notes.md`]: registry(['navigaton/x']) } }),
      GATE,
      ['category-enum'],
    )
    expectGateGreen(run, { contains: ['1 registry names checked'] })
  })

  it('多份 .ts 的名字合并去重后一起判（分母跨文件累加）', () => {
    const run = runGate(
      build({
        names: ['action/add'],
        files: { [`${GENERATED}/b.ts`]: registry(['navigation/arrow-down', 'action/add']) },
      }),
      GATE,
      ['category-enum'],
    )
    // a.ts 1 条 + b.ts 2 条，其中 action/add 重复 ⇒ 去重后 2
    expectGateGreen(run, { contains: ['2 registry names checked'] })
  })
})

describe('audit:icon-naming-rules — 接线钉（判据 → 退出码 → 点名输出）', () => {
  it('不传 mode → exit **2** + usage，⛔ 不是静默 exit 0（这条闸有两个 npm key 各带一个参数）', () => {
    const run = runGate(build(), GATE)
    expectGateRed(run, {
      status: 2,
      marker: 'usage: audit-icon-naming-rules.mjs <category-enum | leaf-kebab>',
    })
  })

  it('传无效 mode → 同样 exit 2（不是 fallback 到某个默认判据）', () => {
    const run = runGate(build(), GATE, ['leaf-kebap'])
    expectGateRed(run, { status: 2, marker: 'usage:' })
  })

  it('多条违例 → 逐条印出、计数正确、按 name 排序（接线不吞 findings）', () => {
    const run = runGate(
      build({ names: ['zzz-bad/x', 'action/add', 'aaa-bad/y'] }),
      GATE,
      ['category-enum'],
    )
    expect(run.status).toBe(1)
    expect(run.stdout).toContain('2 violation(s):')
    expect(run.stdout).toContain('aaa-bad/y')
    expect(run.stdout).toContain('zzz-bad/x')
    expect(run.stdout.indexOf('aaa-bad/y')).toBeLessThan(run.stdout.indexOf('zzz-bad/x'))
    expect(run.stdout).toContain('Fix: rename to an approved category')
  })

  it('两个 mode 的修复指引不串（leaf-kebab 红档给的是 leaf 的修法）', () => {
    const run = runGate(build({ names: ['action/Close_Big'] }), GATE, ['leaf-kebab'])
    expect(run.stdout).toContain('Fix: rename leaf to lower-kebab')
    expect(run.stdout).not.toContain('add it to CATEGORY_ENUM')
  })

  it('generated 目录整个不存在 → 非零崩溃（fail-closed），⛔ 不是静默 exit 0', () => {
    // ⚠️ 如实登记：这是 readdirSync 抛 ENOENT 的**崩溃式** fail-closed，不是判据红。
    // 钉它的意义只在「分母没了不会变成绿」，⛔ 别读成「闸对缺目录有专门判据」。
    const root = createGateFixture({ gate: GATE, prefix: 'icon-naming-fx-nodir' })
    const run = runGate(root, GATE, ['category-enum'])
    expect(run.status).not.toBe(0)
    expect(`${run.stderr}${run.stdout}`).toContain('ENOENT')
  })
})
