// tests/audit-binding-config-parity.test.ts
// -----------------------------------------------------------------------------
// `audit:binding-config-parity`（L4 pre-commit + L5 gitea-pr-checks）的**整脚本**回归面。
//
// 为什么是整脚本而不是 import 判据函数：该闸 371 行**零导出**、两类判据与整个 runner
// 都写在顶层。量具 `pnpm report:gate-regression-face` 把它记为「零判据覆盖 · P1/P2 双 PASS」
// （P3 = UNCLEAR，因为它的输入面是一个 **`import` 进来的 TS 模块**而不是 readdir 的目录 ——
// 实测这反而是最好造的一种 fixture：写一份自己的 `components.config.ts` 就完全可控）。
// 本文件是那条判定的兑现（[[INFRA-F138]]）。⛔ 闸本体一行没改。
//
// ✅ **那处「实现与头注释矛盾」2026-08-26 已闭合 —— 改的是头注释，不是实现**（owner 拍板）：
// 头注释原逐字承诺「If a local type alias can't be found …, prints WARN and skips
// (does NOT silently pass, does NOT false-positive)」，而 `unionWarns` 声明后**从未被
// push 过**、`warnMessages` 恒空 ⇒ 那条 WARN 通路在实现里不存在。未解析的 alias 会走
// else 分支被当成**字面量成员**参与比较 ⇒ 实际产生的是 **FAIL**。
// ⇒ 闸本体（含那三段恒不执行的 warn 分支）**一行没改**，改的是那句话：头注释现在
// 逐字写「the unresolved identifier is kept AS A LITERAL MEMBER … it **FAILs**」。
// ⇒ 本文件下方那条用例钉的**仍是现行行为**，只是它现在与头注释一致了，不再是「钉住一个
// 与文档相反的事实」。⛔ 仍别读成对该行为的背书：它安全（假红不是假绿）但不优雅。
//
// 覆盖：绿档非空过 + NAME-SET 四条分叉（两侧差集 / vModel / passthrough / 缺 defineProps）
// + UNION-BODY 六条（本地 alias / inline / 跳过非 union / 跳过结构类型 / imported / 再导出链）
// + 3 条接线钉。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-binding-config-parity.mjs'
const CONFIG = 'src/web-components/components.config.ts'

afterEach(cleanupGateFixtures)

type PropCfg = { name: string; tsType: string }
type Cfg = {
  name: string
  canonicalPath?: string
  props: PropCfg[]
  vModel?: { elementProperty: string } | null
}

function configSource(cfgs: Cfg[]): string {
  const body = cfgs
    .map((c) =>
      JSON.stringify(
        {
          name: c.name,
          canonicalPath: c.canonicalPath ?? `../canonical/${c.name}.vue`,
          props: c.props,
          vModel: c.vModel ?? null,
        },
        null,
        2,
      ),
    )
    .join(',\n')
  return `export const COMPONENT_CONFIGS = [\n${body}\n]\n`
}

/** 造一个 SFC：defineProps 行必须有缩进（闸的 propLineRe 要求行首空白）。 */
function sfc(props: Record<string, string>, extra = ''): string {
  const lines = Object.entries(props).map(([n, t]) => `  ${n}?: ${t}`)
  return `<template><div /></template>
<script setup lang="ts">
${extra}defineProps<{
${lines.join('\n')}
}>()
</script>
`
}

type Overrides = {
  configs?: Cfg[]
  /** src/canonical/ 下的 .vue：组件名 → 内容；传 {} 表示一个都不建 */
  vue?: Record<string, string>
  files?: Record<string, string>
}

function build(o: Overrides = {}) {
  const configs = o.configs ?? [{ name: 'FxWidget', props: [{ name: 'label', tsType: 'string' }] }]
  const vue = o.vue ?? { FxWidget: sfc({ label: 'string' }) }
  const files: Record<string, string> = { [CONFIG]: configSource(configs) }
  for (const [name, body] of Object.entries(vue)) files[`src/canonical/${name}.vue`] = body
  return createGateFixture({
    gate: GATE,
    prefix: 'binding-parity-fx',
    dirs: ['src/canonical', 'src/web-components'],
    files: { ...files, ...(o.files ?? {}) },
  })
}

