// tests/audit-icon-canonical-names.test.ts
// -----------------------------------------------------------------------------
// `audit:icon-canonical-names`（L4 pre-commit + L5 gitea-pr-checks，⛔ 不在 prepublishOnly 链）
// 的**整脚本**回归面。挂载真源 = 闸脚本头注释的「挂载层」段。
//
// 为什么是整脚本而不是 import 判据函数：该闸 161 行**零导出**、判据写在顶层，
// 结构上没有可 import 的符号。量具 `pnpm report:gate-regression-face` 把它记为
// 「零判据覆盖 · P1/P2 双 PASS」。本文件是那条判定的兑现（[[INFRA-F138]]）。
// ⛔ 闸本体一行没改。
//
// ⚠️ 这条闸会读 `dist/icons/manifest.json`（catalog 名放行通路），而 fixture root 是
// mkdtemp 的空树 ⇒ 默认走它自己的 try/catch「catalog 可选」分支。本文件**两条通路都测**：
// 不给 catalog（缺失不崩）· 给 catalog（catalog 名不算 finding）。
// 这也顺带证明了 F138 harness 的一条设计判据 —— ⛔ 别把 `dist` 软链进 fixture。
//
// 覆盖：绿档非空过 + A/B 两类 finding + 四种**不该判**的 name 形态 + catalog 两条通路
// + 递归扫描与双目录扫描面 + 4 条接线钉。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-icon-canonical-names.mjs'
const GENERATED = 'src/icons/generated'

afterEach(cleanupGateFixtures)

type RegEntry = { name: string; aliases?: string[] }

/** 造一份形似 `src/icons/generated/*.ts` 的注册表（闸按词法解析 name + aliases）。 */
function registry(entries: RegEntry[]): string {
  return `export const icons = [\n${entries
    .map(
      (e) =>
        `  { name: '${e.name}', exportName: 'IconX', aliases: [${(e.aliases ?? [])
          .map((a) => `'${a}'`)
          .join(', ')}] },`,
    )
    .join('\n')}\n]\n`
}

const DEFAULT_REGISTRY: RegEntry[] = [
  { name: 'action/close', aliases: ['close'] },
  { name: 'navigation/arrow-down', aliases: ['arrow-down'] },
  { name: 'status/done', aliases: [] },
]

type Overrides = {
  entries?: RegEntry[]
  /** `src/canonical/` 下的 .vue 内容，键是相对 src/canonical 的路径 */
  canonical?: Record<string, string>
  /** `src/components/` 下的 .vue 内容 */
  components?: Record<string, string>
  /** dist/icons/manifest.json 的 records[].name（不传则整个文件不存在） */
  catalog?: string[]
  files?: Record<string, string>
}

function build(o: Overrides = {}) {
  const files: Record<string, string> = {
    [`${GENERATED}/a.ts`]: registry(o.entries ?? DEFAULT_REGISTRY),
  }
  for (const [rel, body] of Object.entries(o.canonical ?? { 'FxIconProbe.vue': '<template><div /></template>\n' })) {
    files[`src/canonical/${rel}`] = body
  }
  for (const [rel, body] of Object.entries(o.components ?? {})) files[`src/components/${rel}`] = body
  if (o.catalog) {
    files['dist/icons/manifest.json'] = JSON.stringify({ records: o.catalog.map((name) => ({ name })) })
  }
  return createGateFixture({
    gate: GATE,
    prefix: 'icon-canon-fx',
    dirs: [GENERATED, 'src/canonical', 'src/components'],
    files: { ...files, ...(o.files ?? {}) },
  })
}

describe('audit:icon-canonical-names — 绿档 + 自印非空过', () => {
  it('全用 canonical 名 → exit 0，且自印的两个数是 fixture 自己的（≠ 真仓库 ⇒ 非空过）', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><Icon name="action/close" /></template>\n' } }),
      GATE,
    )
    // 真仓库是几百个 canonical name —— 「3 canonical names, 2 aliases」不可能对上。
    expectGateGreen(run, {
      contains: [
        'audit-icon-canonical-names: 3 canonical names, 2 aliases; scanned 2 dirs',
        'OK — every static <Icon name> uses its canonical registry name.',
      ],
    })
  })
})

