// `detectFrameTheme` 的钉子 —— 判据真源 = `scripts/lib/detect-frame-theme.mjs` 头注释
// （其上游 = `docs/internal/mockup-conventions.md` §M23.18 代码块）。
//
// ⚠️ **一套测试全绿 ⛔ 不构成判别力证据。** 这批钉子的判别力有两层实证：
//   ① lab 侧对应量具做过 11 臂故障注入（分档 / 递归上限 / 纯白谓词 / 画序 / luma 系数
//      各改坏一次，对应钉子变红，还原后回归全绿）——
//      读数 `ai-ds-lab/runs/detect-frame-theme/{fault-injection,tiered-fault-injection}.json`。
//   ② 移植后与 lab 已发表读数在 **8481 个真实 Figma frame** 上 6 字段逐字段比对 = 0 差异
//      （lab `probes/detect-frame-theme/verify-ds-port.mjs`，含源码级注入 5 臂阴性对照）。
// ⇒ 本文件钉的是**语义**；「这套语义在真实面上是什么分布」在 lab 侧，⛔ 不在这里。
//
// `expected` 一律**从 §M23.18 的判据推出**，⛔ 不是跑出来再回填。
// `luma` 注释是 IEEE double 下的实算值，写出来是为了让阈值边界那几条可复核 ——
// ⛔ 它不参与断言。

import { describe, expect, it } from 'vitest'
import {
  CONTAIN_TOLERANCE_PX,
  DARK_THRESHOLD,
  GATE_ARM_DEFAULT,
  LIGHT_THRESHOLD,
  MAX_BACKDROP_DEPTH,
  boxContains,
  coversFrame,
  detectFrameTheme,
  detectFrameThemeForGate,
  detectFrameThemeTiered,
  detectFrameThemeTieredDetailed,
  detectFrameThemeTieredStrict,
  isBackdropChild,
  isExactWhite,
  isFillBound,
  resolveTiered,
  tierOf,
  topmostOpaqueSolidPaint,
} from '../scripts/lib/detect-frame-theme.mjs'

type Node = Record<string, unknown> | null | undefined
type Theme = 'light' | 'dark' | 'unknown'

const solidFill = (r: number, g: number, b: number, extra: Record<string, unknown> = {}) => ({
  type: 'SOLID', color: { r, g, b }, ...extra,
})
/** 只有 fills 的裸节点（§M23.18 判定核心只看这个）。 */
const solid = (r: number, g: number, b: number, extra: Record<string, unknown> = {}) => ({
  fills: [solidFill(r, g, b, extra)],
})

// ══ 1. §M23.18 三态判定：23 条 fixture ═══════════════════════════════════════

