import { describe, it, expect } from 'vitest'
import {
  // @ts-expect-error — plain JS ESM audit script, no types
  classifyHashNSemantic, hashNumbersIn, declarationsIn, collectUnits, SEMANTIC_VALUES,
} from '../scripts/audit-mockup-hash-n-semantic.mjs'

// helper: Figma-REST-shaped tree. `texts` 变成同一个 SECTION 下的兄弟 TEXT 节点 ——
// 与真交付形态一致（带标号的产品帧与写声明的注释卡是兄弟，不是祖孙）。
function section(id: string, name: string, texts: string[]) {
  return {
    id, name, type: 'SECTION',
    children: texts.map((characters, i) => ({ id: `${id}:t${i}`, name: `t${i}`, type: 'TEXT', characters })),
  }
}
function doc(sections: any[]) {
  return { id: '0:0', name: 'Document', type: 'DOCUMENT', children: [
    { id: '0:1', name: 'Page 1', type: 'CANVAS', children: sections },
  ] }
}

describe('audit-mockup-hash-n-semantic — 值域闭集', () => {
  it('闭集逐字就是 §M40 语义类型表里那两个值，⛔ 没有第三个', () => {
    expect([...SEMANTIC_VALUES]).toEqual(['sequence', 'name'])
  })
})

describe('hashNumbersIn — 防线 ① hex / 防线 ② 单个标号', () => {
  it('must-hit：`Slot #1` / `Slot #3` 认出 1 和 3', () => {
    expect([...hashNumbersIn('Slot #1 · Slot #3')].sort()).toEqual([1, 3])
  })

  it('🔴 阴性对照（防线 ①）：hex 颜色 `#2FB54E` ⛔ 不得被当成标号 `#2`', () => {
    expect([...hashNumbersIn('accent = #2FB54E, danger = #EA4233')]).toEqual([])
  })

  it('🔴 阴性对照：3 位以上数字（ticket 号形态）⛔ 不当标号', () => {
    expect([...hashNumbersIn('see #123 and #4567')]).toEqual([])
  })

  it('hex 与真标号混在同一段时，只认真标号', () => {
    expect([...hashNumbersIn('fill #2FB54E on Session #1 and Session #2')].sort()).toEqual([1, 2])
  })
})

describe('declarationsIn — 声明行解析', () => {
  it('must-hit：合法值 + 括号里的中文解释不参与判定', () => {
    expect(declarationsIn('#N semantic: sequence（自动生成 · 删除后重排）')).toEqual([
      { value: 'sequence', ok: true },
    ])
    expect(declarationsIn('#N semantic: name（手动输入 · 删除后不改后续序号）')).toEqual([
      { value: 'name', ok: true },
    ])
  })

  it('must-hit：值域外的词被认出且标 ok=false', () => {
    expect(declarationsIn('#N semantic: auto')).toEqual([{ value: 'auto', ok: false }])
  })

  it('🔴 阴性对照：mockup 里到处都有的列头 `Name` ⛔ 不得被当成声明', () => {
    expect(declarationsIn('| Name | Status | Bitrate |')).toEqual([])
    expect(declarationsIn('Name')).toEqual([])
  })
})

describe('collectUnits — 单位是 SECTION（声明与标号是兄弟）', () => {
  it('同一 SECTION 下的兄弟 TEXT 归进同一个单位', () => {
    const units = collectUnits(doc([section('1:1', 'Delivery', ['Slot #1', 'Slot #2'])]))
    const s = units.find((u: any) => u.unitId === '1:1')
    expect(s.texts.map((t: any) => t.characters)).toEqual(['Slot #1', 'Slot #2'])
  })
})

describe('classifyHashNSemantic — 主判据', () => {
  it('must-hit M40-A：≥2 个不同标号 + 无声明 ⇒ 违反，且点名该单位', () => {
    const { violations, scanned } = classifyHashNSemantic(
      doc([section('1:1', 'RPSV3 Slots', ['Slot #1', 'Slot #2', 'Slot #3'])]),
    )
    expect(scanned).toBe(1)
    expect(violations).toHaveLength(1)
    expect(violations[0].probe).toBe('M40-A')
    expect(violations[0].unitName).toBe('RPSV3 Slots')
    expect(violations[0].why).toContain('#1')
  })

  it('must-not-hit：有合法声明 ⇒ 零违反（分母仍计 1）', () => {
    const { violations, scanned } = classifyHashNSemantic(
      doc([section('1:1', 'RPSV3 Slots', [
        'Slot #1', 'Slot #2',
        '#N semantic: sequence（自动生成 · 删除后重排）',
      ])]),
    )
    expect(scanned).toBe(1)
    expect(violations).toEqual([])
  })

  it('must-hit M40-B：声明值在闭集外 ⇒ 违反（且**不看**有没有标号）', () => {
    const { violations, scanned } = classifyHashNSemantic(
      doc([section('1:1', 'Card only', ['#N semantic: auto'])]),
    )
    expect(scanned).toBe(1)
    expect(violations.map((v: any) => v.probe)).toEqual(['M40-B'])
    expect(violations[0].why).toContain('auto')
  })

  it('🔴 阴性对照（防线 ②）：只有一个 `#1` ⇒ ⛔ 不触发', () => {
    const { violations, scanned } = classifyHashNSemantic(
      doc([section('1:1', 'One only', ['Version #1'])]),
    )
    expect(scanned).toBe(0)
    expect(violations).toEqual([])
  })

  it('🔴 阴性对照（防线 ①）：整段只有 hex 颜色 ⇒ ⛔ 不触发', () => {
    const { violations, scanned } = classifyHashNSemantic(
      doc([section('1:1', 'Palette', ['#2FB54E', '#30B54E', '#EA4233'])]),
    )
    expect(scanned).toBe(0)
    expect(violations).toEqual([])
  })

  it('🔴 分母口径：完全没有标号也没有声明的单位 ⛔ 不进分母（没判过它）', () => {
    const { scanned } = classifyHashNSemantic(
      doc([section('1:1', 'Plain', ['Bitrate', 'Status'])]),
    )
    expect(scanned).toBe(0)
  })

  it('声明写在兄弟注释卡里也算 —— 单位是 SECTION，不是 frame', () => {
    const tree = doc([{
      id: '1:1', name: 'Delivery', type: 'SECTION',
      children: [
        { id: '1:2', name: 'Product frame', type: 'FRAME', children: [
          { id: '1:3', name: 'row', type: 'TEXT', characters: 'Session #1' },
          { id: '1:4', name: 'row', type: 'TEXT', characters: 'Session #2' },
        ] },
        { id: '1:5', name: 'Design Rules', type: 'FRAME', children: [
          { id: '1:6', name: 'card', type: 'TEXT', characters: '#N semantic: name（手动输入 · 删除后不改后续序号）' },
        ] },
      ],
    }])
    const { violations, scanned } = classifyHashNSemantic(tree)
    expect(scanned).toBe(1)
    expect(violations).toEqual([])
  })

  it('两个 SECTION 各判各的 —— 一个声明了 ⛔ 不替另一个背书', () => {
    const { violations } = classifyHashNSemantic(doc([
      section('1:1', 'Declared', ['Slot #1', 'Slot #2', '#N semantic: sequence']),
      section('2:1', 'Undeclared', ['Stream #1', 'Stream #2']),
    ]))
    expect(violations).toHaveLength(1)
    expect(violations[0].unitName).toBe('Undeclared')
  })
})
