// INFRA-F129 (b)：root 是 Figma wrapper 时，视觉 paint 取具名子节点。
// 判据 / 作用面 / 诚实边界的唯一真源 = scripts/lib/paint-source-node.mjs 头注释。
//
// ⚠️ 本文件里的 fixture 不是判据真源。fixture 与该头注释冲突时，**改 fixture**，
// 别改实现（memory `plan-fixture-is-not-the-criterion-source`）。
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { paintSourceNode } from '../scripts/lib/paint-source-node.mjs'

const solid = (hex: string, extra: Record<string, unknown> = {}) => ({
  type: 'SOLID', visible: true, opacity: 1, blendMode: 'NORMAL', hex, ...extra,
})

/** Tooltips 的真实形态：wrapper root（无 paint）+ SLOT "Content"（带 paint）+ arrow 容器。 */
function tooltipVariant(overrides: Record<string, unknown> = {}) {
  return {
    name: 'dark theme=on, pointing=left down',
    type: 'COMPONENT',
    layoutMode: 'VERTICAL',
    gap: -6,
    w: 81,
    h: 26.48528289794922,
    fills: [],
    strokes: [],
    strokeWeight: 1,
    children: [
      {
        id: '1408:17039',
        name: 'Content',
        type: 'SLOT',
        visible: true,
        w: 81,
        h: 24,
        r: 4,
        fills: [solid('#353535')],
        strokes: [solid('#595959')],
        children: [],
      },
      {
        id: '1669:25910',
        name: 'Frame 2465',
        type: 'FRAME',
        visible: true,
        w: 81,
        h: 8.485281944274902,
        fills: [],
        strokes: [],
        children: [],
      },
    ],
    ...overrides,
  }
}

const TOOLTIPS_TARGET = { figmaName: 'Tooltips', expectedPaintFromChildName: 'Content' }

describe('paintSourceNode — 声明了子节点名', () => {
  it('返回那个具名子节点，而不是 root（Tooltips 形态）', () => {
    const node = paintSourceNode(tooltipVariant(), TOOLTIPS_TARGET)
    expect(node.id).toBe('1408:17039')
    expect(node.name).toBe('Content')
    expect(node.fills[0].hex).toBe('#353535')
    expect(node.strokes[0].hex).toBe('#595959')
  })

  it('root 自己的空 fills/strokes 不参与 —— 这正是那个假 null 的来源', () => {
    const variant = tooltipVariant()
    expect(variant.fills).toEqual([])
    expect(variant.strokes).toEqual([])
    const node = paintSourceNode(variant, TOOLTIPS_TARGET)
    expect(node.fills).not.toEqual([])
  })

  it('按 name 逐字相等匹配 —— 不做前缀 / 大小写 / 模糊匹配', () => {
    for (const wrong of ['content', 'Content ', 'Conten', 'Contents']) {
      expect(() => paintSourceNode(tooltipVariant(), { ...TOOLTIPS_TARGET, expectedPaintFromChildName: wrong }))
        .toThrow(/命中 0 个/)
    }
  })

  it('只看直接子节点 —— 埋在孙层的同名节点不算', () => {
    const variant = tooltipVariant({
      children: [
        {
          name: 'Wrapper',
          type: 'FRAME',
          visible: true,
          fills: [],
          strokes: [],
          children: [{ name: 'Content', type: 'SLOT', visible: true, fills: [solid('#353535')], strokes: [] }],
        },
      ],
    })
    expect(() => paintSourceNode(variant, TOOLTIPS_TARGET)).toThrow(/命中 0 个/)
  })
})