describe('§M23.18 三态判定（只读 fills[0]）', () => {
  const cases: Array<{ name: string; node: Node; expected: Theme; why: string }> = [
    // ── 三态 ──
    { name: 'dark-frame-141414', node: solid(0.078, 0.078, 0.078), expected: 'dark',
      why: '深色 --bg-layer1 #141414 ⇒ luma 0.078 < 0.25' },
    { name: 'light-frame-ffffff', node: solid(1, 1, 1), expected: 'light',
      why: '浅色 --bg-layer1 #FFFFFF ⇒ luma ≈ 1 > 0.85' },
    { name: 'mid-gray-0.50', node: solid(0.5, 0.5, 0.5), expected: 'unknown',
      why: '中间灰度 ⇒ 落在 [0.25, 0.85] 的 unknown 口子里' },

    // ── 阈值边界：钉 `>` / `<` 的**严格性**，⛔ 不是 `>=` / `<=` ──
    { name: 'luma-exactly-0.85', node: solid(0.505, 1, 0.9825), expected: 'unknown',
      why: '判据是 `luma > 0.85`（严格）⇒ 恰好 0.85 ⛔ 不是 light；此三元组在 IEEE double 下恰等于 0.85' },
    { name: 'luma-just-above-0.85', node: solid(0.505, 1, 0.9835), expected: 'light',
      why: 'luma 0.850114 ⇒ 越过上阈值（与上一条构成 must-hit / must-not-hit 对）' },
    { name: 'luma-exactly-0.25', node: solid(0, 0.248, 0.916), expected: 'unknown',
      why: '判据是 `luma < 0.25`（严格）⇒ 恰好 0.25 ⛔ 不是 dark' },
    { name: 'luma-just-below-0.25', node: solid(0, 0.248, 0.915), expected: 'dark',
      why: 'luma 0.249886 ⇒ 越过下阈值' },
    { name: 'luma-just-above-0.25', node: solid(0, 0.248, 0.917), expected: 'unknown',
      why: 'must-not-hit：刚过下阈值只能回 unknown，⛔ 不得被判成 light 或 dark' },
    { name: 'gray-0.30-inside-unknown-band', node: solid(0.3, 0.3, 0.3), expected: 'unknown',
      why: '带内下段 ⇒ 若下阈值被抬到 0.35，这条必红' },
    { name: 'gray-0.80-inside-unknown-band', node: solid(0.8, 0.8, 0.8), expected: 'unknown',
      why: '带内上段 ⇒ 若上阈值被压到 0.75，这条必红' },

    // ── 通道权重：钉 0.299 / 0.587 / 0.114 各自的位置 ──
    { name: 'green-dominant-near-white', node: solid(0.55, 1, 1), expected: 'light',
      why: 'r 低 g/b 高 ⇒ 真系数下 0.865 > 0.85；r↔g 对调后只有 0.736 ⇒ 那条改坏必红' },
    { name: 'pure-blue', node: solid(0, 0, 1), expected: 'dark',
      why: '蓝权重最小 ⇒ 纯蓝是 dark；g↔b 对调后为 0.587（unknown）⇒ 那条改坏必红' },

    // ── fills 缺失 / 非 SOLID / 空节点 ──
    { name: 'fills-key-missing', node: { name: 'AFTER · Some Page', children: [] }, expected: 'unknown',
      why: '节点上压根没有 fills 字段（透明 / 继承背景的 frame）' },
    { name: 'fills-empty-array', node: { fills: [] }, expected: 'unknown',
      why: 'fills 存在但为空数组 ⇒ fills[0] 取不到' },
    { name: 'fill-gradient-linear',
      node: { fills: [{ type: 'GRADIENT_LINEAR', gradientStops: [{ color: { r: 1, g: 1, b: 1, a: 1 } }] }] },
      expected: 'unknown', why: '非 SOLID：GradientPaint 身上没有 `.color`（颜色在 gradientStops 里）' },
    { name: 'fill-image', node: { fills: [{ type: 'IMAGE', imageHash: 'abc123', scaleMode: 'FILL' }] },
      expected: 'unknown', why: '非 SOLID：ImagePaint 身上没有 `.color`' },
    { name: 'fills-not-an-array-mixed', node: { fills: Symbol('figma.mixed') as unknown }, expected: 'unknown',
      why: 'fills 不是数组（真实 Figma 里是 figma.mixed 这个 Symbol）⇒ ⛔ 不得被当成有 fill' },
    { name: 'empty-node-null', node: null, expected: 'unknown',
      why: '空节点 null —— ⚠️ §M23.18 伪代码原文会在这里抛 TypeError，见模块头注释「偏离一」' },
    { name: 'empty-node-undefined', node: undefined, expected: 'unknown', why: '空节点 undefined（同上）' },
    { name: 'empty-node-bare-object', node: {}, expected: 'unknown', why: '空节点 `{}`' },

    // ── 盲区留痕（⚠️ 钉的是 §M23.18 **当前**的行为，⛔ 不是在为它背书）──
    { name: 'blindspot-invisible-white-fill', node: solid(1, 1, 1, { visible: false }), expected: 'light',
      why: '⚠️ 盲区：⛔ 不看 `visible` ⇒ 隐藏的白 fill 照样判 light。真实面上见异常 light 先查这一条' },
    { name: 'blindspot-zero-opacity-white-fill', node: solid(1, 1, 1, { opacity: 0 }), expected: 'light',
      why: '⚠️ 盲区：⛔ 不看 `opacity` ⇒ 全透明的白 fill 照样判 light' },
    { name: 'stacked-fills-only-index-0',
      node: { fills: [solidFill(1, 1, 1), solidFill(0, 0, 0)] }, expected: 'light',
      why: '⚠️ 盲区：只读 fills[0]，同节点其余 paint 一律不参与' },
  ]

  it.each(cases)('$name ⇒ $expected', ({ node, expected }) => {
    expect(detectFrameTheme(node)).toBe(expected)
  })

  it('fixture 面自身的判别力：三态都出现过（⛔ 不许全是同一个值）', () => {
    const tally = new Set(cases.map((c) => c.expected))
    expect([...tally].sort()).toEqual(['dark', 'light', 'unknown'])
  })

  it('🔴 阈值是导出的常量，⛔ 不是散落的字面量', () => {
    expect(LIGHT_THRESHOLD).toBe(0.85)
    expect(DARK_THRESHOLD).toBe(0.25)
  })
})

