import { describe, it, expect } from 'vitest'
import {
  // @ts-expect-error — plain JS ESM audit script, no types
  isReferenceName, isProductFrameName, PRODUCT_FRAME_RE, classifyRegion,
  findLanguageIsolationViolations, fontFamilyOf, ZH_FONT,
} from '../scripts/audit-mockup-language-isolation.mjs'

// helper: Figma-REST-shaped TEXT node.
function text(id: string, name: string, characters: string, fontFamily = 'Roboto') {
  return { id, type: 'TEXT', name, characters, style: { fontFamily } }
}
function frame(id: string, name: string, children: any[]) {
  return { id, type: 'FRAME', name, children }
}

describe('isReferenceName — 参考素材子树识别', () => {
  // 🔴 这条是本闸设计的地基：ZH ref 被 audit-mockup-bilingual-spacing.mjs:77-78 逐字
  // 列为 REFERENCE 子树。判错 ⇒ 二分下它会落进 product ⇒ 闸第一次跑就误伤
  // owner 2026-07-09 明确批准的中文参考旁注（实测节点 7687:1203 name = "ZH ref caption"）。
  it('命中活树里的真实命名 "ZH ref caption"（无 · 分隔符）', () => {
    expect(isReferenceName('ZH ref caption')).toBe(true)
  })

  it('命中头注释枚举的四种形态', () => {
    expect(isReferenceName('REFERENCE — Config-T Network')).toBe(true)
    expect(isReferenceName('ref-caption · V4-2333')).toBe(true)
    expect(isReferenceName('param-table · LCD')).toBe(true)
    expect(isReferenceName('ZH ref · 参数说明')).toBe(true)
  })

  it('⛔ 不误吃产品 UI 里正常的英文术语（TVU 终端真有这类 label）', () => {
    expect(isReferenceName('Reference Signal')).toBe(false)
    expect(isReferenceName('reference-clock')).toBe(false)
    expect(isReferenceName('Preference')).toBe(false)
  })
})

describe('isProductFrameName — 产品帧正向识别（§M33，owner 2026-09-15 拍定 SCREEN · 前缀）', () => {
  it('命中标记形态', () => {
    expect(isProductFrameName('SCREEN · Config-T Network')).toBe(true)
    expect(isProductFrameName('SCREEN · LCD Home')).toBe(true)
  })

  // 🔴 大小写敏感是硬要求：活树实测 `screen` 作普通产品词出现 58 次。
  // 带 /i 会把 `full screen-2ch` 这类真产品帧整棵误判 —— 方向与 REFERENCE 那条同源。
  it('⛔ 大小写敏感：小写 screen 不命中', () => {
    expect(isProductFrameName('screen · Config-T')).toBe(false)
    expect(isProductFrameName('Screen · Config-T')).toBe(false)
  })

  // review F2（2026-09-15）：上面两条只能从样本行为**间接**推断没加 /i —— 将来有人手滑
  // 加上 /i，这两条会红得莫名其妙（"screen · Config-T 怎么变 true 了"），得自己倒推半天
  // 才想到是 flags 变了。直接钉 flags 更抗改：谁改了 flags，这条断言直接指名道姓。
  it('⛔ 直接钉住 PRODUCT_FRAME_RE 没有 /i（大小写敏感是 binding 约束，不是默认行为）', () => {
    expect(PRODUCT_FRAME_RE.flags).toBe('')
  })

  it('⛔ 不误吃活树里真实存在的普通产品命名', () => {
    expect(isProductFrameName('full screen-2ch')).toBe(false)
    expect(isProductFrameName('Multi full screen/Audio lock to Input 1')).toBe(false)
    expect(isProductFrameName('SCREENSHOT reference')).toBe(false)  // 无 · 分隔符
  })

  // 🔴 C1（2026-09-15 最终评审）：§M33 Acceptance 逐字写「匹配器容忍中点前后空白有无
  // （`SCREEN·X` 也过）」，而当时的 `/^SCREEN\s*·\s/` 中点**后**要求至少一个空白 ⇒
  // 照文档那句话命名的人会拿到一个**看不见的漏判**（静默不判，正是本闸最怕的失败方向）。
  // 裁定（依 Ruling R9「宽松朝多判方向失败=安全方向，收紧会制造新的静默不判」）：
  // 改正则去兑现文档，⛔ 不是改文档迁就正则。本条把这个容忍度逐样本钉死 ——
  // 两边（正则 / §M33 那句话）是一对必须同步的副本，谁收紧了正则，这里直接红。
  it('🔴 中点前后空白有无都命中（容忍度与 §M33 文字逐字一致）', () => {
    expect(isProductFrameName('SCREEN · X')).toBe(true)    // 规范写法
    expect(isProductFrameName('SCREEN·X')).toBe(true)      // 前后都无空白
    expect(isProductFrameName('SCREEN ·X')).toBe(true)     // 只有中点前有
    expect(isProductFrameName('SCREEN· X')).toBe(true)     // 只有中点后有
    expect(isProductFrameName('SCREEN  ·  X')).toBe(true)  // 两侧多个空白
  })
})

