import { describe, it, expect } from 'vitest'
import { mkdtempSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import {
  compareSemver,
  parsePeerFloor,
  extractNamedImports,
  scanImports,
  evaluate,
  VUE_API_SINCE,
  API_TABLES,
  TABLE_META,
  UNKNOWN_IS_BLOCKING,
  SCAN_DIR,
  peerSpecifiersIn,
} from '../scripts/audit-framework-api-floor.mjs'

const REPO_ROOT = resolve(__dirname, '..')

/** 造一个临时扫描面，让 scanImports 能被真 fs 驱动。 */
function fixtureRoot(files: Record<string, string>): string {
  const root = mkdtempSync(join(tmpdir(), 'api-floor-'))
  for (const [rel, body] of Object.entries(files)) {
    const abs = join(root, rel)
    mkdirSync(resolve(abs, '..'), { recursive: true })
    writeFileSync(abs, body)
  }
  return root
}

const okFloor = { ok: true as const, floor: '3.5.0', raw: '^3.5.0' }
const scanOf = (pairs: Array<[string, string]>) => ({
  ok: true as const,
  files: 10,
  statements: pairs.length,
  imports: new Map(pairs.map(([name, file]) => [name, { kind: 'value', files: new Set([file]) }])),
})

describe('compareSemver', () => {
  it('orders versions', () => {
    expect(compareSemver('3.5.0', '3.4.0')).toBe(1)
    expect(compareSemver('3.4.9', '3.5.0')).toBe(-1)
    expect(compareSemver('3.5.0', '3.5.0')).toBe(0)
    expect(compareSemver('3.10.0', '3.9.0')).toBe(1) // 数字比较, 不是字符串比较
  })

  it('throws on non X.Y.Z input instead of silently returning 0', () => {
    // 脏输入静默变成"相等"是假绿的经典入口。
    expect(() => compareSemver('3.5', '3.5.0')).toThrow()
    expect(() => compareSemver('^3.5.0', '3.5.0')).toThrow()
    expect(() => compareSemver('', '3.5.0')).toThrow()
  })
})

describe('parsePeerFloor — 认得的形态', () => {
  it.each([
    ['^3.5.0', '3.5.0'],
    ['~3.5.0', '3.5.0'],
    ['>=3.5.0', '3.5.0'],
    ['=3.5.0', '3.5.0'],
    ['3.5.0', '3.5.0'],
    ['>=3.5.0 <4.0.0', '3.5.0'],
    ['  ^3.4.0  ', '3.4.0'],
  ])('%s → 下界 %s', (range, floor) => {
    const r = parsePeerFloor(range)
    expect(r.ok).toBe(true)
    expect(r.floor).toBe(floor)
  })
})

describe('parsePeerFloor — 形态没见过时 fail closed（不是放行）', () => {
  it.each([
    [''],
    ['   '],
    ['*'],
    ['3.5'],
    ['^3'],
    ['latest'],
    ['^3.5.0 || ^2.0.0'], // 多段范围: 下界不唯一, 不猜
  ])('%s → ok:false', (range) => {
    const r = parsePeerFloor(range as string)
    expect(r.ok).toBe(false)
    expect(typeof r.reason).toBe('string')
  })

  it('非字符串输入也 fail closed', () => {
    for (const bad of [undefined, null, 3.5, {}, []]) {
      // @ts-expect-error 刻意喂脏输入
      expect(parsePeerFloor(bad).ok).toBe(false)
    }
  })
})

describe('extractNamedImports', () => {
  it('单行具名 import', () => {
    const got = extractNamedImports(`import { ref, computed } from 'vue'`)
    expect(got.map((g) => g.name)).toEqual(['ref', 'computed'])
    expect(got.every((g) => g.kind === 'value')).toBe(true)
  })

  it('跨行 import（SFC 里很常见）', () => {
    const got = extractNamedImports(`import {\n  ref,\n  useId,\n  watch,\n} from 'vue'`)
    expect(got.map((g) => g.name)).toEqual(['ref', 'useId', 'watch'])
  })

  it('as 改名时记的是"被 import 的名字"而不是本地名', () => {
    const got = extractNamedImports(`import { useId as makeId } from 'vue'`)
    expect(got).toEqual([{ name: 'useId', kind: 'value' }])
  })

  it('import type {} 与内联 type 都标成 type', () => {
    const a = extractNamedImports(`import type { Ref, App } from 'vue'`)
    expect(a.every((x) => x.kind === 'type')).toBe(true)
    const b = extractNamedImports(`import { ref, type Ref } from 'vue'`)
    expect(b).toEqual([
      { name: 'ref', kind: 'value' },
      { name: 'Ref', kind: 'type' },
    ])
  })

  it('default / namespace import 有各自的占位名', () => {
    expect(extractNamedImports(`import Vue from 'vue'`)).toEqual([{ name: 'default', kind: 'default' }])
    expect(extractNamedImports(`import * as vue from 'vue'`)).toEqual([{ name: '*', kind: 'namespace' }])
    expect(extractNamedImports(`import Vue, { ref } from 'vue'`).map((g) => g.name)).toEqual(['default', 'ref'])
  })

  it('别的包不算，双引号也认', () => {
    expect(extractNamedImports(`import { useId } from 'vue-router'`)).toEqual([])
    expect(extractNamedImports(`import { useId } from "vue"`).map((g) => g.name)).toEqual(['useId'])
  })

  it('单文件里多条 import 都收', () => {
    const src = `import { ref } from 'vue'\nimport type { Ref } from 'vue'\nconst x = 1\n`
    expect(extractNamedImports(src).map((g) => g.name).sort()).toEqual(['Ref', 'ref'])
  })

  it('动态 import 抓不到 —— 覆盖面声明里承认过，这里把它钉成已知边界', () => {
    expect(extractNamedImports(`const { useId } = await import('vue')`)).toEqual([])
  })
})

describe('peerSpecifiersIn — 产物是否真从 consumer 解析 peer（S6 的判据函数）', () => {
  it('认得 from / 动态 import / require 三种 specifier 位置', () => {
    expect(peerSpecifiersIn(`import { ref } from "vue"`, 'vue')).toEqual(['vue'])
    expect(peerSpecifiersIn(`import x from 'vue'`, 'vue')).toEqual(['vue'])
    expect(peerSpecifiersIn(`const m = await import('vue')`, 'vue')).toEqual(['vue'])
    expect(peerSpecifiersIn(`const m = require("vue")`, 'vue')).toEqual(['vue'])
  })

  it('子路径算消费，同名前缀不算', () => {
    expect(peerSpecifiersIn(`import x from 'vue/dist/vue.esm-bundler.js'`, 'vue')).toEqual([
      'vue/dist/vue.esm-bundler.js',
    ])
    expect(peerSpecifiersIn(`import x from 'vuex'`, 'vue')).toEqual([])
    expect(peerSpecifiersIn(`import x from 'vue-router'`, 'vue')).toEqual([])
  })

  // ⚠️ 这条是整个设计的理由。dist-wc 的文件头逐字是：
  //     /**\n * @vue/shared v3.5.32\n * (c) 2018-present Yuxi (Evan) You ...
  // 全文 grep 'vue' 会命中它，从而把一份**把 Vue 内联打包了**的产物判成「消费 peer」——
  // 结论与事实完全相反。判据只认 specifier 位置正是为了躲开这个。
  it('不被版权注释里的包名骗到（这正是 dist-wc 的实际形态）', () => {
    const header = `/**\n * @vue/shared v3.5.32\n * (c) 2018-present Yuxi (Evan) You\n **/\nconst x = 1`
    expect(peerSpecifiersIn(header, 'vue')).toEqual([])
    // 阴性对照：同一份文本里真放一个 specifier，就必须命中 —— 否则上面那个 [] 是空过。
    expect(peerSpecifiersIn(`${header}\nimport { ref } from 'vue'`, 'vue')).toEqual(['vue'])
  })

  it('去重 + 排序，多处 import 只算一个 specifier', () => {
    const src = `import {a} from 'vue'\nimport {b} from 'vue'\nimport c from 'vue/jsx-runtime'`
    expect(peerSpecifiersIn(src, 'vue')).toEqual(['vue', 'vue/jsx-runtime'])
  })

  it('包名里的正则元字符被转义（scoped 包名含 /、点号等）', () => {
    expect(peerSpecifiersIn(`import x from '@vue/server-renderer'`, '@vue/server-renderer')).toEqual(
      ['@vue/server-renderer'],
    )
    // `.` 若没转义会变成任意字符，把 'vueXjs' 也匹配上。
    expect(peerSpecifiersIn(`import x from 'vueXjs'`, 'vue.js')).toEqual([])
  })
})

describe('scanImports — 分母 fail closed', () => {
  it('扫描面不存在 → ok:false 并给理由', () => {
    const r = scanImports(fixtureRoot({ 'placeholder.txt': 'x' }), 'vue', 'no-such-dir')
    expect(r.ok).toBe(false)
    expect(r.reason).toContain('no-such-dir')
  })

  it('真扫一个 fixture：文件数 / 语句数 / 符号数都对得上', () => {
    const root = fixtureRoot({
      'src/A.vue': `<script setup lang="ts">\nimport { ref, useId } from 'vue'\n</script>`,
      'src/nested/B.ts': `import type { Ref } from 'vue'\nexport const x = 1`,
      'src/C.ts': `export const noVueHere = 1`,
      'src/D.md': `import { useId } from 'vue'`, // 扩展名不在扫描面内, 不该被算进去
    })
    const r = scanImports(root, 'vue', 'src')
    expect(r.ok).toBe(true)
    expect(r.files).toBe(3) // .vue + 2×.ts, .md 不算
    expect(r.statements).toBe(2) // C.ts 没有 vue import
    expect([...r.imports.keys()].sort()).toEqual(['Ref', 'ref', 'useId'])
  })
})

describe('evaluate — S1 too-new（闸的本体）', () => {
  const base = { table: VUE_API_SINCE, tableMeta: TABLE_META, today: '2026-07-31' }

  it('下界 3.4.0 + 代码用 useId → 必须报 too-new（这正是 INFRA-F85 的病灶）', () => {
    const r = evaluate({
      ...base,
      floorResult: { ok: true, floor: '3.4.0', raw: '^3.4.0' },
      scan: scanOf([['useId', 'src/components/Tooltip/Tooltip.vue'], ['ref', 'src/x.ts']]),
    })
    expect(r.tooNew.map((t) => t.name)).toEqual(['useId'])
    expect(r.tooNew[0].since).toBe('3.5.0')
    expect(r.tooNew[0].files).toEqual(['src/components/Tooltip/Tooltip.vue'])
    expect(r.tooNew[0].source).toMatch(/vue-3-5/)
  })

  it('下界 3.5.0 + 同样的 useId → 0（修完就该绿）', () => {
    const r = evaluate({
      ...base,
      floorResult: okFloor,
      scan: scanOf([['useId', 'src/components/Tooltip/Tooltip.vue']]),
    })
    expect(r.tooNew).toEqual([])
  })

  it('peer range 解析失败时不硬算，交给 S2 报错', () => {
    const r = evaluate({
      ...base,
      floorResult: { ok: false, reason: 'bad range' },
      scan: scanOf([['useId', 'src/x.vue']]),
    })
    expect(r.tooNew).toEqual([])
    expect(r.floorError).toBe('bad range')
  })
})

describe('evaluate — S5 unknown（owner 2026-07-31 定 report-only）', () => {
  const base = { table: VUE_API_SINCE, tableMeta: TABLE_META, today: '2026-07-31' }

  it('表里没有的符号进 unknown，且不冒充 too-new', () => {
    const r = evaluate({
      ...base,
      floorResult: okFloor,
      scan: scanOf([['someVue36Api', 'src/new.vue']]),
    })
    expect(r.unknown.map((u) => u.name)).toEqual(['someVue36Api'])
    expect(r.tooNew).toEqual([])
  })

  it('report-only 这一档是显式常量，不是散在代码里的隐含行为', () => {
    expect(UNKNOWN_IS_BLOCKING).toBe(false)
  })
})

describe('evaluate — presentAtFloor 型证据只在测过的下界成立', () => {
  const base = { table: VUE_API_SINCE, tableMeta: TABLE_META, today: '2026-07-31' }

  it('下界 = 实测版本 → 通过，不算 unknown', () => {
    const r = evaluate({ ...base, floorResult: okFloor, scan: scanOf([['toRef', 'src/locale/index.ts']]) })
    expect(r.tooNew).toEqual([])
    expect(r.unknown).toEqual([])
  })

  it('下界降到 3.4.0 → 证据不再适用，退回 unknown 并说明原因（不是继续蒙混通过）', () => {
    const r = evaluate({
      ...base,
      floorResult: { ok: true, floor: '3.4.0', raw: '^3.4.0' },
      scan: scanOf([['toRef', 'src/locale/index.ts']]),
    })
    expect(r.unknown.map((u) => u.name)).toEqual(['toRef'])
    expect(r.unknown[0].note).toContain('3.5.0')
  })
})

describe('evaluate — S3 denominator / S4 table freshness', () => {
  const base = { table: VUE_API_SINCE, tableMeta: TABLE_META, floorResult: okFloor }

  it('扫到 0 个文件 → 红（不是"扫到 0 所以通过"）', () => {
    const r = evaluate({
      ...base,
      today: '2026-07-31',
      scan: { ok: true, files: 0, statements: 0, imports: new Map() },
    })
    expect(r.denominator.ok).toBe(false)
    expect(r.denominator.errors.length).toBeGreaterThan(0)
  })

  it('有文件但 0 条 import 语句 → 也红（正则被改坏的信号）', () => {
    const r = evaluate({
      ...base,
      today: '2026-07-31',
      scan: { ok: true, files: 170, statements: 0, imports: new Map() },
    })
    expect(r.denominator.ok).toBe(false)
    expect(r.denominator.errors.join(' ')).toContain('import')
  })

  it('scan 自己 ok:false → 理由透传进 denominator', () => {
    const r = evaluate({
      ...base,
      today: '2026-07-31',
      scan: { ok: false, reason: '扫描面不存在或不是目录：src', files: 0, statements: 0, imports: new Map() },
    })
    expect(r.denominator.ok).toBe(false)
    expect(r.denominator.errors[0]).toContain('src')
  })

  it('过了复核到期日 → S4 红', () => {
    const r = evaluate({ ...base, today: '2026-10-29', scan: scanOf([['ref', 'src/x.ts']]) })
    expect(r.tableStale.stale).toBe(true)
    expect(r.tableStale.due).toBe(TABLE_META.reviewDue)
  })

  it('到期日当天不算过期（边界）', () => {
    const r = evaluate({ ...base, today: TABLE_META.reviewDue, scan: scanOf([['ref', 'src/x.ts']]) })
    expect(r.tableStale.stale).toBe(false)
  })
})

describe('表本身的自洽性', () => {
  it('每个条目要么有 since 要么有 presentAtFloor，且都必须带来源', () => {
    for (const [name, entry] of Object.entries(VUE_API_SINCE)) {
      const hasVersion = Boolean(entry.since) !== Boolean(entry.presentAtFloor)
      expect(hasVersion, `${name} 必须恰好有 since 或 presentAtFloor 之一`).toBe(true)
      expect(typeof entry.source, `${name} 缺来源`).toBe('string')
      expect(entry.source.length, `${name} 的来源不能是空串`).toBeGreaterThan(10)
      const v = entry.since || entry.presentAtFloor
      expect(() => compareSemver(v, '3.0.0'), `${name} 的版本号形态非法: ${v}`).not.toThrow()
    }
  })

  it('TABLE_META 日期形态正确且到期日晚于复核日', () => {
    expect(TABLE_META.reviewedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/)
    expect(TABLE_META.reviewDue).toMatch(/^\d{4}-\d{2}-\d{2}$/)
    expect(TABLE_META.reviewDue > TABLE_META.reviewedAt).toBe(true)
  })

  it('peer 里的每个框架都该有表；没有表的只能是 report-only（这里钉住当前形态）', () => {
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'))
    expect(Object.keys(pkg.peerDependencies)).toEqual(['vue'])
    expect(Object.keys(API_TABLES)).toContain('vue')
  })
})

describe('真仓库当前状态（回归钉）', () => {
  it('src 的 vue import 面 100% 在表里，且无一新于当前 peer 下界', () => {
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'))
    const floorResult = parsePeerFloor(pkg.peerDependencies.vue)
    expect(floorResult.ok).toBe(true)
    const scan = scanImports(REPO_ROOT, 'vue', SCAN_DIR)
    expect(scan.ok).toBe(true)
    expect(scan.files).toBeGreaterThan(100)
    expect(scan.statements).toBeGreaterThan(10)
    const r = evaluate({
      floorResult,
      scan,
      table: VUE_API_SINCE,
      tableMeta: TABLE_META,
      today: new Date().toISOString().slice(0, 10),
    })
    expect(r.tooNew, `too-new: ${JSON.stringify(r.tooNew)}`).toEqual([])
    expect(r.unknown.map((u) => u.name), '有符号不在表里 → 补进 VUE_API_SINCE').toEqual([])
    expect(r.denominator.ok).toBe(true)
  })

  it('useId 仍是本仓库里唯一的 3.5-only API 使用者（先例传染的观察点）', () => {
    const scan = scanImports(REPO_ROOT, 'vue', SCAN_DIR)
    const files = [...(scan.imports.get('useId')?.files ?? [])].sort()
    expect(files).toEqual(['src/canonical/SelectBoxBase.vue', 'src/components/Tooltip/Tooltip.vue'])
  })
})

describe('闸必须真挂上（否则就是个没人跑的脚本）', () => {
  it('挂进 prepublishOnly 链（L5）', () => {
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'))
    expect(pkg.scripts['audit:framework-api-floor']).toContain('audit-framework-api-floor.mjs')
    // 2026-08-25（第 9 步）：39 步串从 `prepublishOnly` 搬到 `gate-chain`；
    // `prepublishOnly` 现在是跑它的 runner 入口。两条都断言 —— 只断言前者会在
    // 「有人把串写回入口」时静默通过，而那正是退回 fail-fast 的形态。
    expect(pkg.scripts['gate-chain']).toContain('audit:framework-api-floor')
    expect(pkg.scripts.prepublishOnly).toContain('run-gate-chain.mjs')
  })

  it('挂进 pre-commit（L4），触发面含 package.json / src / 闸脚本自身', () => {
    const hook = readFileSync(resolve(REPO_ROOT, '.husky/pre-commit'), 'utf8')
    expect(hook).toContain('audit:framework-api-floor')
    expect(hook).toContain('audit-framework-api-floor')
    expect(hook).toMatch(/package\\?\.json/)
  })

  // ── 残余① 的那条闸（INFRA-F87 残余①，2026-07-31）─────────────────────────────
  // 静态表看不见的四类（编译器宏 / option 形态 / 同名 API 新签名 / src 之外）只有
  // 「按下界真装 + 真跑」能闭合。它必须真挂在 CI 上，否则就是个没人跑的脚本 ——
  // 这正是 meta-rules 反模式 #7（声明 ≠ 被消费）。
  it('peer 下界冒烟闸挂进 Gitea pr-checks（L5，PR 与 push:master 都跑）', () => {
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'))
    expect(pkg.scripts['smoke:peer-floor']).toContain('smoke-peer-floor.mjs')
    const ci = readFileSync(resolve(REPO_ROOT, '.gitea/workflows/pr-checks.yml'), 'utf8')
    expect(ci).toContain('pnpm run smoke:peer-floor')
    // gate 平权（INFRA-F61）：拦 PR 的也必须拦 master 直推。
    expect(ci).toMatch(/on:[\s\S]*pull_request:[\s\S]*push:[\s\S]*branches:\s*\[master\]/)
  })

  it('冒烟闸的判据是 fail closed 的（拿不到下界 / 没产物 / 分母为 0 都必须红）', () => {
    // 三条都实测注入过（peer 退回 ^3.4.0 → S1 红并报 useId；peer='*' → S2；删掉整个
    // peerDependencies → S2 并提示「连闸一起删」）。这里钉住判据没被悄悄放宽成 warn。
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    // 换行无关地匹配 —— 判据调用是多行 prettier 格式，写死单行会钉住格式而不是钉住判据。
    expect(src).toMatch(/fail\(\s*'S2'/)
    expect(src).toMatch(/fail\(\s*'S3'/)
    expect(src).toMatch(/fail\(\s*'S4'/)
    expect(src).toMatch(/fail\(\s*'S5'/)
    expect(src).toContain('--ignore-scripts') // 否则 npm pack 会触发全量 prepare
    expect(src).toMatch(/process\.exit\(1\)/)
  })

  // ── INFRA-F87 残余② (2026-08-03) ─────────────────────────────────────────
  it('冒烟闸覆盖全部 3 个碰 peer 框架的出口面，不只 exports[.]', () => {
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    const m = src.match(/const SURFACES = \[([\s\S]*?)\n\]/)
    expect(m, 'SURFACES 表不在预期的字面形态').toBeTruthy()
    for (const key of ['.', './chart', './web-components']) {
      expect(m![1], `出口 ${key} 不在 SURFACES 里`).toContain(`key: '${key}'`)
    }
    // 出口若被悄悄摘掉，闸会继续绿 —— 这正是残余② 原本的形态（只测 1 面却读起来像
    // 测了全部）。钉住它，别让缩覆盖面变成无声操作。
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'))
    for (const key of ['./chart', './web-components']) {
      expect(pkg.exports[key], `package.json 少了 ${key} 出口`).toBeTruthy()
    }
  })

  it('分母按出口面各判一次，不按合计判', () => {
    // 合计不为 0 会把「某一面在空转」整个盖住。两组注入实测：wc 不注册任何元素 → 红；
    // chart 不导出任何组件 → 红；两次合计都还是非 0。
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    expect(src).toMatch(/for \(const s of SURFACES\)[\s\S]{0,600}fail\(\s*'S4'/)
  })

  // ── INFRA-F87 残余② · S6 出口面 peer 消费性（2026-08-03）────────────────────
  // 缘起：同日实测发现「3 个碰 peer 框架的出口面」这句话本身不成立 —— `./web-components`
  // 把 Vue 内联打包（vite.web-components.config.ts 没有 external），装什么版本都一样，
  // 那一面在这条闸里**结构上不可能变红**。S4 看不见它：S4 只问分母是否为 0，而它分母
  // 是 37，非零却与 peer 无关。
  it('每个出口面都必须声明 consumesPeer —— 不分类就不知道它的绿意味着什么', () => {
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    const m = src.match(/const SURFACES = \[([\s\S]*?)\n\]/)
    expect(m, 'SURFACES 表不在预期的字面形态').toBeTruthy()
    // 每个 `key:` 后面都要跟一个 `consumesPeer:`，数量对齐即可确认没有漏声明的面。
    const keys = [...m![1].matchAll(/key: '/g)].length
    const flags = [...m![1].matchAll(/consumesPeer: (?:true|false)/g)].length
    expect(flags, `${keys} 个出口面只有 ${flags} 个声明了 consumesPeer`).toBe(keys)
  })

  it('S6 双向红：声称消费却零 specifier = 空转；声称不消费却有 specifier = 契约面变了', () => {
    // 两条注入都实测过（wc 翻成 true → 报空转；'.' 翻成 false → 报契约面变了），
    // 外加漏字段那条。这里钉住判据没被削成单向或降级成 warn。
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    expect(src).toMatch(/s\.consumesPeer && found\.length === 0[\s\S]{0,200}fail\(\s*'S6'/)
    expect(src).toMatch(/!s\.consumesPeer && found\.length > 0[\s\S]{0,200}fail\(\s*'S6'/)
    expect(src).toMatch(/typeof s\.consumesPeer !== 'boolean'[\s\S]{0,200}fail\(\s*'S6'/)
  })

  it('声明的 consumesPeer 必须与构建配置里的 external 对得上（根因层，不依赖构建产物）', () => {
    // 这是比「扫产物」更早的一道：产物可能不在（没 build），但配置永远在。若哪天有人
    // 给 wc 配置加上 external: ['vue'] 而忘了改分类，这条会先红。
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    const table = src.match(/const SURFACES = \[([\s\S]*?)\n\]/)![1]
    const declOf = (key: string) =>
      /consumesPeer: true/.test(table.split(`key: '${key}'`)[1].split('key:')[0])

    const mainCfg = readFileSync(resolve(REPO_ROOT, 'vite.config.ts'), 'utf8')
    const wcCfg = readFileSync(resolve(REPO_ROOT, 'vite.web-components.config.ts'), 'utf8')
    const externalsVue = (cfg: string) => /external:\s*\[[^\]]*['"]vue['"]/.test(cfg)

    // '.' 与 './chart' 都由 vite.config.ts 产出，它 external 了 vue → 真消费 peer。
    expect(externalsVue(mainCfg), 'vite.config.ts 不再 external vue？').toBe(true)
    expect(declOf('.')).toBe(true)
    expect(declOf('./chart')).toBe(true)
    // './web-components' 由 vite.web-components.config.ts 产出，它**不** external。
    expect(
      externalsVue(wcCfg),
      'vite.web-components.config.ts 开始 external vue 了 —— 那一面已进入 peer 下界契约，' +
        '请把 SURFACES 里的 consumesPeer 改成 true，并确认它的符号进了 VUE_API_SINCE 表。',
    ).toBe(false)
    expect(declOf('./web-components')).toBe(false)
  })

  it('S7 点名产物里的动态 peer import（两条闸都盖不住的那一类）', () => {
    // 静态表按设计不解析 `await import('vue')`；冒烟闸也只在代码路径**被执行到**时才
    // 链接它，而 SSR 只跑默认状态。所以这一类必须被单独点名，不能靠任何一条闸兜。
    // 实测：往 dist/chart.js 尾部塞一处动态 import → 闸当场点名；复原后回到 0。
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    expect(src).toMatch(/S7/)
    expect(src).toMatch(/dynamicRe|import\\\\s\*\\\\\(/)
    // report-only（与 owner 2026-07-31 给 S5 unknown 定的那一档一致）—— 别悄悄升成阻塞
    // 而不留记录，也别把它降级成一句注释。
    expect(src).toContain('S7 report-only')
  })

  it('web-components 那一面如实声明它没有渲染、没有真浏览器', () => {
    // 这一面只证明「模块在 peer 下界能求值 + 注册了 N 个元素」。若哪天它被误读成
    // 「CE 在浏览器里可用」，损失是真实的 —— 所以声明必须留在运行输出里，不能只在注释。
    const src = readFileSync(resolve(REPO_ROOT, 'scripts/smoke-peer-floor.mjs'), 'utf8')
    expect(src).toMatch(/log\([^)]*没有\*\*渲染/)
    expect(src).toContain('不渲染')
  })
})
