import { describe, it, expect } from 'vitest'
import {
  blank,
  stripCssComments,
  scanFile,
  declaredCustomProps,
  varReferences,
  scanVarScope,
} from '../figma-sync/audit-no-hardcoded-design-tokens.mjs'

// INFRA-F83 回归钉子。病灶：三条正则原先直扫 <style> 原文，注释里出现
// `<属性>: … Npx` 就被判成硬编码；值等于既有 token 时是 exactMatch → **blocking**。
// 实证 = UserMenu.vue 的注释 "No margin: the 4px above and below comes from the
// panel's own gap" 让提交 exit 1，当时只把注释措辞改别扭绕开、闸没修。
//
// 分母刻意窄：只喂被测的那一小段 source + 手搓的 token 字典，不喂 src/ 全量与
// variables.css 全量。理由 = Task 3 的教训（窄分母用例原先喂整个 real.recipes，
// 加第二个配方时假失败两次）。这里同理：真实 19 条 noMatch 变动不该弄红本文件。
const dict = {
  spacingByValue: new Map([['4px', '--sp-xxs']]),
  radiusByValue: new Map<string, string>(),
  colorByValue: new Map<string, string[]>(),
  zIndexByValue: new Map<string, string[]>(),
}
const noAllowlist = new Set<string>()

const vue = (styleBody: string) =>
  `<template><div class="probe" /></template>\n<style scoped>\n${styleBody}\n</style>\n`

describe('stripCssComments', () => {
  it('保长度 + 保换行（行号才不会漂）', () => {
    const src = '.a {\n  /* margin: 4px\n     still comment */\n  display: block;\n}'
    const out = stripCssComments(src)
    expect(out).toHaveLength(src.length)
    expect(out.split('\n')).toHaveLength(src.split('\n').length)
  })

  it('注释内容真的被盖掉，注释外原样保留', () => {
    const out = stripCssComments('.a { /* margin: 4px */ display: block; }')
    expect(out).not.toMatch(/margin/)
    expect(out).toContain('display: block')
  })

  it('blank() 是通用的等长覆盖器', () => {
    expect(blank('aXXb', /XX/g)).toBe('a  b')
    expect(blank('a\nXb', /X/g)).toBe('a\n b')
  })
})

describe('scanFile — 注释 vs 真声明', () => {
  it('must-NOT-fire：注释里的 margin: 4px 不再报（F83 的原始形态）', () => {
    const source = vue(
      `.probe {\n  /* No margin: the 4px above and below comes from the panel's own gap */\n  display: block;\n}`,
    )
    expect(scanFile('src/components/Probe.vue', source, dict, noAllowlist)).toEqual([])
  })

  it('must-FIRE：真 CSS 里的 margin: 4px 照旧 blocking（别把判据一起削掉）', () => {
    const source = vue(`.probe { margin: 4px; }`)
    const found = scanFile('src/components/Probe.vue', source, dict, noAllowlist)
    expect(found).toHaveLength(1)
    expect(found[0]).toMatchObject({
      category: 'spacing',
      property: 'margin',
      value: '4px',
      exactMatch: true,
      matchedToken: '--sp-xxs',
    })
  })

  it('同一 block 里注释与真声明并存时，只报真声明', () => {
    const source = vue(
      `.probe {\n  /* margin: 4px is intentionally NOT set here */\n  margin: 4px;\n}`,
    )
    const found = scanFile('src/components/Probe.vue', source, dict, noAllowlist)
    expect(found).toHaveLength(1)
    expect(found[0].exactMatch).toBe(true)
  })

  it('剥注释后行号仍指真声明所在行（等长覆盖的真正目的）', () => {
    // <template>=1, <style scoped>=2, .probe {=3, 注释首行=4, 注释次行=5, margin=6
    const source = vue(
      `.probe {\n  /* a multi-line comment\n     mentioning margin: 4px */\n  margin: 4px;\n}`,
    )
    const found = scanFile('src/components/Probe.vue', source, dict, noAllowlist)
    expect(found).toHaveLength(1)
    expect(found[0].line).toBe(6)
  })

  it('allowlist 仍按 file:line:value 生效（剥注释没打乱这个键）', () => {
    const source = vue(`.probe { margin: 4px; }`)
    const line = scanFile('src/components/Probe.vue', source, dict, noAllowlist)[0].line
    const allowed = new Set([`src/components/Probe.vue:${line}:4px`])
    expect(scanFile('src/components/Probe.vue', source, dict, allowed)).toEqual([])
  })
})