describe('classifyRegion — 二分：标记内 product / 标记外 unmarked', () => {
  it('根与未标记帧都是 unmarked', () => {
    expect(classifyRegion('Frame 29', 'unmarked', 'FRAME')).toBe('unmarked')
    expect(classifyRegion('PRD · V4-1864', 'unmarked', 'FRAME')).toBe('unmarked')
  })

  it('SCREEN · 帧切 product，并向下继承', () => {
    expect(classifyRegion('SCREEN · Config-T Network', 'unmarked', 'FRAME')).toBe('product')
    expect(classifyRegion('body', 'product', 'TEXT')).toBe('product')
  })

  // 🔴 刻意：product 子树内**不再**放 annotation / reference 逃逸。
  // §M33 逐字「在产品 frame 里叠加的 ZH 翻译 sticky note = ❌ 不合规」
  // ⇒ 让它逃逸等于把该抓的那一类静默豁免掉。真有例外走 --allow-node。
  it('⛔ product 子树内 annotation / reference 命名不逃逸', () => {
    expect(classifyRegion('UX · Delivery Card', 'product', 'FRAME')).toBe('product')
    expect(classifyRegion('ZH ref caption', 'product', 'TEXT')).toBe('product')
    expect(classifyRegion('REFERENCE — old page', 'product', 'FRAME')).toBe('product')
  })

  // 🔴 C2（2026-09-15 最终评审）：只有**容器**类型能起一个 product 子树。
  // 设备屏幕在 Figma 里只可能是 FRAME/SECTION/COMPONENT/COMPONENT_SET/INSTANCE/GROUP；
  // 一个**叫** `SCREEN · X` 的 TEXT 说明文字不是屏幕，让它起子树 ⇒ markedFrames 被顶上去
  // ⇒ unverified 与空分母告警同时失效（见 findLanguageIsolationViolations 那块的回归用例）。
  it('🔴 只有容器类型能起 product 子树：TEXT / VECTOR 叫 SCREEN · 也不算', () => {
    expect(classifyRegion('SCREEN · Config-T Network', 'unmarked', 'TEXT')).toBe('unmarked')
    expect(classifyRegion('SCREEN · Config-T Network', 'unmarked', 'VECTOR')).toBe('unmarked')
    // ⛔ 缺省 nodeType 刻意不默认成 FRAME —— 默认成容器等于把类型闸悄悄关掉。
    expect(classifyRegion('SCREEN · Config-T Network', 'unmarked', undefined)).toBe('unmarked')
  })

  it('六种容器类型都能起 product 子树', () => {
    for (const t of ['FRAME', 'SECTION', 'COMPONENT', 'COMPONENT_SET', 'INSTANCE', 'GROUP']) {
      expect(classifyRegion('SCREEN · LCD Home', 'unmarked', t)).toBe('product')
    }
  })
})

