import { describe, it, expect } from 'vitest'
// @ts-expect-error — .mjs 无类型声明，本仓其它 audit 测试同形
import { adjudicateByDeclaration, indexNodes } from '../scripts/report-geometry-rows-backlog.mjs'

/**
 * 「声明证据分档」的判据面（`report-geometry-rows-backlog.mjs`）。
 *
 * 它回答 owner 2026-08-27 那一问：「能不能自动根据规则判定，减少人工参与」。
 * 答案 = **能，但只有一档**。这里钉住那一档的边界，防它悄悄扩大。
 *
 * ⛔ **本文件测的是「Figma 自己怎么声明的」，⛔ 不是「这个设计好不好」。**
 *    分档读的全是设计师写下的属性（`itemSpacing` / `layoutMode` /
 *    `layoutPositioning` / `layoutSizingVertical`）——
 *    `FIGMA_AS_SOURCE_OF_TRUTH.md` §验证方法 要求 deterministic、禁启发式，本档合规。
 *
 * ⚠️ 覆盖边界如实登记：这是**纯函数单测**，⛔ 覆盖不到接线
 *    （memory `gate-regression-face-whole-script-fixture`）。可接受的理由是
 *    `report-geometry-rows-backlog.mjs` **无挂载、是 report 不是闸**（always exit 0），
 *    因此不在 `audit:gate-regression-ratchet` 的覆盖面里。⛔ 若哪天给它上了闸，
 *    这条理由就失效，要补整脚本 fixture harness。
 */

/** 造一棵最小树，让 indexNodes 能按 id 查到行节点自己的声明。 */
function treeOf(rows: Array<Record<string, unknown>>) {
  return indexNodes({ id: 'root', children: rows })
}

describe('A 档 —— 文件内部矛盾（唯一「不需要人判」的一档）', () => {
  it('容器声明 itemSpacing=16 而实际 gap 34、且无 ABSOLUTE 子 ⇒ A', () => {
    const f = {
      probe: 'R2', label: 'Frame 3071', layoutMode: 'VERTICAL', itemSpacing: 16,
      gaps: [16, 16, 34], nodes: [{ id: 'r1' }, { id: 'r2' }, { id: 'r3' }, { id: 'r4' }],
    }
    const v = adjudicateByDeclaration(f, treeOf([{ id: 'r1' }, { id: 'r2' }, { id: 'r3' }, { id: 'r4' }]))
    expect(v.tier).toBe('A')
    expect(v.why).toContain('itemSpacing=16')
    expect(v.why).toContain('34')
  })

  it('同样的矛盾，但有一个 ABSOLUTE 子来解释 ⇒ 降到 B（⛔ 不许还算 A）', () => {
    const f = {
      probe: 'R2', label: 'Frame 3071', layoutMode: 'VERTICAL', itemSpacing: 16,
      gaps: [16, 34], nodes: [{ id: 'r1' }, { id: 'r2' }, { id: 'r3' }],
    }
    const v = adjudicateByDeclaration(f, treeOf([
      { id: 'r1' }, { id: 'r2', layoutPositioning: 'ABSOLUTE' }, { id: 'r3' },
    ]))
    expect(v.tier).toBe('B')
    expect(v.why).toContain('ABSOLUTE')
  })

  it('gap 全等于声明的 itemSpacing ⇒ C（本条命中来自别的口径，⛔ 不许当矛盾报）', () => {
    const f = {
      probe: 'R2', label: 'x', layoutMode: 'VERTICAL', itemSpacing: 8,
      gaps: [8, 8], nodes: [{ id: 'r1' }, { id: 'r2' }, { id: 'r3' }],
    }
    expect(adjudicateByDeclaration(f, treeOf([{ id: 'r1' }])).tier).toBe('C')
  })

  it('亚像素抖动（16 vs 16.4）不算矛盾 ⇒ C，不是 A', () => {
    const f = {
      probe: 'R2', label: 'x', layoutMode: 'VERTICAL', itemSpacing: 16,
      gaps: [16, 16.4], nodes: [{ id: 'r1' }],
    }
    expect(adjudicateByDeclaration(f, treeOf([{ id: 'r1' }])).tier).toBe('C')
  })
})