describe('paintSourceNode — fail closed（⛔ 不准静默回落到 root）', () => {
  it('命中 0 个 → throw，且消息列出实际可见直接子节点（便于定位 Figma 改名）', () => {
    const variant = tooltipVariant({
      children: [{ name: 'Body', type: 'SLOT', visible: true, fills: [solid('#353535')], strokes: [] }],
    })
    expect(() => paintSourceNode(variant, TOOLTIPS_TARGET)).toThrow(/命中 0 个/)
    expect(() => paintSourceNode(variant, TOOLTIPS_TARGET)).toThrow(/SLOT "Body"/)
    expect(() => paintSourceNode(variant, TOOLTIPS_TARGET)).toThrow(/Tooltips/)
  })

  it('命中 ≥2 个 → 同样 throw（歧义不猜）', () => {
    const dup = { name: 'Content', type: 'SLOT', visible: true, fills: [solid('#111111')], strokes: [] }
    const variant = tooltipVariant({ children: [dup, { ...dup }] })
    expect(() => paintSourceNode(variant, TOOLTIPS_TARGET)).toThrow(/命中 2 个/)
  })

  it('一个可见直接子节点都没有 → throw 且消息说明这一点', () => {
    expect(() => paintSourceNode(tooltipVariant({ children: [] }), TOOLTIPS_TARGET))
      .toThrow(/无可见直接子节点/)
  })

  it('visible:false 的同名子节点不算命中（隐藏节点不渲染）', () => {
    const variant = tooltipVariant({
      children: [{ name: 'Content', type: 'SLOT', visible: false, fills: [solid('#353535')], strokes: [] }],
    })
    expect(() => paintSourceNode(variant, TOOLTIPS_TARGET)).toThrow(/命中 0 个/)
  })

  it('恰好 1 个可见 + 1 个隐藏同名 → 取那个可见的，不 throw', () => {
    const variant = tooltipVariant({
      children: [
        { name: 'Content', type: 'SLOT', visible: false, fills: [solid('#000000')], strokes: [] },
        { name: 'Content', type: 'SLOT', visible: true, fills: [solid('#353535')], strokes: [] },
      ],
    })
    expect(paintSourceNode(variant, TOOLTIPS_TARGET).fills[0].hex).toBe('#353535')
  })
})

describe('paintSourceNode — 未声明时零影响（守住其余 900+ entry）', () => {
  it('target 没有 expectedPaintFromChildName → 逐字返回 variant 本身（同一对象引用）', () => {
    const variant = tooltipVariant()
    expect(paintSourceNode(variant, { figmaName: 'Tooltips' })).toBe(variant)
  })

  it('target 为 undefined / 空对象也回落到 variant，不抛', () => {
    const variant = tooltipVariant()
    expect(paintSourceNode(variant, undefined as never)).toBe(variant)
    expect(paintSourceNode(variant, {})).toBe(variant)
  })

  it('即便 variant 有一个名叫 Content 的子节点，未声明就不下降', () => {
    const variant = tooltipVariant()
    expect(paintSourceNode(variant, {}).fills).toEqual([])
  })
})

describe('paintSourceNode — 对活源的钉子（⛔ 别用 fixture 代替它）', () => {
  const tokenized = JSON.parse(fs.readFileSync(
    path.resolve(__dirname, '../figma-data/normalized/components-tokenized/tooltips__1408_17152.json'),
    'utf8',
  ))

  it('真 extract 数据里，12 个 Tooltips 变体全部恰好有一个可见的 SLOT "Content"', () => {
    expect(tokenized.variants).toHaveLength(12)
    for (const variant of tokenized.variants) {
      const node = paintSourceNode(variant, TOOLTIPS_TARGET)
      expect(node.type, variant.name).toBe('SLOT')
      expect(node.fills.length, variant.name).toBeGreaterThan(0)
      expect(node.strokes.length, variant.name).toBeGreaterThan(0)
    }
  })

  it('且 root 自己确实无 paint —— 证明「expected null 是比错节点」而非「extract 缺数据」', () => {
    for (const variant of tokenized.variants) {
      expect(variant.fills, variant.name).toEqual([])
      expect(variant.strokes, variant.name).toEqual([])
    }
  })

  // 这一条是「gap: -6 不是缺数据」的算术证据：root 的高度正好等于气泡 + 箭头 + 那个负 gap。
  // ⚠️ 精度取 5 位小数不是 6：Figma 的 h 是浮点，实测残差 ~9.5e-7（26.485281944… vs 26.485282897…），
  // 卡到 1e-7 会把浮点噪声读成「算术不闭合」。
  it('root 的 gap 是真值：SLOT h + arrow h + gap === root h（*-down 那批）', () => {
    const downs = tokenized.variants.filter((v: { name: string }) => / down$/.test(v.name))
    expect(downs).toHaveLength(6)
    for (const variant of downs) {
      const slot = variant.children.find((c: { name: string }) => c.name === 'Content')
      const arrow = variant.children.find((c: { name: string }) => c.name !== 'Content')
      expect(variant.gap, variant.name).toBe(-6)
      expect(slot.h + arrow.h + variant.gap, variant.name).toBeCloseTo(variant.h, 5)
    }
  })

  // 阴性对照：上面那条若换成一个**假的** gap，算术必须不闭合 —— 否则那条断言是空过。
  it('阴性对照：把 gap 换成 0，算术就不闭合（证明上一条不是恒真）', () => {
    const variant = tokenized.variants.find((v: { name: string }) => / down$/.test(v.name))
    const slot = variant.children.find((c: { name: string }) => c.name === 'Content')
    const arrow = variant.children.find((c: { name: string }) => c.name !== 'Content')
    expect(slot.h + arrow.h + 0).not.toBeCloseTo(variant.h, 5)
  })
})
