// tests/audit-canonical-slot-guard.test.ts
// -----------------------------------------------------------------------------
// `audit:canonical-slot-guard`（L4 pre-commit + L5 gitea-pr-checks）的**整脚本**回归面。
//
// 为什么是整脚本而不是 import 判据函数：该闸 84 行**零导出**、判据写在顶层，
// 结构上没有可 import 的符号。量具 `pnpm report:gate-regression-face` 把它记为
// 「零判据覆盖 · P1/P2 双 PASS」。本文件是那条判定的兑现（[[INFRA-F138]]）。
// ⛔ 闸本体一行没改。
//
// ⚠️ 这条闸的**绿档输出里没有任何数字**（只有一句固定的 `✓ …`），所以 harness 判据 3
// 「绿档要钉 fixture 自己的读数」在这里没有直接落点。替代的非空过凭据 = **红档点名的
// 文件路径**：fixture 里那些 `.vue` 的名字在真仓库 `src/canonical/` 下不存在，
// 跑错了树不可能印出来。每个绿档用例都与一个「同一份 fixture 只改一行就转红并点名它」
// 的配对用例同在 —— ⛔ 别把绿档单独读成证据。
//
// 覆盖：2 类判据（`$slots.` / `useSlots(`）× 致败 + 三种注释剥离的 must-not-hit
// + `http://` 守卫的 must-hit 对照 + 扫描面边界（子目录 / 非 .vue / src/components）
// + 3 条接线钉。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-canonical-slot-guard.mjs'
const CANON = 'src/canonical'
const GREEN = '✓ audit-canonical-slot-guard: no CE-unsafe $slots/useSlots in src/canonical/*.vue'

afterEach(cleanupGateFixtures)

/** fixture 专属组件名 —— 真仓库 src/canonical/ 下不存在，红档点名它 = 跑对了树的凭据。 */
const PROBE = 'FxSlotProbe.vue'

function build(files: Record<string, string>) {
  return createGateFixture({
    gate: GATE,
    prefix: 'slot-guard-fx',
    dirs: [CANON],
    files,
  })
}

/** 一个 CE-safe 的干净组件（用 useHasSlot，闸放行的那条正路）。 */
const CLEAN = `<template>
  <div class="fx"><slot name="footer" /></div>
</template>
<script setup lang="ts">
import { useHasSlot } from './composables/useHasSlot'
const hasFooter = useHasSlot('footer')
</script>
`

describe('audit:canonical-slot-guard — 绿档（配对的红档点名 fixture 路径 = 非空过凭据）', () => {
  it('canonical 全走 useHasSlot → exit 0', () => {
    const run = runGate(build({ [`${CANON}/${PROBE}`]: CLEAN }), GATE)
    expectGateGreen(run, { contains: [GREEN] })
  })

  it('同一份 fixture 只把 useHasSlot 换成 $slots → 转红并点名 **fixture 自己的**路径', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: CLEAN.replace("useHasSlot('footer')", '!!$slots.footer') }),
      GATE,
    )
    expectGateRed(run, {
      marker: '❌ CE-unsafe slot detection in canonical component(s) [INFRA-F54]:',
      checks: [`src/canonical/${PROBE}:`],
    })
  })
})

describe('audit:canonical-slot-guard — 判据 (1) $slots.', () => {
  it('模板里 `v-if="$slots.footer"` → 红且给 $slots 那条 hint', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<template>\n  <div v-if="$slots.footer"><slot name="footer" /></div>\n</template>\n` }),
      GATE,
    )
    expectGateRed(run, {
      checks: [
        `src/canonical/${PROBE}:2`,
        '$slots.footer',
        '`$slots.x` is always empty under defineCustomElement',
      ],
    })
  })

  it('⛔ must-not-hit：`$slots` 不带点（如 `Object.keys($slots)`）不报 —— 判据是 `$slots.`', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<script setup>\nconst n = Object.keys($slots).length\n</script>\n` }),
      GATE,
    )
    expectGateGreen(run, { contains: [GREEN] })
  })
})

describe('audit:canonical-slot-guard — 判据 (2) useSlots(', () => {
  it('`useSlots()` → 红且给 useSlots 那条 hint（与 $slots 的 hint 不同）', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<script setup>\nimport { useSlots } from 'vue'\nconst s = useSlots()\n</script>\n` }),
      GATE,
    )
    expectGateRed(run, {
      checks: [
        '`useSlots()` does not reflect light-DOM slotted children in a CE',
      ],
    })
    expect(run.stderr).not.toContain('always empty under defineCustomElement')
  })

  it('`useSlots ()` 带空格照样报（判据是 `useSlots\\s*\\(`，不是逐字匹配）', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<script setup>\nconst s = useSlots ()\n</script>\n` }),
      GATE,
    )
    expectGateRed(run, { checks: [`src/canonical/${PROBE}:2`] })
  })
})