// ── INFRA-F147 ② 回归钉子 ───────────────────────────────────────────────────
// 病灶（2026-09-10 一次真实 CI 红实测撞出）：本闸把「写成 var(--x)」判为合规，
// ⛔ 不检查 --x 在该文件作用域里解析得到。实证 = design-process-review.html 那页没有
// token 作用域，照本闸建议把 41 处字面量换成 var(--sp-l) 之类后闸 exact 0 判绿，
// 而页面塌了 189,643 / 4,608,000 像素（4.1155%）—— var() 解析不到 ⇒ 声明被整条丢弃。
// ⇒ 这些钉子钉的是**具名事实**（哪一种写法该红、哪一种不该），⛔ 不钉「解析器有没有抛异常」。
const HTML = (head: string, body = '') =>
  `<!doctype html><html><head>${head}</head><body>${body}</body></html>`
const page = (css: string, body = '') => HTML(`<style>\n${css}\n</style>`, body)
const PROBE = 'playground/public/__t.html'

describe('declaredCustomProps — 作用域采集', () => {
  it('一行多个声明必须全部收到', () => {
    // ⚠️ 这条钉的是我自己踩过的坑：先前用「行首 --x:」的 grep 口径数
    // design-process-review.html 得 15，真值 24 —— 差的 9 个全是「一行多个声明」。
    expect(declaredCustomProps(page(':root{--a:1px;--b:2px;--c:3px}')).size).toBe(3)
  })

  it('内联 style="" 里的声明也算作用域', () => {
    expect(declaredCustomProps(HTML('', '<div style="--x:1px"></div>')).has('--x')).toBe(true)
  })

  it('must-NOT-fire：注释里的声明⛔不算作用域（算进去就是假绿）', () => {
    const got = declaredCustomProps(page('/* --ghost:1px; */ :root{--real:2px}'))
    expect([...got]).toEqual(['--real'])
  })
})

describe('varReferences — 引用采集', () => {
  it('取到名字，并把带兜底值的标出来', () => {
    const got = varReferences(page('a{padding:var(--x);margin:var(--y, 4px)}'))
    expect(got.map((r) => [r.name, r.hasFallback])).toEqual([
      ['--x', false],
      ['--y', true],
    ])
  })

  it('must-NOT-fire：<style> 之外正文里的 var(--x) 字样⛔不是引用', () => {
    expect(varReferences(HTML('', '写法是 var(--sp-l) 这样'))).toEqual([])
  })
})

describe('scanVarScope — 独立分享页档的作用域判据', () => {
  it('must-FIRE：本文件没有定义 ⇒ blocking（这就是那次 4.12% 像素塌的形态）', () => {
    const found = scanVarScope(PROBE, page('.a{padding:var(--sp-l)}'), noAllowlist)
    expect(found).toHaveLength(1)
    expect(found[0]).toMatchObject({
      category: 'varScope',
      value: '--sp-l',
      blocking: true,
      exactMatch: false,
    })
  })

  it('must-NOT-fire：本文件 :root 里定义了 ⇒ 放行（有定义那一侧）', () => {
    expect(scanVarScope(PROBE, page(':root{--sp-l:24px}.a{padding:var(--sp-l)}'), noAllowlist)).toEqual([])
  })

  it('must-NOT-fire：写了兜底值 ⇒ 放行（声明不会被丢弃，页面不塌）', () => {
    expect(scanVarScope(PROBE, page('.a{padding:var(--sp-l, 24px)}'), noAllowlist)).toEqual([])
  })

  it('must-FIRE：内联 style 属性里的未解析引用照样拦', () => {
    const found = scanVarScope(PROBE, HTML('', '<div style="padding:var(--sp-l)"></div>'), noAllowlist)
    expect(found).toHaveLength(1)
    expect(found[0].value).toBe('--sp-l')
  })

  it('must-NOT-fire：注释里的 var() ⛔不报（与 F83 同族，别让它诱导人扭曲注释）', () => {
    expect(scanVarScope(PROBE, page('.a{/* padding:var(--sp-l) */ color:red}'), noAllowlist)).toEqual([])
  })

  it('allowlist 逃生口按 file:line:--var 生效（运行时 setProperty 那类静态判不了的例外）', () => {
    const src = page('.a{padding:var(--sp-l)}')
    const line = scanVarScope(PROBE, src, noAllowlist)[0].line
    expect(scanVarScope(PROBE, src, new Set([`${PROBE}:${line}:--sp-l`]))).toEqual([])
  })

  it('行号指真引用所在行（多行 <style> 里也不漂）', () => {
    // <!doctype…<style> = 第 1 行，css 首行 = 第 2 行，故第三行才是 padding 那行
    const src = page('.a{color:red}\n.b{border:0}\n.c{padding:var(--sp-l)}')
    const found = scanVarScope(PROBE, src, noAllowlist)
    expect(found).toHaveLength(1)
    expect(found[0].line).toBe(4)
  })

  it('同一行同一个变量只报一次（⛔ 不把一处问题报成两条）', () => {
    const found = scanVarScope(PROBE, page('.a{padding:var(--sp-l);margin:var(--sp-l)}'), noAllowlist)
    expect(found).toHaveLength(1)
  })
})
