// tests/audit-export-coverage.test.ts
// -----------------------------------------------------------------------------
// `audit:export-coverage`（L4 pre-commit + L5 gitea-pr-checks 两挂）的**整脚本**回归面。
//
// 为什么是整脚本而不是 import 判据函数：该闸零导出、判据在 `main()` 里，
// 量具 `pnpm report:gate-regression-face` 此前把它记在「零判据覆盖」且标 `P1=FAIL(cwd)`。
// ⛔ 闸本体一行没改。
//
// 🔴 与 `tests/audit-doc-de-mirror.test.ts` 同批，兑现同一条证伪（[[INFRA-F138]]，2026-08-26）：
//   `P1=FAIL(cwd)` **不是** fixture-root harness 的封路条件 —— `runGate` 本就传 `cwd: root`
//   （`tests/lib/gate-fixture-root.ts:131`），于是 `ROOT = process.cwd()` 的闸在 fixture 下
//   读的全是 fixture。理由与三向实证写在那份文件的头注释里，此处不复述第二份。
//
// ✅ 本闸的绿档**能**自证非空过（与 de-mirror 那条不同）：它自印
//   `scanned N component .vue files; M files reachable …`，两个数都是 fixture 自己的，
//   真仓库不可能同时对上 ⇒ 直接钉这两个数即可。
//
// 覆盖：可达性传递 / 双入口 union / 孤儿检出 / allowlist / resolveImport 分支
//       + 接线钉（entry 缺失 fail-closed · 逐条印出 · 计数 · 退出码 · 流向）。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-export-coverage.mjs'

const INDEX = 'src/index.ts'
const CHART = 'src/chart.ts'

/** 极简 SFC —— 闸只读 import 语句，模板内容不参与判据。 */
const sfc = (imports = '') => `<script setup>\n${imports}</script>\n<template><div /></template>\n`

/**
 * 基线 fixture：3 个组件 .vue，全部可达。
 *   index.ts → canonical/Alpha.vue → components/AlphaBase/AlphaBase.vue（传递可达，测「base 无需 allowlist」）
 *   chart.ts → canonical/Beta.vue                                      （第二入口 union）
 * ⇒ 自印 `scanned 3 … 5 files reachable`（5 = 2 entry + 3 vue）
 */
function baseFiles(): Record<string, string> {
  return {
    [INDEX]: `import Alpha from './canonical/Alpha.vue'\nexport { Alpha }\n`,
    [CHART]: `import Beta from './canonical/Beta.vue'\nexport { Beta }\n`,
    'src/canonical/Alpha.vue': sfc(`import AlphaBase from '../components/AlphaBase/AlphaBase.vue'\n`),
    'src/canonical/Beta.vue': sfc(),
    'src/components/AlphaBase/AlphaBase.vue': sfc(),
  }
}

function build(overrides: Record<string, string> = {}, opts: { drop?: string[] } = {}) {
  const files = { ...baseFiles(), ...overrides }
  for (const k of opts.drop ?? []) delete files[k]
  return createGateFixture({ gate: GATE, prefix: 'export-cov-fx', files })
}

const SUMMARY = '[audit:export-coverage] scanned'
const PASS_MARK = '✅ PASS — every component is reachable'

afterEach(cleanupGateFixtures)

