// tests/is-cli-entry.test.ts
// -----------------------------------------------------------------------------
// `scripts/lib/is-cli-entry.mjs` 的回归面（[[INFRA-F140]]）。
//
// 🔴 **为什么这一份格外要紧**：它是 **16 条随包 CLI 共用的唯一入口判据**。判错一次的
// 后果不是「少检查一点」，而是**整条闸静默不跑、exit 0、stdout/stderr 双空**，而
// consumer 的 CI 一直显示绿（实测这个状态在下游存在过至少 19 天）。
//
// 覆盖分两层，**两层缺一不可**：
//   · 单元层：判据函数本身（快路径 / symlink / import 场景 / 各种退化输入）。
//   · **端到端层：真 symlink + 真 spawn**。⚠️ 单元层结构上测不到 ESM loader 的行为 ——
//     `import.meta.url` 是否已 realpath 由 loader 决定，而那正是本缺陷的一半病根。
//     所以必须真起一个进程、真穿过一层 symlink。
//
// ⛔ 别把端到端那批换成「传假 argv 给函数」—— 那样测的是我对 loader 的**假设**，
//    不是 loader 的**行为**。本仓「判据必须取终态事实」纪律的直接应用。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import { spawnSync } from 'node:child_process'
import {
  lstatSync,
  mkdirSync,
  mkdtempSync,
  realpathSync,
  rmSync,
  symlinkSync,
  writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { isCliEntry } from '../scripts/lib/is-cli-entry.mjs'

const REPO_ROOT = resolve(__dirname, '..')
const GUARD_SRC = resolve(REPO_ROOT, 'scripts/lib/is-cli-entry.mjs')

const roots: string[] = []
afterEach(() => {
  for (const r of roots.splice(0)) rmSync(r, { recursive: true, force: true })
})

function tmp(prefix: string): string {
  // ⚠️ realpath 掉 —— macOS 的 tmpdir 是 `/var/folders/…` 而 `/var` 本身是 symlink。
  //    不 realpath 的话「无 symlink 的对照组」根本不存在，本文件一半用例会失去意义。
  const r = realpathSync(mkdtempSync(join(tmpdir(), `${prefix}-`)))
  roots.push(r)
  return r
}

// ===========================================================================
describe('isCliEntry — 单元层', () => {
  it('快路径：argv[1] 与自身路径逐字相同 ⇒ true', () => {
    const self = join(tmp('ice-fast'), 'fx-entry.mjs')
    expect(isCliEntry(pathToFileURL(self).href, ['node', self])).toBe(true)
  })

  it('🔴 核心：argv[1] 走 symlink、自身已 realpath ⇒ **仍是 true**', () => {
    // 这一条就是整个 [[INFRA-F140]] 的判据。三种手写形态在这里全部返回 false。
    const real = tmp('ice-real')
    const entry = join(real, 'fx-entry.mjs')
    writeFileSync(entry, '// fx\n')

    const linkHome = tmp('ice-link')
    const link = join(linkHome, 'linked')
    symlinkSync(real, link, 'dir')
    expect(lstatSync(link).isSymbolicLink()).toBe(true)

    const invokedViaLink = join(link, 'fx-entry.mjs')
    expect(invokedViaLink).not.toBe(entry) // 自证：两条路径字面不同
    expect(isCliEntry(pathToFileURL(entry).href, ['node', invokedViaLink])).toBe(true)
  })

  it('import 场景：argv[1] 是**别的**文件 ⇒ false（守卫的本职）', () => {
    const root = tmp('ice-import')
    const self = join(root, 'fx-lib.mjs')
    const other = join(root, 'fx-runner.mjs')
    writeFileSync(self, '// fx\n')
    writeFileSync(other, '// fx\n')
    expect(isCliEntry(pathToFileURL(self).href, ['node', other])).toBe(false)
  })

  it('⛔ must-not-hit：basename 相同但目录不同 ⇒ false（防「宽松匹配」）', () => {
    // 判据一旦被改成比 basename / 比后缀，`import` 场景会被误判成 CLI —— 语义整个翻面。
    const a = tmp('ice-a')
    const b = tmp('ice-b')
    const self = join(a, 'fx-same-name.mjs')
    const other = join(b, 'fx-same-name.mjs')
    writeFileSync(self, '// fx\n')
    writeFileSync(other, '// fx\n')
    expect(isCliEntry(pathToFileURL(self).href, ['node', other])).toBe(false)
  })

  it('退化输入一律 false，⛔ 不抛（守卫抛错等于把宿主脚本打死）', () => {
    const self = join(tmp('ice-degen'), 'fx-entry.mjs')
    const url = pathToFileURL(self).href
    expect(isCliEntry(url, ['node'])).toBe(false) // 无 argv[1]（如 `node -e`）
    expect(isCliEntry(url, [])).toBe(false)
    // @ts-expect-error 故意喂错类型：守卫必须扛住而不是抛
    expect(isCliEntry(undefined, ['node', self])).toBe(false)
    // @ts-expect-error 同上
    expect(isCliEntry(123, ['node', self])).toBe(false)
    expect(isCliEntry('https://example.com/x.mjs', ['node', self])).toBe(false)
    expect(isCliEntry('/not/a/url.mjs', ['node', self])).toBe(false)
  })

  it('argv[1] 指向**不存在**的文件 ⇒ false（realpath 抛错时退回不跑）', () => {
    // 真入口的文件一定存在 ⇒ 此时「不跑」是对的。⛔ 别改成 true「保险起见跑一下」。
    const root = tmp('ice-enoent')
    const self = join(root, 'fx-entry.mjs')
    writeFileSync(self, '// fx\n')
    expect(isCliEntry(pathToFileURL(self).href, ['node', join(root, 'fx-gone.mjs')])).toBe(false)
  })
})

// ===========================================================================
describe('isCliEntry — 端到端层（真 symlink + 真 spawn）', () => {
  /** 建一棵假包：`<root>/scripts/lib/is-cli-entry.mjs` + 一个用它当守卫的宿主脚本。 */
  function buildPackage(prefix = 'ice-e2e'): string {
    const root = tmp(prefix)
    mkdirSync(join(root, 'scripts/lib'), { recursive: true })
    // ⛔ 拷不链：被链进来的 lib 其 import.meta.url 会 realpath 回真仓库（本轮不受影响，
    //    但保持与 gate-fixture-root harness 同一纪律，别在这里开例外）。
    writeFileSync(join(root, 'scripts/lib/is-cli-entry.mjs'), readGuard())
    writeFileSync(
      join(root, 'scripts/fx-host.mjs'),
      [
        "import { isCliEntry } from './lib/is-cli-entry.mjs'",
        '// 正常路径必然印东西 —— 于是「零输出 + exit 0」= 守卫判错了。',
        'if (isCliEntry(import.meta.url)) {',
        "  console.log('fx-host ran · scanned 7 fx-units')",
        '  process.exit(3)',
        '}',
        '',
      ].join('\n'),
    )
    return root
  }

  function readGuard(): string {
    // 从活源读，⛔ 不在测试里抄一份实现
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    return require('node:fs').readFileSync(GUARD_SRC, 'utf8')
  }

  const run = (cwd: string, script: string) =>
    spawnSync(process.execPath, [script], { cwd, encoding: 'utf8' })

  it('🔴 经 pnpm 形状的 symlink 调用 ⇒ 宿主脚本**真的跑**（这是 F140 的验收条件）', () => {
    const pkg = buildPackage()
    const consumer = tmp('ice-consumer')
    mkdirSync(join(consumer, 'node_modules/@ux-team'), { recursive: true })
    const link = join(consumer, 'node_modules/@ux-team/tvu-design-system')
    symlinkSync(pkg, link, 'dir')

    // fail-closed 自证：没有真 symlink 的话本条是空过
    expect(lstatSync(link).isSymbolicLink()).toBe(true)
    expect(realpathSync(link)).not.toBe(link)

    const r = run(consumer, 'node_modules/@ux-team/tvu-design-system/scripts/fx-host.mjs')
    // 🔑 三样一起钉：退出码不是 0 · 输出非空 · 点名只有真跑过才有的读数
    expect(r.status).toBe(3)
    expect(`${r.stdout}${r.stderr}`).not.toBe('')
    expect(r.stdout).toContain('scanned 7 fx-units')
  })

  it('直连（无 symlink）⇒ 与 symlink 侧**逐字相同**', () => {
    const pkg = buildPackage()
    const r = run(pkg, join(pkg, 'scripts/fx-host.mjs'))
    expect(r.status).toBe(3)
    expect(r.stdout).toContain('scanned 7 fx-units')
  })

  it('被 import 时**不跑**（守卫的本职，端到端确认）', () => {
    const pkg = buildPackage()
    writeFileSync(
      join(pkg, 'scripts/fx-importer.mjs'),
      [
        "await import('./fx-host.mjs')",
        "console.log('importer done · host stayed quiet')",
        '',
      ].join('\n'),
    )
    const r = run(pkg, join(pkg, 'scripts/fx-importer.mjs'))
    expect(r.status).toBe(0)
    expect(r.stdout).toContain('importer done')
    // ⛔ 反向钉：宿主的 CLI 分支一个字都不许印
    expect(r.stdout).not.toContain('scanned 7 fx-units')
  })

  /**
   * 六种手写形态 —— 逐字对应 `scripts/lib/is-cli-entry.mjs` 头注释的 F1–F6。
   *
   * · **F1–F3** = 本仓 16 条随包脚本**实测真踩过的**三种（2026-08-26）。
   * · **F4–F6** = 同病形态，2026-09-14 补登记（此前头注释的形态表只有三种 ⇒
   *   照它写守卫的人仍可能写出这三种）。
   *
   * ⛔ 每一项都是**替身实现**，签名与真守卫一致；本表是形态的**唯一执行面** ——
   * 头注释说「六种」，这里就必须见证六种，⛔ 别只留一种当样板。
   */
  const HAND_ROLLED: ReadonlyArray<{ id: string; imports: string[]; expr: string }> = [
    {
      id: 'F1',
      imports: ["import { fileURLToPath } from 'node:url'"],
      expr: 'process.argv[1] === fileURLToPath(moduleUrl)',
    },
    { id: 'F2', imports: [], expr: 'moduleUrl === `file://${process.argv[1]}`' },
    {
      id: 'F3',
      imports: ["import { fileURLToPath } from 'node:url'", "import { resolve } from 'node:path'"],
      expr: 'resolve(process.argv[1]) === resolve(fileURLToPath(moduleUrl))',
    },
    { id: 'F4', imports: [], expr: 'process.argv[1] === new URL(moduleUrl).pathname' },
    {
      id: 'F5',
      imports: ["import { pathToFileURL } from 'node:url'"],
      expr: 'moduleUrl === pathToFileURL(process.argv[1]).href',
    },
    {
      id: 'F6',
      imports: ["import { fileURLToPath } from 'node:url'", "import { resolve } from 'node:path'"],
      expr: 'resolve(process.argv[1]) === fileURLToPath(moduleUrl)',
    },
  ]

  for (const f of HAND_ROLLED) {
    it(`🔴 阴性对照 ${f.id}：手写形态 ⇒ symlink 侧当场退化成 exit 0 + 零输出`, () => {
      // ⇒ 这一批证明上面那批**不是空过**：同一棵 fixture、同一条命令，只换判据实现。
      // ⛔ 别删 —— 没有它，「守卫是对的」和「宿主脚本恰好总能跑」区分不开。
      const pkg = buildPackage()
      writeFileSync(
        join(pkg, 'scripts/lib/is-cli-entry.mjs'),
        [
          ...f.imports,
          `// 手写形态 ${f.id}（[[INFRA-F140]] 头注释逐字登记的六种之一）`,
          'export function isCliEntry(moduleUrl) {',
          `  return !!process.argv[1] && (${f.expr})`,
          '}',
          '',
        ].join('\n'),
      )
      const consumer = tmp(`ice-consumer-neg-${f.id}`)
      mkdirSync(join(consumer, 'node_modules/@ux-team'), { recursive: true })
      const link = join(consumer, 'node_modules/@ux-team/tvu-design-system')
      symlinkSync(pkg, link, 'dir')

      // fail-closed 自证：没有真 symlink 的话本条是空过
      expect(lstatSync(link).isSymbolicLink()).toBe(true)
      expect(realpathSync(link)).not.toBe(link)

      const r = run(consumer, 'node_modules/@ux-team/tvu-design-system/scripts/fx-host.mjs')
      expect(r.status).toBe(0) // ← 假绿
      expect(r.stdout).toBe('')
      expect(r.stderr).toBe('')

      // 而直连侧照样跑 ⇒ 差异确实来自那一层 symlink，不是 fixture 坏了
      // （这一臂同时挡住「替身写崩了 ⇒ 两侧都不跑」被误读成「形态同病」）
      const direct = run(pkg, join(pkg, 'scripts/fx-host.mjs'))
      expect(direct.status).toBe(3)
      expect(direct.stdout).toContain('scanned 7 fx-units')
    })
  }

  it('🔴 F4 的**第二层**病：路径含空格 ⇒ **零 symlink 也失配**（`.pathname` 保留 `%20`）', () => {
    // 上面那批全靠 symlink 才现形；这一条不同 —— 同一条路径、⛔ 一层 symlink 都没有。
    // ⇒ F4 比 F1–F3 多一个触发面，而 macOS 上「用户目录含空格」是常见布局。
    const f4 = HAND_ROLLED.find((f) => f.id === 'F4')
    if (!f4) throw new Error('fail-closed：F4 不在形态表里 ⇒ 本条是空过')

    const pkg = buildPackage('ice sp') // ← 目录名刻意含空格
    expect(pkg).toContain(' ') // fail-closed 自证：真的拿到了含空格的路径

    // 阳性对照先跑：真守卫在含空格路径下**照常跑** ⇒ 空格本身不是问题
    const ok = run(pkg, join(pkg, 'scripts/fx-host.mjs'))
    expect(ok.status).toBe(3)
    expect(ok.stdout).toContain('scanned 7 fx-units')

    // 只换判据实现 ⇒ 同一条命令当场退化
    writeFileSync(
      join(pkg, 'scripts/lib/is-cli-entry.mjs'),
      [
        ...f4.imports,
        '// 手写形态 F4（`.pathname` 保留百分号编码）',
        'export function isCliEntry(moduleUrl) {',
        `  return !!process.argv[1] && (${f4.expr})`,
        '}',
        '',
      ].join('\n'),
    )
    const bad = run(pkg, join(pkg, 'scripts/fx-host.mjs'))
    expect(bad.status).toBe(0) // ← 假绿，且这次与 symlink 无关
    expect(bad.stdout).toBe('')
    expect(bad.stderr).toBe('')
  })
})