describe('audit:binding-config-parity — 绿档 + 自印非空过', () => {
  it('名集与联合体都对齐 → exit 0，且自印的是 fixture 自己的计数（≠ 真仓库 ⇒ 非空过）', () => {
    const run = runGate(build(), GATE)
    // 真仓库 COMPONENT_CONFIGS 是 20+ 条 —— 「1/1 PASS」不可能对上。
    expectGateGreen(run, {
      contains: [
        '✅ [FxWidget] PASS — name-set + union-body parity OK',
        'audit:binding-config-parity — 1/1 PASS, 0 FAIL',
      ],
    })
  })
})

describe('audit:binding-config-parity — 判据 1：NAME-SET PARITY', () => {
  it('SFC 多一个 prop、config 没有 → 红并点名差在哪一侧', () => {
    const run = runGate(build({ vue: { FxWidget: sfc({ label: 'string', extra: 'boolean' }) } }), GATE)
    expectGateRed(run, {
      marker: '❌ [FxWidget] NAME-SET PARITY MISMATCH',
      checks: ['In SFC defineProps but NOT in config: extra'],
    })
  })

  it('config 多一个 prop、SFC 没有 → 红并点名另一侧', () => {
    const run = runGate(
      build({ configs: [{ name: 'FxWidget', props: [{ name: 'label', tsType: 'string' }, { name: 'ghost', tsType: 'string' }] }] }),
      GATE,
    )
    expectGateRed(run, { checks: ['In config but NOT in SFC defineProps: ghost'] })
  })

  it('两侧同时有差 → 两行都印（不是只报一侧）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'ghost', tsType: 'string' }] }],
        vue: { FxWidget: sfc({ label: 'string' }) },
      }),
      GATE,
    )
    expectGateRed(run, {
      checks: ['In SFC defineProps but NOT in config: label', 'In config but NOT in SFC defineProps: ghost'],
    })
  })

  it('`vModel.elementProperty` 计入 config 名集 → SFC 的 modelValue 被它满足（绿）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'label', tsType: 'string' }], vModel: { elementProperty: 'modelValue' } }],
        vue: { FxWidget: sfc({ label: 'string', modelValue: 'string' }) },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('⛔ must-HIT 对照：同一份 SFC 把 vModel 改回 null → modelValue 立刻变成未登记项', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'label', tsType: 'string' }], vModel: null }],
        vue: { FxWidget: sfc({ label: 'string', modelValue: 'string' }) },
      }),
      GATE,
    )
    expectGateRed(run, { checks: ['In SFC defineProps but NOT in config: modelValue'] })
  })
})

describe('audit:binding-config-parity — 无 defineProps 的两条分叉', () => {
  it('SFC 无 defineProps **且** config 名集为空 → 绿，且印的是 passthrough 那句专门文案', () => {
    const run = runGate(
      build({ configs: [{ name: 'FxWidget', props: [] }], vue: { FxWidget: '<template><div /></template>\n' } }),
      GATE,
    )
    expectGateGreen(run, {
      contains: ['✅ [FxWidget] PASS — no defineProps in SFC (useAttrs passthrough) + config.props empty — consistent'],
    })
  })

  it('SFC 无 defineProps **但** config 有 props → 红（⛔ 不是当成 passthrough 放行）', () => {
    const run = runGate(build({ vue: { FxWidget: '<template><div /></template>\n' } }), GATE)
    expectGateRed(run, { checks: ['❌ [FxWidget] Could not find defineProps<{...}>'] })
  })

  it('SFC 无 defineProps 且 config.props 空、但有 vModel → 红（vModel 也算名集，不算空）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [], vModel: { elementProperty: 'modelValue' } }],
        vue: { FxWidget: '<template><div /></template>\n' },
      }),
      GATE,
    )
    expectGateRed(run, { checks: ['Could not find defineProps<{...}>'] })
  })

  it('SFC 文件整个读不到 → 红并点名路径（⛔ 不是跳过）', () => {
    const run = runGate(build({ vue: {} }), GATE)
    expectGateRed(run, { checks: ['❌ [FxWidget] Cannot read SFC at', 'FxWidget.vue'] })
  })
})

