// tests/export-claude-design-bundle-cli.test.ts
// -----------------------------------------------------------------------------
// `scripts/export-claude-design-bundle.mjs` 的**整脚本回归面**，射程限定在它的
// **「跳过（缺失）」那一族绿档 WARN** —— 构建产物不在场时，它**照常 exit 0 导出一个包**，
// 只在 stderr 上说一句「这几样跳过了」。
//
// 🔴 **⛔ 它不是闸（是导出工具）—— 这一点必须先说清，否则本面会被误读。**
//   同族处方逐字登记过：「它列在供给侧表里只因为形态相同（裸 `console.warn`），
//   ⛔ 别把它算进『闸的覆盖率』」。实测坐实：它的 npm key 是 `export:` 前缀，
//   既不进元闸的文件名闭集、也不进棘轮的闸清单 ⇒ **补它不会改变任何闸的覆盖读数**。
//   ⚠️ **但这不等于「不该补」**：那句「跳过（缺失）」确实是绿档唯一的免责声明 ——
//   包发出去了、少了四样东西，而**退出码是 0**。它退化成静默时没有任何读数会变。
//
// 🔴 **为什么这一族 WARN 特别容易静默地变成错误的包**：四条的触发条件全是
//   **`dist*` 不在场**（tokens.json / composition.json / web-components / icons manifest）。
//   而 `dist*` 是 gitignored 的构建产物 ⇒ **任何没跑过 build 的环境**（新 clone、CI 的
//   fresh checkout、另一个 worktree）都会静默少打包这四样，而使用者拿到的包看起来是正常的。
//
// ⚠️ **本面登记的 fixture 成本（2026-09-15 实测，⛔ 是读数不是抱怨）**：
//   这条脚本的 fixture 比同族那批闸贵一个数量级 —— 要 **git 仓**（它跑
//   `git rev-parse --short HEAD`）+ **20 个必需项**（19 文件 + 1 个整目录）才跑得起来；
//   缺任何一个它就 `process.exit(1)` 或 ENOENT 崩，而**崩溃也是非零退出** ⇒ 红得理由不对。
//   ⇒ 其中 14 个由被测脚本自己 `export` 的 `REFERENCE_DOCS` **动态取**，⛔ 不写死
//   （文档清单变了本面自动跟上）；剩下 6 个在下方显式列出。
//
// ⛔ 断言必须**点名 WARN 的原文**，⛔ 不许写 `expect(stderr).not.toBe('')` ——
//   fixture 里那些 `execSync git` 的报错会透传进 stderr ⇒「非空」这类断言恒真。
//
// ⚠️ **WARN 前缀是 `⚠ `（U+26A0 裸码位），⛔ 不是 `⚠️ `（带 VS16）** ——
//   两者字节不同（`e2 9a a0` vs `e2 9a a0 ef b8 8f`）。本面刻意只匹配正文、不匹配前缀，
//   免得手打错一个就全组假绿。
//
// ⛔ 污染纪律：本文件除被测脚本自身外**不出现任何闸的 `.mjs` basename、也不出现任何
//   `audit:` npm key**。
//
// ⚠️ **本面登记的覆盖边界（⛔ 是边界，不是 TODO）**：
//   1. 六条 warnings 里**只覆盖四条** —— 实测这四条由「`dist*` 缺失」一次触发。
//      另两条（`--with-icon-svg` 超上限 · 系统无 `zip` 命令）**未钉**：前者要造图标产物，
//      后者要改 `PATH`。⛔ 这是如实登记的边界。
//   2. 只抓 **must-hit** 那一侧（「摘掉 WARN 会不会红」）。
//   3. **⛔ 不断言导出包的条目数 / commit sha** —— 那随真仓库内容漂，钉它等于钉一个会变的数。
//   4. **环境名**：macOS · Node v24.15.0 · vitest。⛔ 别把读数搬到别的环境。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterAll } from 'vitest'
import { execFileSync } from 'node:child_process'
import { cpSync, mkdirSync, writeFileSync } from 'node:fs'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createGateFixture, runGate, cleanupGateFixtures, gitCleanEnv } from './lib/gate-fixture-root'
import { REFERENCE_DOCS } from '../scripts/export-claude-design-bundle.mjs'