describe('audit:canonical-slot-guard — 注释剥离（三种形态都必须 must-not-hit）', () => {
  it('HTML 注释里的 $slots 不报（文档里解释这个 bug 的那些注释）', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<template>\n  <!-- 别用 $slots.footer：CE 下恒空 -->\n  <slot name="footer" />\n</template>\n` }),
      GATE,
    )
    expectGateGreen(run, { contains: [GREEN] })
  })

  it('JS 块注释 /* */ 里的 useSlots( 不报', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<script setup>\n/* 历史写法：useSlots() —— 已废弃 */\nconst x = 1\n</script>\n` }),
      GATE,
    )
    expectGateGreen(run, { contains: [GREEN] })
  })

  it('JS 行注释 // 里的 $slots. 不报', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<script setup>\n// 曾经是 $slots.footer\nconst x = 1\n</script>\n` }),
      GATE,
    )
    expectGateGreen(run, { contains: [GREEN] })
  })

  it('⛔ must-HIT 对照：`https://` 里的 `//` 不被当行注释 ⇒ 同行后面的 $slots. 照样报', () => {
    // stripComments 的行注释正则带 `(^|[^:])` 守卫，专为不吃掉 URL 而写。
    // 若那个守卫被删，本行会被整行剥掉、判据静默失明 —— 这条用例就是它的钉。
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<script setup>\nconst u = 'https://x.example'; const has = !!$slots.footer\n</script>\n` }),
      GATE,
    )
    expectGateRed(run, { checks: [`src/canonical/${PROBE}:2`, '$slots.footer'] })
  })
})

describe('audit:canonical-slot-guard — 扫描面边界', () => {
  it('⛔ must-not-hit：`src/canonical/composables/` 子目录不被扫（useHasSlot 自己就住那儿）', () => {
    const run = runGate(
      build({
        [`${CANON}/${PROBE}`]: CLEAN,
        [`${CANON}/composables/useHasSlot.ts`]: `export function useHasSlot(){ return !!$slots.x }\n`,
      }),
      GATE,
    )
    expectGateGreen(run, { contains: [GREEN] })
  })

  it('⛔ must-not-hit：`src/components/` 里的 useSlots() 合法（base 组件永远是嵌套的，不是 CE 根）', () => {
    const run = runGate(
      build({
        [`${CANON}/${PROBE}`]: CLEAN,
        'src/components/Base.vue': `<script setup>\nconst s = useSlots()\n</script>\n`,
      }),
      GATE,
    )
    expectGateGreen(run, { contains: [GREEN] })
  })

  it('⛔ must-not-hit：canonical 目录下的非 .vue 文件不被扫', () => {
    const run = runGate(
      build({
        [`${CANON}/${PROBE}`]: CLEAN,
        [`${CANON}/notes.md`]: '示例：`v-if="$slots.footer"`\n',
      }),
      GATE,
    )
    expectGateGreen(run, { contains: [GREEN] })
  })
})

describe('audit:canonical-slot-guard — 接线钉（判据 → 退出码 → 点名输出）', () => {
  it('多文件多处违例 → 全部逐条印出（接线不吞 findings、不止报第一条）', () => {
    const run = runGate(
      build({
        [`${CANON}/${PROBE}`]: `<template>\n  <div v-if="$slots.a" />\n</template>\n`,
        [`${CANON}/FxSlotProbe2.vue`]: `<script setup>\nconst s = useSlots()\n</script>\n`,
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stderr).toContain(`src/canonical/${PROBE}:2`)
    expect(run.stderr).toContain('src/canonical/FxSlotProbe2.vue:2')
    expect(run.stderr).toContain('Fix: use the CE-safe `useHasSlot()` composable')
  })

  it('同一份文件里两处违例都报（逐行扫描，不是每文件只记一条）', () => {
    const run = runGate(
      build({ [`${CANON}/${PROBE}`]: `<script setup>\nconst a = $slots.x\nconst b = useSlots()\n</script>\n` }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stderr).toContain(`src/canonical/${PROBE}:2`)
    expect(run.stderr).toContain(`src/canonical/${PROBE}:3`)
  })

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

  it('空 canonical 目录 → exit 0（分母为空即绿，如实登记的空过面）', () => {
    // ⚠️ 这条钉的是**现行行为**，不是背书：0 个 .vue 时闸印绿。
    // 真仓库里该目录恒非空，且 `audit:export-coverage` 等另有分母守卫；
    // ⛔ 别把这条读成「分母塌了也没关系」。
    const run = runGate(build({}), GATE)
    expectGateGreen(run, { contains: [GREEN] })
  })
})
