import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  parseLayoutTokens,
  extractMediaWidths,
  extractWidthLiterals,
  scanRepo,
  evaluate,
  EXEMPTIONS,
  TOKENS_FILE,
  SCAN_TARGETS,
  CONTAINER_REPORT_MIN_PX,
} from '../scripts/audit-layout-tokens.mjs'

const REPO_ROOT = resolve(__dirname, '..')
const realTokens = () => parseLayoutTokens(readFileSync(resolve(REPO_ROOT, TOKENS_FILE), 'utf-8'))

/** 最小 scan 形状，供 evaluate 单独驱动 */
function scanOf(mediaHits: unknown[], fileCount = 1) {
  return { fileCount, mediaHits, literalHits: [] } as Parameters<typeof evaluate>[0]['scan']
}

describe('parseLayoutTokens', () => {
  it('把 --bp-* 与 --container-* 分开、值取 number', () => {
    const t = parseLayoutTokens(':root{--bp-sm:640px;--container-prose:840px;--sp-m:16px;}')
    expect([...t.bp.entries()]).toEqual([['bp-sm', 640]])
    expect([...t.container.entries()]).toEqual([['container-prose', 840]])
  })

  it('不把别的 token 误收（--sp-* / --z-* / 颜色）', () => {
    const t = parseLayoutTokens(':root{--sp-m:16px;--z-modal:1000;--brand:#0a0;}')
    expect(t.bp.size).toBe(0)
    expect(t.container.size).toBe(0)
  })

  it('真仓库里两组都非空（分母回归钉）', () => {
    const t = realTokens()
    expect(t.bp.size).toBeGreaterThan(0)
    expect(t.container.size).toBeGreaterThan(0)
  })
})

describe('extractMediaWidths', () => {
  it('抽出 max-width / min-width 两种 feature', () => {
    const hits = extractMediaWidths('@media (max-width: 640px) { a{} }\n@media (min-width:960px){ b{} }')
    expect(hits.map((h) => [h.feature, h.px])).toEqual([
      ['max-width', 640],
      ['min-width', 960],
    ])
  })

  it('一个 @media 里两个宽度条件都抽到', () => {
    const hits = extractMediaWidths('@media (min-width: 640px) and (max-width: 960px) { a{} }')
    expect(hits.map((h) => h.px)).toEqual([640, 960])
  })

  it('非宽度媒体特性不产生命中', () => {
    expect(extractMediaWidths('@media (prefers-color-scheme: dark) { a{} }')).toEqual([])
    expect(extractMediaWidths('@media print { a{} }')).toEqual([])
  })

  it('认不出的宽度形态 → px=null（fail closed on input shape）', () => {
    const hits = extractMediaWidths('@media (width <= 640px) { a{} }')
    expect(hits).toHaveLength(1)
    expect(hits[0].px).toBeNull()
    expect(hits[0].feature).toBe('unparsed')
  })

  it('prelude 跨行也能抽到', () => {
    const hits = extractMediaWidths('@media\n  (max-width: 720px)\n{ a{} }')
    expect(hits.map((h) => h.px)).toEqual([720])
  })
})

describe('extractWidthLiterals', () => {
  it('只收 ≥ 门槛的字面量，var() 不算', () => {
    const src = 'a{max-width:600px} b{max-width:599px} c{max-width:var(--container-prose)}'
    expect(extractWidthLiterals(src, CONTAINER_REPORT_MIN_PX).map((h) => h.px)).toEqual([600])
  })

  // 2026-08-12：S5 此前不剥注释（extractMediaWidths 08-11 修过、这边漏了），
  // 解释性注释里的 `max-width: 720px` 被当成真声明计入 → report-only 处数报高。
  it('注释里的 max-width 不计入（同 extractMediaWidths 的注释中和）', () => {
    const src = '/* 原 @media (max-width: 720px) 块已删除 */\na{color:red}'
    expect(extractWidthLiterals(src, CONTAINER_REPORT_MIN_PX)).toEqual([])
  })

  it('多行注释被中和后行号不漂（等长空格 + 保留换行）', () => {
    const src = '/* 提到 max-width: 900px\n   还提到 max-width: 1100px */\nb{max-width:960px}'
    const hits = extractWidthLiterals(src, CONTAINER_REPORT_MIN_PX)
    expect(hits.map((h) => h.px)).toEqual([960])
    expect(hits[0].line).toBe(3)
  })

  // 阴性对照：修的是「注释不算」，不是「放宽判据」—— 注释外的真声明照收
  it('注释相邻的真声明仍照收', () => {
    const src = '/* max-width: 720px 只是说明 */ a{max-width:1280px}'
    expect(extractWidthLiterals(src, CONTAINER_REPORT_MIN_PX).map((h) => h.px)).toEqual([1280])
  })
})

describe('evaluate — S1 分母 fail closed', () => {
  it('--bp-* 为空 → 红（防闸空转成假绿）', () => {
    const { failures } = evaluate({
      tokens: { bp: new Map(), container: new Map([['container-prose', 840]]) },
      scan: scanOf([]),
      exemptions: [],
    })
    expect(failures.map((f) => f.code)).toContain('S1-no-breakpoint-tokens')
  })

  it('--container-* 为空 → 红', () => {
    const { failures } = evaluate({
      tokens: { bp: new Map([['bp-sm', 640]]), container: new Map() },
      scan: scanOf([]),
      exemptions: [],
    })
    expect(failures.map((f) => f.code)).toContain('S1-no-container-tokens')
  })
})