describe('audit:binding-config-parity — 判据 2：UNION-BODY PARITY', () => {
  it('本地 `type X = …` 与 config.tsType 一致 → 绿', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'color', tsType: "'red' | 'green'" }] }],
        vue: { FxWidget: sfc({ color: 'Color' }, "type Color = 'red' | 'green'\n") },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('SFC 的 alias 多一个成员 → 红并点名两侧差集（designer 改了 SFC 没改 config）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'color', tsType: "'red' | 'green'" }] }],
        vue: { FxWidget: sfc({ color: 'Color' }, "type Color = 'red' | 'green' | 'blue'\n") },
      }),
      GATE,
    )
    expectGateRed(run, {
      marker: '❌ [FxWidget] UNION-BODY PARITY MISMATCH',
      checks: ['[color] UNION-BODY MISMATCH', "In SFC but NOT config: 'blue'"],
    })
  })

  it('config 多一个成员 → 红并点名另一侧', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'color', tsType: "'red' | 'green' | 'blue'" }] }],
        vue: { FxWidget: sfc({ color: 'Color' }, "type Color = 'red' | 'green'\n") },
      }),
      GATE,
    )
    expectGateRed(run, { checks: ["In config but NOT SFC: 'blue'"] })
  })

  it('inline union（SFC 里直接写字面量联合）也对得上', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'size', tsType: "'S' | 'M'" }] }],
        vue: { FxWidget: sfc({ size: "'S' | 'M'" }) },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('⛔ must-not-hit：config.tsType 没有 `|` → 整条跳过联合体检查', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'label', tsType: 'string' }] }],
        vue: { FxWidget: sfc({ label: 'SomeUnresolvedAlias' }) },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('⛔ must-not-hit：config.tsType 含 `{`（结构类型）→ 跳过（Table.columns / Chart.datasets 那类）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'columns', tsType: "{ key: string; align?: 'left' | 'right' }[]" }] }],
        vue: { FxWidget: sfc({ columns: 'ColumnDef[]' }) },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('alias 作为**联合成员之一**出现时也被展开（`StepStyle | Record<…>` 那种形态）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'style', tsType: "'number' | 'icon' | Record<string, string>" }] }],
        vue: { FxWidget: sfc({ style: 'StepStyle | Record<string, string>' }, "type StepStyle = 'number' | 'icon'\n") },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })
})

describe('audit:binding-config-parity — 跨文件类型解析', () => {
  it('`import type { X } from "./y"` 的 union 被解析（type-only 形态）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'kind', tsType: "'pie' | 'donut'" }] }],
        vue: { FxWidget: sfc({ kind: 'FxKind' }, "import type { FxKind } from './fx-kinds'\n") },
        files: { 'src/canonical/fx-kinds.ts': "export type FxKind = 'pie' | 'donut'\n" },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('mixed inline 形态 `import { fn, type X } from "./y"` 也解析（值名被无害忽略）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'kind', tsType: "'pie' | 'donut'" }] }],
        vue: { FxWidget: sfc({ kind: 'FxKind' }, "import { buildFx, type FxKind } from './fx-kinds'\n") },
        files: { 'src/canonical/fx-kinds.ts': "export const buildFx = 1\nexport type FxKind = 'pie' | 'donut'\n" },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('再导出链递归解析（canonical → base → 真正声明处，Chart 那条链的形态）', () => {
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'kind', tsType: "'pie' | 'donut'" }] }],
        vue: { FxWidget: sfc({ kind: 'FxKind' }, "import type { FxKind } from './fx-base'\n") },
        files: {
          'src/canonical/fx-base.ts': "import type { FxKind } from './fx-deep'\nexport type { FxKind }\n",
          'src/canonical/fx-deep.ts': "export type FxKind = 'pie' | 'donut'\n",
        },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['1/1 PASS, 0 FAIL'] })
  })

  it('⚠️ 未解析的 alias → **红**（现行行为，2026-08-26 起头注释与之一致）', () => {
    // ⛔ 这条钉的是实现的**事实**，不是对它的背书 —— `unionWarns` 从未被 push，
    // WARN 通路在实现里不存在，未解析的标识符被当成字面量成员参与比较。
    // 方向安全（假红而非假绿）。2026-08-26 owner 拍板**改头注释、不补实现** ⇒ 闸的
    // 头注释现已逐字声明这里 FAIL；本用例因此同时是那句声明的漂移钉：谁把 WARN 通路
    // 真的补上（让这里不再红），这条会当场转红，逼他一并更新头注释。
    const run = runGate(
      build({
        configs: [{ name: 'FxWidget', props: [{ name: 'kind', tsType: "'pie' | 'donut'" }] }],
        vue: { FxWidget: sfc({ kind: 'NowhereDefined' }) },
      }),
      GATE,
    )
    expectGateRed(run, { checks: ['[kind] UNION-BODY MISMATCH', "In SFC but NOT config: 'NowhereDefined'"] })
    // 头注释承诺的 WARN 段落确实不出现 —— 这一行就是那处矛盾的直接证据
    expect(run.stdout).not.toContain('WARNINGS (union extraction incomplete')
  })
})