// ══ 2. 分档（⛔ 只看 frame 自身 fills[0]）════════════════════════════════════

const BOX = { x: 0, y: 0, width: 100, height: 100 }
const WHITE = () => solidFill(1, 1, 1)
const NAVY = () => solidFill(0.05, 0.08, 0.2)
const CREAM = () => solidFill(0.97, 0.96, 0.94)
/** frame：默认 100×100。 */
const frame = (o: Record<string, unknown> = {}) => ({
  id: 'f:1', name: 'Frame', absoluteBoundingBox: { ...BOX }, ...o,
})
/** 铺满 frame 的子节点（box 与 frame 等大 ⇒ 包含成立）。 */
const cover = (o: Record<string, unknown> = {}) => ({
  id: 'c:1', name: 'Cover', absoluteBoundingBox: { ...BOX }, ...o,
})

describe('分档 tierOf', () => {
  it('T1 —— fills[0] 非纯白 ⇒ 信 frame 自己的 fill', () => {
    expect(tierOf(frame({ fills: [NAVY()] }))).toBe('T1')
    expect(tierOf(frame({ fills: [CREAM()] }))).toBe('T1')
  })

  it('T1 —— 纯白但 paint 绑了变量 ⇒ 信它（实测该档 precision 100%）', () => {
    const f = frame({ fills: [{ ...WHITE(), boundVariables: { color: { type: 'VARIABLE_ALIAS', id: 'V:1' } } }] })
    expect(tierOf(f)).toBe('T1')
  })

  it('T1 —— 纯白但节点整体绑了 fills ⇒ 信它', () => {
    const f = frame({ fills: [WHITE()], boundVariables: { fills: [{ type: 'VARIABLE_ALIAS', id: 'V:2' }] } })
    expect(tierOf(f)).toBe('T1')
    expect(isFillBound(f, (f as { fills: unknown[] }).fills[0])).toBe(true)
  })

  it('T2 —— 恰为纯白且未绑变量 ⇒ ⛔ 不信它（实测该档 precision 仅 28.7%）', () => {
    expect(tierOf(frame({ fills: [WHITE()] }))).toBe('T2')
  })

  it('🔴 纯白是**精确**相等，⛔ 不近似 —— #fffffe 落 T1', () => {
    expect(isExactWhite({ r: 1, g: 1, b: 1 })).toBe(true)
    expect(isExactWhite({ r: 1, g: 1, b: 254 / 255 })).toBe(false)
    expect(tierOf(frame({ fills: [solidFill(1, 1, 254 / 255)] }))).toBe('T1')
  })

  it('T2 第二支 —— fills 缺失 / 空 / 非 SOLID / 无 color', () => {
    expect(tierOf(frame())).toBe('T2')
    expect(tierOf(frame({ fills: [] }))).toBe('T2')
    expect(tierOf(frame({ fills: [{ type: 'GRADIENT_LINEAR' }] }))).toBe('T2')
    expect(tierOf(frame({ fills: [{ type: 'SOLID' }] }))).toBe('T2')
  })

  it('分档 ⛔ 不看子树 —— 挂上铺底子层不改变档位（边界⑤）', () => {
    expect(tierOf(frame({ fills: [NAVY()], children: [cover({ fills: [WHITE()] })] }))).toBe('T1')
  })

  it('🔴 T3 ⛔ 不由 tierOf 产生 —— 它只回 T1/T2', () => {
    const seen = new Set(
      [frame({ fills: [NAVY()] }), frame({ fills: [WHITE()] }), frame(), null, undefined]
        .map((n) => tierOf(n)),
    )
    expect([...seen].sort()).toEqual(['T1', 'T2'])
    expect(resolveTiered(frame({ fills: [WHITE()] })).tier).toBe('T3')
  })
})