describe('audit:icon-canonical-names — 判据 A：用了已注册 alias', () => {
  it('`name="close"` → 红，A 段点名 file:line 并给出该换成哪个 canonical', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template>\n  <Icon name="close" />\n</template>\n' } }),
      GATE,
    )
    expectGateRed(run, {
      marker: '1 non-canonical <Icon name> reference(s):',
      checks: [
        'A. Alias used — replace with canonical name:',
        'src/canonical/FxIconProbe.vue:2',
        'name="close"  →  name="action/close"',
      ],
    })
  })

  it('同一份 fixture 只把 alias 换成 canonical → 绿（致败源就是那一处）', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template>\n  <Icon name="action/close" />\n</template>\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static <Icon name> uses its canonical registry name.'] })
  })
})

describe('audit:icon-canonical-names — 判据 B：未注册的名字', () => {
  it('既非 canonical 也非 alias → 红，落在 B 段（与 A 段分开）', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template>\n  <Icon name="totally-made-up" />\n</template>\n' } }),
      GATE,
    )
    expectGateRed(run, {
      checks: [
        'B. Unknown name (not a registered canonical or alias — verify it resolves):',
        'name="totally-made-up"',
      ],
    })
    expect(run.stdout).not.toContain('A. Alias used')
  })
})

describe('audit:icon-canonical-names — ⛔ 不该判的 name 形态（must-not-hit）', () => {
  it('动态绑定 `:name=` 不判（静态不可解析）', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><Icon :name="whatever" /></template>\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static'] })
  })

  it('`v-bind:name=` 形态同样不判（`(?<!:)` 守卫）', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><Icon v-bind:name="x" /></template>\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static'] })
  })

  it('值里含 `?`（三元表达式痕迹）不判', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><Icon name="a?b" /></template>\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static'] })
  })

  it('值里含空格不判', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><Icon name="two words" /></template>\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static'] })
  })

  it('非 `<Icon>` 标签上的 name=（如 `<slot name="footer">`）不判', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><slot name="footer" /><div name="x" /></template>\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static'] })
  })
})