describe('findLanguageIsolationViolations — 未标记一律不判', () => {
  it('没有任何 SCREEN · 帧 ⇒ markedFrames=0 且 scannedNodes=0、零违例', () => {
    const tree = frame('0:1', 'Page', [
      frame('0:2', 'PRD · V4-1864', [text('0:3', 'body', '中文注释')]),
      frame('0:4', 'Frame 29', [text('0:5', 'label', '中文也不判')]),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.markedFrames).toBe(0)
    expect(r.scannedNodes).toBe(0)
    expect(r.violations).toHaveLength(0)
    // 🔴 R1（owner 裁定，2026-09-15）：brief 原给 toBe(2)，但对所有未标记 FRAME 计数时
    // 这棵树的 root '0:1' 自己也是 FRAME ⇒ root + '0:2' + '0:4' = 3，不是 2。
    expect(r.unmarkedSkipped).toBe(3)
  })

  it('SCREEN · 帧内的中文 ⇒ 报 product-ui-han', () => {
    const tree = frame('0:1', 'Page', [
      frame('0:2', 'SCREEN · Config-T Network', [
        text('0:3', 'title', '2 路通道未选择 Receiver'),
        text('0:4', 'ok', 'Confirm'),
      ]),
      frame('0:5', 'PRD · card', [text('0:6', 'zh', '这一段不该被判')]),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.markedFrames).toBe(1)
    expect(r.scannedNodes).toBe(2)
    expect(r.violations.map((v: any) => v.nodeId)).toEqual(['0:3'])
    expect(r.violations[0].probe).toBe('product-ui-han')
  })

  it('SCREEN · 帧内用注释字体但无中文 ⇒ 报 product-ui-zh-font', () => {
    const tree = frame('0:1', 'Page', [
      frame('0:2', 'SCREEN · LCD', [text('0:3', 'label', 'Confirm', ZH_FONT)]),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(1)
    expect(r.violations[0].probe).toBe('product-ui-zh-font')
  })

  it('--allow-node 仍能豁免 product 区个别节点', () => {
    const tree = frame('0:1', 'Page', [
      frame('0:2', 'SCREEN · LCD', [text('0:3', 'title', '中文')]),
    ])
    const r = findLanguageIsolationViolations(tree, { allowNodeIds: new Set(['0:3']) })
    expect(r.violations).toHaveLength(0)
    expect(r.allowedSkipped).toBe(1)
  })

  // 🔴 C2 回归（2026-09-15 最终评审现取复现）：`markedFrames` 曾对**任意** node type 计数 ⇒
  // 一个叫 `SCREEN · Config-T Network` 的**说明 TEXT** 就能把它顶成 1，于是
  //   · 引擎的 `unverified` 不再触发（它的条件是 markedFrames === 0）
  //   · 「0 个标记帧」的空分母告警也不响
  // 而同页那个真含中文、却漏标的设备帧照样不判 ⇒ **一片不携带信息的绿，违例就在旁边**。
  // 一个假标记同时关掉两个诚实信号 —— 正是本分支要防的那件事。
  it('🔴 叫 SCREEN · X 的 TEXT ⛔ 不算已标记帧（markedFrames 必须是 0，⛔ 不是 1）', () => {
    const tree = frame('0:1', 'Page', [
      // 说明文字，名字长得像标记但它是 TEXT，不可能是一块设备屏幕
      text('0:2', 'SCREEN · Config-T Network', '这一节讲 Config-T 网络页'),
      // 旁边就是真含中文、却漏标的设备帧
      frame('0:3', 'Config-T Network', [text('0:4', 'title', '2 路通道未选择 Receiver')]),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.markedFrames).toBe(0)   // ⛔ 顶上去就等于把 unverified 关掉
    expect(r.scannedNodes).toBe(0)   // 漏标帧仍不判（fail-open，如实）
    expect(r.violations).toHaveLength(0)
  })
})

// 🔴 R2（owner 裁定，2026-09-15）：本块 8 项原先全部假设旧三分语义（product 是兜底档），
// 判据反转成二分（product 要 SCREEN · 标记，默认 unmarked）后整块会红，逐项重写如下。
// 保留的 4 项意图：P1 单独红 / P2 单独红 / 二者不重复计 / --allow-node 如实计数。
// 改写的 2 项语义：「annotation 区不报」⇒「未标记区不报」；
//                「reference 区不报」⇒「SCREEN · 子树内 reference/annotation 命名不再逃逸，仍报」。
describe('findLanguageIsolationViolations — 造故障（每臂单独红/绿）', () => {
  it('对照臂：SCREEN · 帧内全英文 ⇒ 0 违例，分母如实', () => {
    const tree = frame('root', 'SCREEN · Device Screen', [
      text('t1', 'title', 'Network Settings'),
      text('t2', 'btn', 'Skip'),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(0)
    expect(r.scannedNodes).toBe(2)
  })

  it('P1 product-ui-han：SCREEN · 帧内混入中文 ⇒ 红（§M33 反例 1/2）', () => {
    const tree = frame('root', 'SCREEN · Device Screen', [
      text('t1', 'dialog-title', '2 channels have no Receiver  2 路通道未选择 Receiver'),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(1)
    expect(r.violations[0].probe).toBe('product-ui-han')
    expect(r.violations[0].han).toBeGreaterThan(0)
  })

  it('P2 product-ui-zh-font：SCREEN · 帧内用注释字体但无中文 ⇒ 红（§M33 反例 3）', () => {
    const tree = frame('root', 'SCREEN · Device Screen', [
      text('t1', 'label', 'Skip', ZH_FONT),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(1)
    expect(r.violations[0].probe).toBe('product-ui-zh-font')
  })

  it('P1 / P2 不重复计：含中文且用 ZH 字体 ⇒ 只报 P1 一条', () => {
    const tree = frame('root', 'SCREEN · Device', [text('t1', 'x', '跳过', ZH_FONT)])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(1)
    expect(r.violations[0].probe).toBe('product-ui-han')
  })

  // ⚠️ 改写点 1：旧语义「annotation 区不报」不再成立 —— 现在根本没有 annotation 档。
  // 即使帧名长得像注释卡（`UX · …`），只要它不在 SCREEN · 子树下就是 unmarked，
  // 不判的原因是"没标记"，不是"识别出这是注释卡"。
  it('未标记区的中文 ⇒ 不报（不是因为识别出 annotation，是因为它根本没标记）', () => {
    const tree = frame('root', 'UX · Delivery Card', [
      text('t1', 'body', 'Changes\n本轮改动'),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(0)
    expect(r.markedFrames).toBe(0)
    expect(r.scannedNodes).toBe(0)
  })

  // ⚠️ 改写点 2（判据反转的核心行为变化）：旧语义「reference 区不报」被明确废止 ——
  // §M33 反例 3 要求 product frame 内任何注释语言都该被抓，放 reference 命名逃逸
  // 等于把该抓的那一类静默豁免掉。同一段中文、同一个 "ZH ref caption" 命名，
  // 唯一的区别是它现在挂在 SCREEN · 子树下 ⇒ 读数从「不报」翻成「报」。
  it('🔴 SCREEN · 子树内的 reference 命名不再逃逸 ⇒ 仍报（判据反转点）', () => {
    const tree = frame('root', 'SCREEN · Device Screen', [
      text('t1', 'ZH ref caption', '参考：旧版设置页', ZH_FONT),
      text('t2', 'title', 'Network Settings'),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(1)
    expect(r.violations[0].probe).toBe('product-ui-han')
    expect(r.violations[0].nodeId).toBe('t1')
    expect(r.scannedNodes).toBe(2) // t1 / t2 都进了分母，reference 命名不再豁免
  })

  it('--allow-node 豁免落在 product 区，且如实计数', () => {
    const tree = frame('root', 'SCREEN · Device', [text('t1', 'x', '待办')])
    const r = findLanguageIsolationViolations(tree, { allowNodeIds: new Set(['t1']) })
    expect(r.violations).toHaveLength(0)
    expect(r.allowedSkipped).toBe(1)
    expect(r.scannedNodes).toBe(0) // 豁免的不进分母 —— 否则「0 违例 / 分母 1」是假绿
  })

  // 原「嵌套：annotation 卡里的 reference 子树不污染 product 分母」在二分下已经没有
  // annotation/reference 两档可嵌套，改造成等价意图：相邻的 SCREEN · 子树与未标记子树
  // 分别独立计数，互不污染 —— 这才是二分下真正对应的隔离风险点。
  it('嵌套：SCREEN · 子树与未标记子树分别独立计数，互不污染', () => {
    const tree = frame('root', 'canvas', [
      frame('a', 'UX · Card', [text('a1', 'body', '说明文字')]),
      frame('p', 'SCREEN · Device', [text('p1', 'btn', 'Save')]),
    ])
    const r = findLanguageIsolationViolations(tree)
    expect(r.violations).toHaveLength(0)
    expect(r.markedFrames).toBe(1)
    expect(r.scannedNodes).toBe(1) // 只有 p1 进了分母，a1 因未标记不判
  })
})

describe('fontFamilyOf — 形态兼容', () => {
  it('读 REST 的 style.fontFamily', () => {
    expect(fontFamilyOf({ style: { fontFamily: 'Roboto' } })).toBe('Roboto')
  })
  it('回落到 Plugin API 形态的 fontName.family', () => {
    expect(fontFamilyOf({ fontName: { family: ZH_FONT } })).toBe(ZH_FONT)
  })
  it('取不到 ⇒ 空串（⛔ 不抛，避免一个缺字段的节点炸掉整轮）', () => {
    expect(fontFamilyOf({})).toBe('')
    expect(fontFamilyOf(null)).toBe('')
  })
})
