import { describe, it, expect } from 'vitest'
import { probeI2 } from '../scripts/audit-mockup-integrity.mjs'

/**
 * §M-INTEGRITY §I2 — probeI2() regression guard.
 *
 * Bug fixed 2026-08-05 (V4-2333 走查轮二十五, located 轮二十二): probeI2() collected every
 * top-level SECTION twice — once via walk(page, null) (which already recurses into
 * page.children with parent = page) and once via a second explicit pass over
 * page.children. The i/j pair loop then compared each top-level Section WITH ITSELF,
 * so I2 reported one phantom FAIL per top-level Section on a perfectly clean page.
 * The dedupe could not filter them: a self-pair's key is `X|X`, which is unique.
 *
 * These tests are the 双向探针 the fix needs: a clean page MUST report zero and a genuinely
 * overlapping page MUST still report the overlap (guards the fix from having silently
 * disabled the check). One direction alone would pass for the wrong reason — an audit that
 * always returns 0 satisfies the first test.
 *
 * ⚠️ Scope limit — measured 2026-08-05, do not overstate what this file locks: the
 * "clean page -> 0" direction guards the SYMPTOM, not the bug. Re-injecting the
 * double-collection while KEEPING the `o.aId !== o.bId` guard leaves all six tests GREEN
 * (verified by mutation: bug+guard = 6 pass, bug without guard = 6 fail), because the
 * guard absorbs the self-pairs. So this file cannot detect a re-introduced
 * double-collection — only the loud failure mode it used to cause. The redundancy itself
 * is guarded by the NOTE at the walk() call site plus review, not by these tests.
 */

type N = {
  id: string
  name: string
  type: string
  absoluteBoundingBox?: { x: number; y: number; width: number; height: number }
  children?: N[]
}

const bbox = (x: number, y: number, width: number, height: number) => ({ x, y, width, height })

const section = (id: string, x: number, y: number, w: number, h: number, children: N[] = []): N => ({
  id,
  name: `Section ${id}`,
  type: 'SECTION',
  absoluteBoundingBox: bbox(x, y, w, h),
  children,
})

const frame = (id: string, x: number, y: number, w: number, h: number): N => ({
  id,
  name: `Frame ${id}`,
  type: 'FRAME',
  absoluteBoundingBox: bbox(x, y, w, h),
})

const page = (children: N[]): N => ({ id: '0:1', name: 'Page', type: 'CANVAS', children })

describe('probeI2 — sibling SECTION overlap', () => {
  it('reports zero on a clean page with several non-overlapping top-level Sections', () => {
    const res = probeI2(page([section('1:1', 0, 0, 100, 100), section('2:2', 0, 200, 100, 100), section('3:3', 0, 400, 100, 100)]))
    expect(res.overlaps).toEqual([])
    expect(res.pass).toBe(true)
  })

  it('never pairs a Section with itself (the double-collection regression)', () => {
    const res = probeI2(page([section('1:1', 0, 0, 100, 100), section('2:2', 0, 200, 100, 100)]))
    expect(res.overlaps.filter(o => o.aId === o.bId)).toEqual([])
  })

  it('still reports a genuine overlap between two top-level Sections', () => {
    const res = probeI2(page([section('1:1', 0, 0, 100, 100), section('2:2', 0, 60, 100, 100)]))
    expect(res.pass).toBe(false)
    expect(res.overlaps).toHaveLength(1)
    expect([res.overlaps[0].aId, res.overlaps[0].bId].sort()).toEqual(['1:1', '2:2'])
    expect(res.overlaps[0].overlapWxH).toBe('100.0x40.0')
  })

  it('covers nested child Sections, and only compares actual siblings', () => {
    // two children of the SAME parent Section overlap -> must be reported
    const nestedOverlap = page([
      section('1:1', 0, 0, 500, 500, [section('1:10', 0, 0, 100, 100), section('1:11', 0, 60, 100, 100)]),
    ])
    const hit = probeI2(nestedOverlap)
    expect(hit.pass).toBe(false)
    expect([hit.overlaps[0].aId, hit.overlaps[0].bId].sort()).toEqual(['1:10', '1:11'])

    // a child Section geometrically inside its own parent is NOT a sibling pair -> zero
    const parentChild = page([section('1:1', 0, 0, 500, 500, [section('1:10', 10, 10, 100, 100)])])
    expect(probeI2(parentChild).overlaps).toEqual([])
  })

  it('ignores non-SECTION nodes', () => {
    const res = probeI2(page([section('1:1', 0, 0, 100, 100), frame('9:9', 0, 60, 100, 100)]))
    expect(res.overlaps).toEqual([])
  })

  it('reports one entry per overlapping pair, not one per collection pass', () => {
    const res = probeI2(page([section('1:1', 0, 0, 100, 100), section('2:2', 0, 50, 100, 100), section('3:3', 0, 90, 100, 100)]))
    const keys = res.overlaps.map(o => [o.aId, o.bId].sort().join('|')).sort()
    expect(keys).toEqual(['1:1|2:2', '1:1|3:3', '2:2|3:3'])
    expect(new Set(keys).size).toBe(keys.length)
  })

  /**
   * §I2 ③ 具名量的分母钉（2026-08-28 加，[[INFRA-F142]] / STATUS §一「§I2 输出契约」）。
   *
   * 病根不是「数报错了」，是**绿档只印 `[page X] pass`、连分母都不印** —— §I2 实证段逐字
   * 记的是：handoff 作者只能自己填一个 `overlap 0`，而那个 0 代表哪个量、到底扫没扫，
   * 读者分不出来。CLI 的汇总行由 `sectionCount` 求和得出，所以这个字段一旦被删/恒 0，
   * 绿档会退回「印了个 0 但没有分母」的原状 —— 而**判据本身仍然全绿，没有任何闸会发现**。
   *
   * ⚠️ 这两条钉的是「分母是真的」，不是「重叠算得对」（那由上面六条管）。
   * 判据取**终态事实**：`sectionCount` 必须随输入里 SECTION 的实际数目变，
   * 且必须把嵌套层数进去 —— 恒等于 `page.children.length` 的实现会被第二条打红。
   */
  it('carries a real SECTION denominator (guards the green-path "0 without a denominator" bug)', () => {
    const clean = probeI2(page([section('1:1', 0, 0, 100, 100), section('2:2', 0, 200, 100, 100), section('3:3', 0, 400, 100, 100)]))
    expect(clean.pass).toBe(true)
    expect(clean.overlaps).toEqual([])
    expect(clean.sectionCount).toBe(3) // ⛔ 绿档也要有分母，否则 handoff 里的 0 无从解释

    const empty = probeI2(page([frame('9:9', 0, 0, 10, 10)]))
    expect(empty.sectionCount).toBe(0) // 「扫了 0 个」与「扫了 3 个都干净」必须可区分
  })

  it('counts nested Sections in the denominator, not just top-level children', () => {
    const nested = page([section('1:1', 0, 0, 500, 500, [section('1:10', 10, 10, 100, 100), section('1:11', 10, 200, 100, 100)])])
    // 顶层 children 只有 1 个 ⇒ 恒用 page.children.length 的实现会在这里红
    expect(nested.children!.length).toBe(1)
    expect(probeI2(nested).sectionCount).toBe(3)
  })
})
