// M32.1「实例必须来自指定设计库」判据的自测（纯函数，不打网络）。
//
// 判据真源 = scripts/lib/mockup-library-origin.mjs，由两个消费方共用：
//   · scripts/audit-mockup-library-origin.mjs（闸，单文件 exit 1）
//   · scripts/ds-health-scan-mockups.mjs（DIM-U12 时间序列观测器，always exit 0）
// 抽出共享真源的意义就是两边同口径 ⇒ 本文件同时钉住四类实例的归属与**分母口径**。

import { describe, expect, it } from 'vitest'
// @ts-expect-error — plain ESM JS lib without .d.ts
import { collectInstances, classifyInstances, foreignRate } from '../scripts/lib/mockup-library-origin.mjs'

const doc = {
  type: 'FRAME',
  children: [
    { type: 'INSTANCE', id: '1:1', name: 'btn', componentId: 'c-designated' },
    { type: 'FRAME', children: [{ type: 'INSTANCE', id: '1:2', name: 'icon', componentId: 'c-foreign' }] },
    { type: 'INSTANCE', id: '1:3', name: 'card', componentId: 'c-local' },
    { type: 'INSTANCE', id: '1:4', name: 'ghost', componentId: 'c-missing' },
    { type: 'TEXT', characters: 'not an instance' },
  ],
}

const componentsMap = {
  'c-designated': { key: 'KEY-DS', remote: true, name: 'Button' },
  'c-foreign': { key: 'KEY-PP', remote: true, name: 'icon/PP/play' },
  'c-local': { key: 'KEY-LOCAL', remote: false, name: 'ProductCard' },
}

const designated = new Set(['KEY-DS'])

describe('mockup 库归属判据', () => {
  it('递归收集 INSTANCE（含嵌套），非 INSTANCE 不收', () => {
    expect(collectInstances(doc).map((n: any) => n.name)).toEqual(['btn', 'icon', 'card', 'ghost'])
  })

  it('四类归属：指定库 / 外来库 / file-local / 解析不到', () => {
    const c = classifyInstances(collectInstances(doc), componentsMap, designated)
    expect(c).toMatchObject({ scanned: 4, resolved: 3, unresolved: 1, local: 1, designated: 1 })
    expect(c.flagged).toEqual([{ instanceName: 'icon', componentName: 'icon/PP/play', key: 'KEY-PP', nodeId: '1:2' }])
  })

  // nodeId 是 2026-08-14 加的**纯 additive** 字段（清理清单要给可点的 Figma 深链）。
  // 这条钉住「加它没有改判据」：四类归属的计数与上面那条逐字相同，flagged 仍只有那一个。
  it('nodeId 是 additive —— 带回实例 id，但四类归属一字未变', () => {
    const c = classifyInstances(collectInstances(doc), componentsMap, designated)
    expect(c.flagged.map((f: any) => f.nodeId)).toEqual(['1:2'])
    expect({ scanned: c.scanned, resolved: c.resolved, unresolved: c.unresolved, local: c.local, designated: c.designated })
      .toEqual({ scanned: 4, resolved: 3, unresolved: 1, local: 1, designated: 1 })
  })

  it('⛔ 分母不含 file-local —— 加再多 file-local 实例都不改变错误率', () => {
    const base = classifyInstances(collectInstances(doc), componentsMap, designated)
    const padded = {
      ...base,
      children: undefined,
      children_: undefined,
      local: base.local + 50,
      scanned: base.scanned + 50,
      resolved: base.resolved + 50,
    }
    expect(foreignRate(padded)).toBe(foreignRate(base))
    expect(foreignRate(base)).toBe(0.5) // 1 外来 / (1 指定 + 1 外来)
  })

  it('无可判归属的 remote 实例 ⇒ n/a（null），⛔ 别当 0% 读', () => {
    const onlyLocal = classifyInstances(
      [{ name: 'card', componentId: 'c-local' }] as any,
      componentsMap,
      designated,
    )
    expect(onlyLocal.designated + onlyLocal.flagged.length).toBe(0)
    expect(foreignRate(onlyLocal)).toBeNull()
  })

  it('全部来自指定库 ⇒ 0（真的 0，与 n/a 可区分）', () => {
    const clean = classifyInstances(
      [{ name: 'btn', componentId: 'c-designated' }] as any,
      componentsMap,
      designated,
    )
    expect(foreignRate(clean)).toBe(0)
  })

  // ── 手搓 FRAME 阴性对照（2026-09-01）─────────────────────────────────────
  // 这两条**不是**在测「判据能抓到手搓」——它抓不到。它们把**抓不到**这件事钉成断言，
  // 因为这条失明的方向是**单调反的**：手搓得越彻底，本判据输出越绿。
  // 注释会漂，断言不会 ⇒ 谁将来「顺手扩一下 collectInstances」，这两条会当场红，
  // 逼他去读 lib 头部那段「为什么故意不修成顺手扫 FRAME」。
  const handBuiltButton = {
    type: 'FRAME',
    name: '手搓交付帧',
    children: [
      {
        type: 'FRAME',
        id: '9:1',
        name: 'Button — Reboot', // 看起来像按钮，实际不是任何组件的实例
        children: [
          { type: 'VECTOR', id: '9:2', name: 'icon' },
          { type: 'TEXT', id: '9:3', characters: 'Reboot' },
        ],
      },
      { type: 'RECTANGLE', id: '9:4', name: '自画分隔线' },
    ],
  }

  it('🔴 手搓 FRAME 假按钮：一个都收不到 —— 本判据对它结构性失明', () => {
    expect(collectInstances(handBuiltButton)).toEqual([])
  })

  it('🔴 全手搓 ⇒ flagged 空且分母为 0 ⇒ 「绿」= 没验，⛔ 不是通过', () => {
    const c = classifyInstances(collectInstances(handBuiltButton), componentsMap, designated)
    expect(c.flagged).toHaveLength(0)
    // 与「全部来自指定库」那条对照着读：那边 foreignRate 是真的 0，这边是 n/a。
    // 两者在 flagged.length 上完全一样（都是 0）—— 这正是消费方不能只看 flagged 的原因。
    expect(c.designated + c.flagged.length).toBe(0)
    expect(foreignRate(c)).toBeNull()
  })

  it('remote 缺省 / 为 false 一律按 file-local 放行（M36）', () => {
    const map = { x: { key: 'K', name: 'Whatever' } } // 没有 remote 字段
    const c = classifyInstances([{ name: 'n', componentId: 'x' }] as any, map, designated)
    expect(c.local).toBe(1)
    expect(c.flagged).toHaveLength(0)
  })
})