describe('audit:binding-config-parity — 接线钉（判据 → 退出码 → 点名输出）', () => {
  it('多组件部分失败 → 计数正确、绿的照印 ✅、总退出码由红的决定', () => {
    const run = runGate(
      build({
        configs: [
          { name: 'FxOk', props: [{ name: 'label', tsType: 'string' }] },
          { name: 'FxBad', props: [{ name: 'ghost', tsType: 'string' }] },
        ],
        vue: { FxOk: sfc({ label: 'string' }), FxBad: sfc({ label: 'string' }) },
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stdout).toContain('✅ [FxOk] PASS')
    expect(run.stderr).toContain('❌ [FxBad] NAME-SET PARITY MISMATCH')
    expect(run.stdout).toContain('audit:binding-config-parity — 1/2 PASS, 1 FAIL')
  })

  it('同一组件名集与联合体**同时**违例 → 两类都印（union 段不因名集已红而被吞）', () => {
    const run = runGate(
      build({
        configs: [
          {
            name: 'FxWidget',
            props: [{ name: 'color', tsType: "'red' | 'green'" }, { name: 'ghost', tsType: 'string' }],
          },
        ],
        vue: { FxWidget: sfc({ color: 'Color' }, "type Color = 'red' | 'blue'\n") },
      }),
      GATE,
    )
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('NAME-SET PARITY MISMATCH')
    expect(run.stderr).toContain('In config but NOT in SFC defineProps: ghost')
    expect(run.stderr).toContain('[color] UNION-BODY MISMATCH')
    // 同时红时**不**再打第二个 ❌ 标题行（namePassed 为 false ⇒ 只印明细）
    expect(run.stdout).not.toContain('✅ [FxWidget] PASS')
  })

  it('全绿 → exit 0 且 summary 的 FAIL 计数为 0（阴性对照：summary 不是恒真文案）', () => {
    const run = runGate(
      build({
        configs: [
          { name: 'FxOk', props: [{ name: 'label', tsType: 'string' }] },
          { name: 'FxAlsoOk', props: [{ name: 'size', tsType: 'string' }] },
        ],
        vue: { FxOk: sfc({ label: 'string' }), FxAlsoOk: sfc({ size: 'string' }) },
      }),
      GATE,
    )
    expectGateGreen(run, { contains: ['audit:binding-config-parity — 2/2 PASS, 0 FAIL'] })
  })

  it('config 模块整个不存在 → 非零崩溃（fail-closed），⛔ 不是静默 exit 0', () => {
    // ⚠️ 如实登记：这是**静态 import 解析失败**的崩溃式 fail-closed，不是判据红。
    // 它与前面几条闸的 ENOENT 同类：分母没了不会变成绿。
    const root = createGateFixture({ gate: GATE, prefix: 'binding-parity-fx-nocfg' })
    const run = runGate(root, GATE)
    expect(run.status).not.toBe(0)
    expect(`${run.stderr}${run.stdout}`).toMatch(/ERR_MODULE_NOT_FOUND|Cannot find module/)
  })
})