// ══ 3. T1 档：委托真源，⛔ 不探子图层 ════════════════════════════════════════

describe('T1 档', () => {
  it('🔴 T1 逐字等于 §M23.18 原判定，即使底下压着一个深色铺底', () => {
    const f = frame({ fills: [CREAM()], children: [cover({ fills: [NAVY()] })] })
    expect(detectFrameThemeTiered(f)).toBe(detectFrameTheme(f))
    expect(detectFrameThemeTiered(f)).toBe('light')
    expect(detectFrameThemeTieredDetailed(f).backdropDepth).toBe(null)
  })
})

// ══ 4. T2 档：递归探铺底 ═════════════════════════════════════════════════════

describe('T2 档：递归探铺底', () => {
  it('递归第 1 层 —— 直接子节点铺底', () => {
    const d = detectFrameThemeTieredDetailed(frame({ fills: [WHITE()], children: [cover({ fills: [NAVY()] })] }))
    expect(d.tier).toBe('T2')
    expect(d.backdropDepth).toBe(1)
    expect(d.verdict).toBe('dark')
  })

  it('🔴 递归第 2 层 —— 铺满但自身无涂层的容器要往里探（只看直接子节点会漏的那一格）', () => {
    const group = cover({ id: 'g:1', name: 'Group', children: [cover({ id: 'c:2', fills: [NAVY()] })] })
    const d = detectFrameThemeTieredDetailed(frame({ fills: [WHITE()], children: [group] }))
    expect(d.backdropDepth).toBe(2)
    expect(d.verdict).toBe('dark')
  })

  it('递归第 3 层可达（上限内）', () => {
    const mid = cover({ id: 'g:2', children: [cover({ id: 'c:3', fills: [NAVY()] })] })
    const d = detectFrameThemeTieredDetailed(
      frame({ fills: [WHITE()], children: [cover({ id: 'g:1', children: [mid] })] }),
    )
    expect(d.backdropDepth).toBe(3)
    expect(d.verdict).toBe('dark')
  })

  it('🔴 第 4 层 ⛔ 不可达 —— 上限 3 是写死的边界，越界记 T3', () => {
    const l3 = cover({ id: 'g:3', children: [cover({ id: 'c:4', fills: [NAVY()] })] })
    const l2 = cover({ id: 'g:2', children: [l3] })
    const d = detectFrameThemeTieredDetailed(
      frame({ fills: [WHITE()], children: [cover({ id: 'g:1', children: [l2] })] }),
    )
    expect(d.tier).toBe('T3')
    expect(d.backdropDepth).toBe(null)
    expect(MAX_BACKDROP_DEPTH).toBe(3)
  })

  it('画序 —— 最顶层的铺底赢（children[n-1] 在上）', () => {
    const d = detectFrameThemeTieredDetailed(frame({
      fills: [WHITE()],
      children: [cover({ id: 'lo', fills: [NAVY()] }), cover({ id: 'hi', fills: [CREAM()] })],
    }))
    expect(d.childId).toBe('hi')
    expect(d.verdict).toBe('light')
  })

  it('🔴 顶层容器里探不到 ⇒ 继续看更低的兄弟，⛔ 不是就此放弃', () => {
    const d = detectFrameThemeTieredDetailed(frame({
      fills: [WHITE()],
      children: [cover({ id: 'lo', fills: [NAVY()] }), cover({ id: 'hi', children: [] })],
    }))
    expect(d.childId).toBe('lo')
    expect(d.verdict).toBe('dark')
  })

  it('🔴 判定语义 100% 来自真源 —— 探到中间灰度铺底时也必须回 unknown', () => {
    const mid = frame({ fills: [WHITE()], children: [cover({ fills: [solidFill(0.5, 0.5, 0.5)] })] })
    expect(detectFrameThemeTiered(mid)).toBe('unknown')
    expect(detectFrameThemeTieredDetailed(mid).tier).toBe('T2')
  })
})