describe('audit:export-coverage — 绿档 + 自印非空过', () => {
  it('全部可达 → exit 0，且自印的两个数是 fixture 自己的（≠ 真仓库 ⇒ 非空过凭据）', () => {
    const run = runGate(build(), GATE)
    expectGateGreen(run, {
      contains: [
        'scanned 3 component .vue files',
        '5 files reachable from public entries [src/index.ts, src/chart.ts]',
        PASS_MARK,
      ],
    })
  })

  it('自印会随 fixture 变化（再加一个可达组件 → 3→4 / 5→6，读数不是写死的）', () => {
    const run = runGate(
      build({
        [INDEX]: `import Alpha from './canonical/Alpha.vue'\nimport Gamma from './canonical/Gamma.vue'\nexport { Alpha, Gamma }\n`,
        'src/canonical/Gamma.vue': sfc(),
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['scanned 4 component .vue files', '6 files reachable'] })
  })
})

describe('audit:export-coverage — 判据：孤儿检出', () => {
  it('canonical 下的组件无人 import → 红，点名它的名字与路径', () => {
    const run = runGate(build({ 'src/canonical/Orphan.vue': sfc() }), GATE)
    expectGateRed(run, {
      marker: '1 component(s) exist but are NOT reachable',
      checks: ['- Orphan  (src/canonical/Orphan.vue)'],
    })
  })

  it('components 下的组件同样在扫描面（两个目录都扫，不是只扫 canonical）', () => {
    const run = runGate(build({ 'src/components/Lonely/Lonely.vue': sfc() }), GATE)
    expectGateRed(run, { checks: ['- Lonely  (src/components/Lonely/Lonely.vue)'] })
  })

  it('递归子目录里的 .vue 也被列举（listFiles 是递归的，不是只看一层）', () => {
    const run = runGate(build({ 'src/components/Deep/Nested/Buried.vue': sfc() }), GATE)
    expectGateRed(run, { checks: ['- Buried  (src/components/Deep/Nested/Buried.vue)'] })
  })
})

describe('audit:export-coverage — 判据：可达性是传递的（原则 8「单一信号」）', () => {
  it('⛔ must-not-hit：base 层只被 canonical wrapper 引用 → 传递可达，不报孤儿、不需要 allowlist', () => {
    // 基线 fixture 里 AlphaBase 就是这个形态：index.ts 不直接引它。
    const run = runGate(build(), GATE)
    expectGateGreen(run, { contains: [PASS_MARK] })
    expect(run.stderr).not.toContain('AlphaBase')
  })

  it('三跳传递（index → Alpha → AlphaBase → Deeper）也可达', () => {
    const run = runGate(
      build({
        'src/components/AlphaBase/AlphaBase.vue': sfc(`import Deeper from './Deeper.vue'\n`),
        'src/components/AlphaBase/Deeper.vue': sfc(),
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['scanned 4 component .vue files', PASS_MARK] })
  })

  it('把那一跳断开 → 立刻报孤儿（证明上一条的绿是传递可达，不是漏判）', () => {
    const run = runGate(
      build({
        'src/components/AlphaBase/AlphaBase.vue': sfc(),
        'src/components/AlphaBase/Deeper.vue': sfc(),
      }),
      GATE,
    )
    expectGateRed(run, { checks: ['- Deeper  (src/components/AlphaBase/Deeper.vue)'] })
  })

  it('第二入口 chart.ts 的可达面并入 union（只被它引到的组件不算孤儿 —— INFRA-F61 那条）', () => {
    // 基线里 Beta 只被 chart.ts 引用。断开 chart.ts 的那一行 ⇒ Beta 变孤儿。
    const run = runGate(build({ [CHART]: `export const chartMarker = 1\n` }), GATE)
    expectGateRed(run, { checks: ['- Beta  (src/canonical/Beta.vue)'] })
  })

  it('⛔ must-not-hit：bare/package import 不进图也不崩溃（只跟相对路径）', () => {
    const run = runGate(
      build({
        [INDEX]: `import { ref } from 'vue'\nimport Alpha from './canonical/Alpha.vue'\nexport { Alpha, ref }\n`,
      }),
      GATE,
    )
    expectGateGreen(run, { contains: [PASS_MARK] })
  })
})

describe('audit:export-coverage — 判据：INTERNAL_ALLOWLIST', () => {
  it('孤儿的**文件名**在 allowlist 里 → 放行（用闸内既有的一个名字）', () => {
    const run = runGate(build({ 'src/canonical/MenuList.vue': sfc() }), GATE)
    expectGateGreen(run, { contains: ['scanned 4 component .vue files', PASS_MARK] })
  })

  it('PASS 行逐字列出 allowlist 成员（豁免是自文档化的，不是静默跳过）', () => {
    const run = runGate(build(), GATE)
    expectGateGreen(run, { contains: ['allowlisted internal [Badge, Select, Logo, MenuList, UserMenu]'] })
  })

  it('如实登记的宽松边界：allowlist 按文件名匹配、**不看路径** —— 换个目录同名照样放行', () => {
    const run = runGate(build({ 'src/components/Whatever/MenuList.vue': sfc() }), GATE)
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('名字差一个字母就不再豁免（证明上一条的绿来自 allowlist 命中，不是恒绿）', () => {
    const run = runGate(build({ 'src/canonical/MenuLists.vue': sfc() }), GATE)
    expectGateRed(run, { checks: ['- MenuLists  (src/canonical/MenuLists.vue)'] })
  })
})

describe('audit:export-coverage — 判据：resolveImport 的候选分支', () => {
  it('无扩展名的 import 解析到 `.vue`（`./canonical/Alpha` → `Alpha.vue`）', () => {
    const run = runGate(
      build({ [INDEX]: `import Alpha from './canonical/Alpha'\nexport { Alpha }\n` }),
      GATE,
    )
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('目录桶解析到 `index.ts`（`./canonical` → `canonical/index.ts`）', () => {
    const run = runGate(
      build({
        [INDEX]: `export * from './canonical'\n`,
        'src/canonical/index.ts': `export { default as Alpha } from './Alpha.vue'\n`,
      }),
      GATE,
    )
    expectGateGreen(run, { contains: [PASS_MARK] })
  })

  it('解析不到的相对 import 被跳过，不崩溃（其余判据照常出结论）', () => {
    const run = runGate(
      build({
        [INDEX]: `import Gone from './canonical/DoesNotExist.vue'\nimport Alpha from './canonical/Alpha.vue'\nexport { Alpha, Gone }\n`,
      }),
      GATE,
    )
    expectGateGreen(run, { contains: [PASS_MARK] })
  })
})

describe('audit:export-coverage — 接线钉（fail-closed → 计数 → 退出码 → 流向）', () => {
  it('主入口 src/index.ts 缺失 → exit 1 且报 entry not found，⛔ 不是「零组件所以 PASS」', () => {
    const run = runGate(build({}, { drop: [INDEX] }), GATE)
    expectGateRed(run, { marker: '[audit:export-coverage] FAIL — entry not found: src/index.ts' })
    expect(run.stdout).not.toContain(PASS_MARK)
  })

  it('第二入口 src/chart.ts 缺失也 fail-closed（两个 entry 都检查，不是只检查第一个）', () => {
    const run = runGate(build({}, { drop: [CHART] }), GATE)
    expectGateRed(run, { marker: 'entry not found: src/chart.ts' })
  })

  it('多个孤儿 → 逐条印出且计数正确（接线不吞 findings）', () => {
    const run = runGate(
      build({
        'src/canonical/OrphanA.vue': sfc(),
        'src/canonical/OrphanB.vue': sfc(),
        'src/components/OrphanC/OrphanC.vue': sfc(),
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('3 component(s) exist but are NOT reachable')
    expect(run.stderr).toContain('- OrphanA')
    expect(run.stderr).toContain('- OrphanB')
    expect(run.stderr).toContain('- OrphanC')
  })

  it('孤儿清单走 stderr、扫描摘要走 stdout（红档也照印摘要 ⇒ 分母可核）', () => {
    const run = runGate(build({ 'src/canonical/Orphan.vue': sfc() }), GATE)
    expect(run.status).toBe(1)
    expect(run.stdout).toContain(SUMMARY)
    expect(run.stdout).toContain('scanned 4 component .vue files')
    expect(run.stderr).toContain('NOT reachable')
  })
})