describe('evaluate — S2 扫描面 fail closed', () => {
  it('0 个文件 → 红，不当通过', () => {
    const { failures } = evaluate({
      tokens: { bp: new Map([['bp-sm', 640]]), container: new Map([['c', 1]]) },
      scan: scanOf([], 0),
      exemptions: [],
    })
    expect(failures.map((f) => f.code)).toContain('S2-empty-scan')
  })
})

describe('evaluate — S3 ad-hoc 断点', () => {
  const tokens = { bp: new Map([['bp-sm', 640]]), container: new Map([['c', 1]]) }

  it('值在 token 集合里 → 放行', () => {
    const { failures } = evaluate({
      tokens,
      scan: scanOf([{ file: 'a.css', feature: 'max-width', px: 640, line: 1, prelude: '' }]),
      exemptions: [],
    })
    expect(failures).toEqual([])
  })

  it('值不在集合里且无豁免 → 红并点名文件行号与值', () => {
    const { failures } = evaluate({
      tokens,
      scan: scanOf([{ file: 'a.css', feature: 'max-width', px: 1024, line: 7, prelude: '' }]),
      exemptions: [],
    })
    expect(failures).toHaveLength(1)
    expect(failures[0].code).toBe('S3-ad-hoc-breakpoint')
    expect(failures[0].message).toContain('a.css:7')
    expect(failures[0].message).toContain('1024px')
  })

  it('豁免按 (file, value) 认，同值不同文件不共享豁免', () => {
    const exemptions = [{ file: 'a.css', value: 1024, addedAt: '2026-07-31', reason: 'x' }]
    const { failures } = evaluate({
      tokens,
      scan: scanOf([
        { file: 'a.css', feature: 'max-width', px: 1024, line: 1, prelude: '' },
        { file: 'b.css', feature: 'max-width', px: 1024, line: 1, prelude: '' },
      ]),
      exemptions,
    })
    expect(failures.map((f) => f.code)).toEqual(['S3-ad-hoc-breakpoint'])
    expect(failures[0].message).toContain('b.css')
  })

  it('unparsed 宽度形态 → 红（不静默跳过）', () => {
    const { failures } = evaluate({
      tokens,
      scan: scanOf([{ file: 'a.css', feature: 'unparsed', px: null, line: 3, prelude: '@media (width <= 640px)' }]),
      exemptions: [],
    })
    expect(failures.map((f) => f.code)).toEqual(['S3-unparsed-media-width'])
  })
})

describe('evaluate — S4 豁免表 shrink-only', () => {
  const tokens = { bp: new Map([['bp-sm', 640]]), container: new Map([['c', 1]]) }

  it('豁免不再命中 → 红，要求删行（修完由闸自己宣布）', () => {
    const { failures } = evaluate({
      tokens,
      scan: scanOf([{ file: 'a.css', feature: 'max-width', px: 640, line: 1, prelude: '' }]),
      exemptions: [{ file: 'a.css', value: 1024, addedAt: '2026-07-31', reason: 'x' }],
    })
    expect(failures.map((f) => f.code)).toEqual(['S4-stale-exemption'])
    expect(failures[0].message).toContain('请删掉该行')
  })

  it('空豁免表是合法终态，不是待办', () => {
    const { failures } = evaluate({
      tokens,
      scan: scanOf([{ file: 'a.css', feature: 'max-width', px: 640, line: 1, prelude: '' }]),
      exemptions: [],
    })
    expect(failures).toEqual([])
  })
})

describe('豁免表自洽性', () => {
  it('每条都带 addedAt(YYYY-MM-DD) + 修法方向的 reason', () => {
    for (const ex of EXEMPTIONS) {
      expect(ex.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/)
      expect(ex.reason.length).toBeGreaterThan(20)
      expect(typeof ex.value).toBe('number')
    }
  })

  it('无重复 (file, value)', () => {
    const keys = EXEMPTIONS.map((e) => `${e.file}|${e.value}`)
    expect(new Set(keys).size).toBe(keys.length)
  })
})

describe('真仓库回归钉', () => {
  it('当前仓库全绿，且每条豁免都真被命中（S4 不红）', () => {
    const scan = scanRepo(REPO_ROOT, SCAN_TARGETS)
    const { failures, usedExemptions } = evaluate({ tokens: realTokens(), scan })
    expect(failures).toEqual([])
    expect(usedExemptions.size).toBe(EXEMPTIONS.length)
    expect(scan.fileCount).toBeGreaterThan(50)
    expect(scan.mediaHits.length).toBeGreaterThan(0)
  })

  it('闸真挂上了：package.json 有 audit:layout-tokens 且进了 prepublishOnly', () => {
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf-8'))
    expect(pkg.scripts['audit:layout-tokens']).toContain('audit-layout-tokens.mjs')
    // 2026-08-25（第 9 步）：39 步串从 `prepublishOnly` 搬到 `gate-chain`；
    // `prepublishOnly` 现在是跑它的 runner 入口。两条都断言 —— 只断言前者会在
    // 「有人把串写回入口」时静默通过，而那正是退回 fail-fast 的形态。
    expect(pkg.scripts['gate-chain']).toContain('audit:layout-tokens')
    expect(pkg.scripts.prepublishOnly).toContain('run-gate-chain.mjs')
  })

  it('闸真挂上了：pre-commit 里有条件 gate', () => {
    const hook = readFileSync(resolve(REPO_ROOT, '.husky/pre-commit'), 'utf-8')
    expect(hook).toContain('audit:layout-tokens')
  })
})