// ══ 5. 铺满资格（几何 / 可见 / 不透明）════════════════════════════════════════

describe('铺满资格', () => {
  it('⛔ 没铺满的子节点不算铺底', () => {
    const small = { id: 'c:s', absoluteBoundingBox: { x: 10, y: 10, width: 50, height: 50 }, fills: [NAVY()] }
    expect(detectFrameThemeTieredDetailed(frame({ fills: [WHITE()], children: [small] })).tier).toBe('T3')
  })

  it(`容差 ${CONTAIN_TOLERANCE_PX}px：差 0.4px 算铺满，差 0.6px 不算`, () => {
    const near = (d: number) => ({ id: 'c:n', absoluteBoundingBox: { x: d, y: 0, width: 100, height: 100 }, fills: [NAVY()] })
    expect(coversFrame(near(0.4), BOX)).toBe(true)
    expect(coversFrame(near(0.6), BOX)).toBe(false)
  })

  it('⛔ 隐藏 / 半透明的子节点不算铺底', () => {
    const hidden = cover({ visible: false, fills: [NAVY()] })
    const faded = cover({ opacity: 0.5, fills: [NAVY()] })
    expect(detectFrameThemeTieredDetailed(frame({ fills: [WHITE()], children: [hidden] })).tier).toBe('T3')
    expect(detectFrameThemeTieredDetailed(frame({ fills: [WHITE()], children: [faded] })).tier).toBe('T3')
  })

  it('缺 bbox ⇒ fail closed（⛔ 不猜铺满）', () => {
    expect(coversFrame({ fills: [NAVY()] }, BOX)).toBe(false)
    expect(coversFrame(cover({ fills: [NAVY()] }), undefined)).toBe(false)
    expect(boxContains(undefined, BOX)).toBe(false)
    expect(boxContains(BOX, undefined)).toBe(false)
  })

  it('半透明 / 非 SOLID 的 paint 不算不透明涂层', () => {
    expect(topmostOpaqueSolidPaint([solidFill(0, 0, 0, { opacity: 0.5 })])).toBeUndefined()
    expect(topmostOpaqueSolidPaint([{ type: 'GRADIENT_LINEAR' }])).toBeUndefined()
    expect(topmostOpaqueSolidPaint([solidFill(0, 0, 0, { visible: false })])).toBeUndefined()
    expect(topmostOpaqueSolidPaint('not-an-array' as unknown)).toBeUndefined()
    // 画序：取最顶那一个
    expect(topmostOpaqueSolidPaint([solidFill(1, 1, 1), solidFill(0, 0, 0)])?.color).toEqual({ r: 0, g: 0, b: 0 })
  })
})

// ══ 6. 🔴 恒等式控制：coversFrame ∧ 有不透明 SOLID ⟺ isBackdropChild ═════════

/**
 * ⚠️ 刻意的**第二份独立实现**，只用于恒等式的另一侧 ——
 * ⛔ 不 import 模块里的 `topmostOpaqueSolidPaint`，否则这条恒等式变成自证。
 */
function topmostIndependent(fills: unknown): unknown {
  if (!Array.isArray(fills)) return undefined
  for (let i = fills.length - 1; i >= 0; i--) {
    const p = fills[i]
    if (!p || p.type !== 'SOLID') continue
    if (p.visible === false) continue
    if ((p.opacity ?? 1) !== 1) continue
    if ((p.color?.a ?? 1) !== 1) continue
    return p
  }
  return undefined
}