describe('C 档 —— 无声明可比（⛔ 不许假装能判）', () => {
  it('容器不是 auto-layout ⇒ C', () => {
    const f = { probe: 'R2', label: 'Menu/2', layoutMode: null, itemSpacing: null, gaps: [4, 12], nodes: [] }
    const v = adjudicateByDeclaration(f, treeOf([]))
    expect(v.tier).toBe('C')
    expect(v.why).toContain('非 auto-layout')
  })

  it('R3（分隔线厚度/色值）恒 C —— auto-layout 不表达这个', () => {
    const f = { probe: 'R3', label: 'w', shapes: [], distinctColours: [], thicknessSpread: 1 }
    expect(adjudicateByDeclaration(f, treeOf([])).tier).toBe('C')
  })

  it('R1 但快照里查不到离群行节点 ⇒ C（fail-honest，不猜）', () => {
    const f = { probe: 'R1', label: 'x', modeHeight: 40, heights: [40, 41], nodes: [{ id: 'missing' }] }
    expect(adjudicateByDeclaration(f, treeOf([{ id: 'other' }])).tier).toBe('C')
  })
})

describe('R1 的 B / A2 —— 行高是内容驱动还是被人写死的', () => {
  it('离群行 HUG ⇒ B（行高由内容决定，不等高是该声明的预期结果）', () => {
    const f = { probe: 'R1', label: 'session-list', modeHeight: 72, heights: [72, 48], nodes: [{ id: 'r9' }] }
    const v = adjudicateByDeclaration(f, treeOf([{ id: 'r9', layoutSizingVertical: 'HUG' }]))
    expect(v.tier).toBe('B')
    expect(v.why).toContain('HUG')
  })

  it('离群行全 FIXED ⇒ A2，且理由必须自带「仍可能是有意」的保留', () => {
    const f = { probe: 'R1', label: 'Frame 1994', modeHeight: 35, heights: [35, 36], nodes: [{ id: 'r1' }] }
    const v = adjudicateByDeclaration(f, treeOf([{ id: 'r1', layoutSizingVertical: 'FIXED' }]))
    expect(v.tier).toBe('A2')
    // ⛔ A2 不是结论档 —— 理由里必须留着这个保留，否则它会被当成 A 用
    expect(v.why).toContain('仍可能是有意')
  })

  it('FIXED 与 HUG 混合 ⇒ B（有 HUG 就有内容驱动的解释，⛔ 不许升成 A2）', () => {
    const f = { probe: 'R1', label: 'x', modeHeight: 40, heights: [40, 52], nodes: [{ id: 'a' }, { id: 'b' }] }
    const v = adjudicateByDeclaration(f, treeOf([
      { id: 'a', layoutSizingVertical: 'FIXED' }, { id: 'b', layoutSizingVertical: 'HUG' },
    ]))
    expect(v.tier).toBe('B')
  })

  it('离群行 ABSOLUTE ⇒ B，且优先于 sizing 判断', () => {
    const f = { probe: 'R1', label: 'x', modeHeight: 40, heights: [40, 90], nodes: [{ id: 'a' }] }
    const v = adjudicateByDeclaration(f, treeOf([
      { id: 'a', layoutPositioning: 'ABSOLUTE', layoutSizingVertical: 'FIXED' },
    ]))
    expect(v.tier).toBe('B')
    expect(v.why).toContain('ABSOLUTE')
  })

  it('sizing 字段缺失 ⇒ C（⛔ 缺数据不等于 FIXED，别默认升档）', () => {
    const f = { probe: 'R1', label: 'x', modeHeight: 40, heights: [40, 41], nodes: [{ id: 'a' }] }
    expect(adjudicateByDeclaration(f, treeOf([{ id: 'a' }])).tier).toBe('C')
  })
})