const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..')
const SCRIPT = 'scripts/export-claude-design-bundle.mjs'

/** 必需项里 `REFERENCE_DOCS` 之外的那几个（实测缺任一即崩）。 */
const EXTRA_REQUIRED = [
  'docs/site-review-manifest.json',
  'docs/CLAUDE_DESIGN_RULES.md',
  'src/tokens/variables.css',
  'figma-data/normalized/variables.json',
  'scripts/upstream-gate.schema.json',
]
/** 它 `readdirSync` 的整个目录。 */
const REQUIRED_DIR = 'docs/internal/affordance-categories'

/** 四条 WARN 的正文特征（⛔ 只匹配正文，不匹配 `⚠` 前缀 —— 见头注释）。 */
const WARN_TOKENS_DTCG = 'dist/tokens/tokens.json → tokens/tokens.dtcg.json'
const WARN_COMPOSITION = 'dist/composition/composition.json → reference/composition.json'
const WARN_CE_RUNTIME = 'dist-wc/tvu-web-components.umd.cjs → assets/tvu-web-components.js'
const WARN_ICON_SPRITE = '图标 sprite 跳过：dist/icons/manifest.json 缺失（需 pnpm build）'

/**
 * 建 fixture：脚本 + 20 个必需项 + 一个 git 仓。
 * @param extraFiles 额外写进 fixture 的文件（用来让某条 WARN 的条件**不**成立）
 */
function buildFixture(extraFiles: Record<string, string> = {}) {
  const root = createGateFixture({
    gate: SCRIPT,
    prefix: 'export-bundle-cli',
    copyFiles: [...REFERENCE_DOCS.map((d: [string, string]) => d[0]), ...EXTRA_REQUIRED],
  })

  // readdirSync 要整个目录
  cpSync(join(REPO, REQUIRED_DIR), join(root, REQUIRED_DIR), { recursive: true })

  for (const [rel, content] of Object.entries(extraFiles)) {
    mkdirSync(dirname(join(root, rel)), { recursive: true })
    writeFileSync(join(root, rel), content)
  }

  // 它跑 `git rev-parse --short HEAD` ⇒ fixture 必须是 git 仓。
  // ⛔ 必须走 gitCleanEnv：否则在 pre-commit 里这两条会落到**真仓库正在提交的 index** 上。
  const env = gitCleanEnv()
  execFileSync('git', ['init', '-q', '.'], { cwd: root, env })
  execFileSync(
    'git',
    ['-c', 'user.email=t@example.invalid', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'fixture'],
    { cwd: root, env },
  )
  return root
}

const runExport = (root: string) =>
  runGate(root, SCRIPT, [`--out=${join(root, 'out')}`, '--no-zip'])

afterAll(() => cleanupGateFixtures())

describe('export-claude-design-bundle —— 构建产物缺失时的绿档免责声明', () => {
  it('E1 没有 dist* 时照常导出（exit 0），且四条「跳过」WARN 都打在 stderr 上', () => {
    const run = runExport(buildFixture())

    // 关键事实：它**没有**因为少了四样东西而失败
    expect(run.status).toBe(0)

    expect(run.stderr).toContain(WARN_TOKENS_DTCG)
    expect(run.stderr).toContain(WARN_COMPOSITION)
    expect(run.stderr).toContain(WARN_CE_RUNTIME)
    expect(run.stderr).toContain(WARN_ICON_SPRITE)
  })

  it('E2 每条 WARN 各自绑在自己的条件上：把那一份产物放进去，只有它消失（⇒ ⛔ 不是恒真）', () => {
    // 只补 dtcg tokens 这一份
    const run = runExport(buildFixture({ 'dist/tokens/tokens.json': '{"color":{}}\n' }))

    expect(run.status).toBe(0)
    // 它的条件不成立了 ⇒ 这条必须消失
    expect(run.stderr).not.toContain(WARN_TOKENS_DTCG)
    // 其余三条的条件没动 ⇒ 必须还在（否则就是「WARN 整块塌了」而不是「这一条消失了」）
    expect(run.stderr).toContain(WARN_COMPOSITION)
    expect(run.stderr).toContain(WARN_CE_RUNTIME)
    expect(run.stderr).toContain(WARN_ICON_SPRITE)
  })
})