describe('恒等式控制', () => {
  it('🔴 coversFrame ∧ 有不透明 SOLID ⟺ isBackdropChild（两侧独立实现）', () => {
    const cases = [
      cover({ fills: [NAVY()] }),
      cover({ children: [] }),
      cover({ fills: [] }),
      cover({ visible: false, fills: [NAVY()] }),
      cover({ opacity: 0.5, fills: [NAVY()] }),
      cover({ fills: [solidFill(0, 0, 0, { opacity: 0.5 })] }),
      cover({ fills: [{ type: 'GRADIENT_LINEAR' }] }),
      { id: 'c:s', absoluteBoundingBox: { x: 10, y: 10, width: 5, height: 5 }, fills: [NAVY()] },
    ]
    let covered = 0
    for (const c of cases) {
      const left = coversFrame(c, BOX) && Boolean(topmostIndependent((c as { fills?: unknown }).fills))
      expect(left, `case ${c.id}`).toBe(isBackdropChild(c, BOX))
      if (isBackdropChild(c, BOX)) covered++
    }
    // ⚠️ 判别力：样本里必须**两侧都出现**，否则这条恒等式可以靠「全 false」恒真通过。
    expect(covered, `真假两侧都要有，实得 ${covered}/${cases.length}`).toBeGreaterThan(0)
    expect(covered).toBeLessThan(cases.length)
  })
})

// ══ 7. 两个臂 ══════════════════════════════════════════════════════════════════

describe('两个臂', () => {
  it('🔴 两个臂只在 T3 上不同：T1 / T2 档必须逐字全等', () => {
    const t1 = frame({ fills: [NAVY()] })
    const t2 = frame({ fills: [WHITE()], children: [cover({ fills: [NAVY()] })] })
    expect(detectFrameThemeTiered(t1)).toBe(detectFrameThemeTieredStrict(t1))
    expect(detectFrameThemeTiered(t2)).toBe(detectFrameThemeTieredStrict(t2))
  })

  it('🔴 T3：「猜一个」回落真源、「说不知道」判 unknown —— 这是两个臂的唯一分岔点', () => {
    const t3 = frame({ fills: [WHITE()] })
    expect(detectFrameThemeTieredDetailed(t3).tier).toBe('T3')
    expect(detectFrameThemeTiered(t3)).toBe('light') // 真源会说 light
    expect(detectFrameThemeTiered(t3)).toBe(detectFrameTheme(t3))
    expect(detectFrameThemeTieredStrict(t3)).toBe('unknown')
  })

  it('T3 且真源也判不出 ⇒ 两个臂都 unknown', () => {
    const t3 = frame()
    expect(detectFrameThemeTiered(t3)).toBe('unknown')
    expect(detectFrameThemeTieredStrict(t3)).toBe('unknown')
  })

  it('null / undefined 节点 ⇒ unknown（⛔ 不抛，沿用真源的 fail-open 取舍）', () => {
    expect(detectFrameThemeTiered(null)).toBe('unknown')
    expect(detectFrameThemeTiered(undefined)).toBe('unknown')
    expect(detectFrameThemeTieredStrict(null)).toBe('unknown')
  })
})

// ══ 8. 闸侧入口 ════════════════════════════════════════════════════════════════

describe('闸侧入口', () => {
  it('🔴 默认臂 = 「说不知道」（strict）—— 改这个默认值必须让这条钉子红', () => {
    expect(GATE_ARM_DEFAULT).toBe('strict')
    const t3 = frame({ fills: [WHITE()] })
    expect(detectFrameThemeForGate(t3)).toBe('unknown')
    expect(detectFrameThemeForGate(t3)).toBe(detectFrameThemeTieredStrict(t3))
  })

  it('显式指定 fallback 臂时走「猜一个」', () => {
    const t3 = frame({ fills: [WHITE()] })
    expect(detectFrameThemeForGate(t3, { arm: 'fallback' })).toBe('light')
    expect(detectFrameThemeForGate(t3, { arm: 'fallback' })).toBe(detectFrameThemeTiered(t3))
  })

  it('两个臂在 T1/T2 上，闸侧入口的两种取值也全等', () => {
    for (const n of [frame({ fills: [NAVY()] }), frame({ fills: [WHITE()], children: [cover({ fills: [NAVY()] })] })]) {
      expect(detectFrameThemeForGate(n, { arm: 'strict' })).toBe(detectFrameThemeForGate(n, { arm: 'fallback' }))
    }
  })
})