describe('audit:icon-canonical-names — catalog 通路（两条都测）', () => {
  it('catalog manifest 存在时，catalog 名放行（runtime loadCatalogIcon 解析）', () => {
    const run = runGate(
      build({
        canonical: { 'FxIconProbe.vue': '<template><Icon name="arrow/down" /></template>\n' },
        catalog: ['arrow/down'],
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static'] })
  })

  it('⛔ must-HIT 对照：同一个名字在 catalog **不**存在时照样报 B（放行来自 catalog，不是名字长得像）', () => {
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><Icon name="arrow/down" /></template>\n' } }),
      GATE,
    )
    expectGateRed(run, { checks: ['B. Unknown name', 'name="arrow/down"'] })
  })

  it('catalog manifest 整个不存在 → 不崩、判据照跑（`catalog 可选` 的 try/catch）', () => {
    // ⚠️ 这也是 F138 harness「⛔ 别把 dist 软链进 fixture」那条设计判据的现场：
    // fixture root 里没有 dist/，闸必须自己走可选分支。
    const run = runGate(
      build({ canonical: { 'FxIconProbe.vue': '<template><Icon name="action/close" /></template>\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['3 canonical names'] })
  })

  it('catalog manifest 是坏 JSON → 同样走 catch，不崩（fail-open 但如实登记）', () => {
    // ⚠️ 钉的是**现行行为**：坏 catalog 被当成「没有 catalog」，于是 catalog 名会报 B。
    // ⛔ 别读成「坏 JSON 无所谓」—— 它的可见后果是多报，不是漏报，方向安全。
    const run = runGate(
      build({
        canonical: { 'FxIconProbe.vue': '<template><Icon name="arrow/down" /></template>\n' },
        files: { 'dist/icons/manifest.json': '{ not json' },
      }),
      GATE,
    )
    expectGateRed(run, { checks: ['B. Unknown name', 'name="arrow/down"'] })
  })
})

describe('audit:icon-canonical-names — 扫描面', () => {
  it('递归子目录（`src/canonical/sub/X.vue` 也扫）', () => {
    const run = runGate(
      build({ canonical: { 'sub/Deep.vue': '<template><Icon name="close" /></template>\n' } }),
      GATE,
    )
    expectGateRed(run, { checks: ['src/canonical/sub/Deep.vue:1'] })
  })

  it('`src/components/` 也在扫描面（两个目录都算）', () => {
    const run = runGate(
      build({ components: { 'Base.vue': '<template><Icon name="close" /></template>\n' } }),
      GATE,
    )
    expectGateRed(run, { checks: ['src/components/Base.vue:1'] })
  })

  it('扫描目录不存在 → vueFiles 返回空、不崩（分母缩小但不假红）', () => {
    const root = createGateFixture({
      gate: GATE,
      prefix: 'icon-canon-fx-nodirs',
      dirs: [GENERATED],
      files: { [`${GENERATED}/a.ts`]: registry(DEFAULT_REGISTRY) },
    })
    const run = runGate(root, GATE)
    expectGateGreen(run, { contains: ['3 canonical names, 2 aliases; scanned 2 dirs', 'OK — every static'] })
  })

  it('非 .vue 文件不扫（`.md` 里的同形态标签不算）', () => {
    const run = runGate(
      build({ files: { 'src/canonical/notes.md': '<Icon name="close" />\n' } }),
      GATE,
    )
    expectGateGreen(run, { contains: ['OK — every static'] })
  })
})

describe('audit:icon-canonical-names — 接线钉（判据 → 退出码 → 点名输出）', () => {
  it('A + B 同时出现 → 两段都印、总数正确、退出码 1（接线不吞任一类）', () => {
    const run = runGate(
      build({
        canonical: { 'FxIconProbe.vue': '<template>\n  <Icon name="close" />\n  <Icon name="nope-nope" />\n</template>\n' },
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stdout).toContain('2 non-canonical <Icon name> reference(s):')
    expect(run.stdout).toContain('A. Alias used')
    expect(run.stdout).toContain('src/canonical/FxIconProbe.vue:2')
    expect(run.stdout).toContain('B. Unknown name')
    expect(run.stdout).toContain('src/canonical/FxIconProbe.vue:3')
    expect(run.stdout).toContain('Fix: replace each alias with its canonical name')
  })

  it('多份文件的 finding 全部印出（不止报第一份）', () => {
    const run = runGate(
      build({
        canonical: { 'FxIconProbe.vue': '<template><Icon name="close" /></template>\n' },
        components: { 'Base.vue': '<template><Icon name="arrow-down" /></template>\n' },
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stdout).toContain('2 non-canonical')
    expect(run.stdout).toContain('src/canonical/FxIconProbe.vue:1')
    expect(run.stdout).toContain('src/components/Base.vue:1')
  })

  it('别名表按首次出现胜出（同一 alias 被两个 canonical 声明时不改判据、只影响建议值）', () => {
    const run = runGate(
      build({
        entries: [
          { name: 'action/close', aliases: ['close'] },
          { name: 'status/close', aliases: ['close'] },
        ],
        canonical: { 'FxIconProbe.vue': '<template><Icon name="close" /></template>\n' },
      }),
      GATE,
    )
    expectGateRed(run, { checks: ['name="close"  →  name="action/close"'] })
  })

  it('registry 目录整个不存在 → 非零崩溃（fail-closed），⛔ 不是静默 exit 0', () => {
    // ⚠️ 如实登记：readdirSync(GENERATED_DIR) 无 try/catch，抛 ENOENT 的**崩溃式** fail-closed。
    // 与上面「扫描目录不存在」那条对照 —— 那侧有 try/catch，这侧没有，两种分母缺失的处置不同。
    const root = createGateFixture({ gate: GATE, prefix: 'icon-canon-fx-noreg' })
    const run = runGate(root, GATE)
    expect(run.status).not.toBe(0)
    expect(`${run.stderr}${run.stdout}`).toContain('ENOENT')
  })
})
