import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  stripCssComments,
  stripHtmlComments,
  countClassUsages,
  lookupDeclaration,
  extractTemplate,
  extractStyle,
  checkStructure,
  checkDeclarations,
  checkDenominator,
  checkFreshness,
  detectForeignFileKey,
  parseCatalogFileLocalHeadings,
  repoRelative,
  DS_LIBRARY_FILE_KEY,
} from '../scripts/audit-file-local-fidelity.mjs'

// 分母刻意窄（Task 3 窄分母教训）：除了「历史 bug 回归」那一条必须用真实 SFC，
// 其余全喂手搓的小段 source，免得 UserMenu.vue 一动就假失败。

describe('剥注释（S2 假绿的唯一防线）', () => {
  it('CSS 注释被剥成等长空白，行数不变', () => {
    const src = 'a {\n  /* background: var(--fake) */\n  color: red;\n}'
    const out = stripCssComments(src)
    expect(out.split('\n').length).toBe(src.split('\n').length)
    expect(out.length).toBe(src.length)
    expect(out).not.toContain('--fake')
  })

  it('注释里的声明不会让 lookupDeclaration 假绿', () => {
    const css = stripCssComments('.p {\n /* width: 360px; */\n height: 10px;\n}')
    expect(lookupDeclaration(css, '.p', 'width').found).toBe(false)
    expect(lookupDeclaration(css, '.p', 'height').value).toBe('10px')
  })

  it('模板注释里提到 class 名不会被计数', () => {
    const tpl = stripHtmlComments('<!-- class="x" 只是注释 --><div class="x" />')
    expect(countClassUsages(tpl, 'x')).toBe(1)
  })
})

describe('countClassUsages 按空白切词，不做子串匹配', () => {
  it('user-menu__avatar 不会把 user-menu__avatar-img 算进去', () => {
    const tpl = '<div class="user-menu__avatar"><img class="user-menu__avatar-img" /></div>'
    expect(countClassUsages(tpl, 'user-menu__avatar')).toBe(1)
    expect(countClassUsages(tpl, 'user-menu__avatar-img')).toBe(1)
  })

  it('单引号 class 也数得到', () => {
    expect(countClassUsages("<i class='a b' />", 'b')).toBe(1)
  })
})

describe('lookupDeclaration', () => {
  it('同 selector 出现多次时取最后一条（CSS 层叠语义）', () => {
    const css = '.a { color: red; }\n.a { color: blue; }'
    expect(lookupDeclaration(css, '.a', 'color').value).toBe('blue')
  })

  it('区分「选择器没了」与「声明没了」', () => {
    expect(lookupDeclaration('.b { color: red }', '.a', 'color').selectorFound).toBe(false)
    const r = lookupDeclaration('.a { color: red }', '.a', 'width')
    expect(r.selectorFound).toBe(true)
    expect(r.found).toBe(false)
  })

  it('空白归一后再比，多余缩进不算漂移', () => {
    expect(lookupDeclaration('.a {\n  padding:  4px   4px\n}', '.a', 'padding').value).toBe('4px 4px')
  })
})

describe('S1 结构闸 — 历史 bug 回归（真实 SFC）', () => {
  const sfc = readFileSync(
    resolve(__dirname, '../src/components/UserMenu/UserMenu.vue'),
    'utf8',
  )
  const entry = {
    component: 'UserMenu',
    structure: [{ class: 'user-menu__divider', expectedCount: 1, figmaNodeId: '6423:95' }],
  }

  it('当前文件是绿的（must-not-fire）', () => {
    expect(checkStructure(entry, sfc).violations).toEqual([])
  })

  it('复现 INFRA-F82 那个藏了近 4 周的 bug：每个 section 各 prepend 一条分隔线 → 必须红', () => {
    // 历史形态：4 条分隔线。这里只在模板里再塞 3 条，其余一字不动。
    const broken = sfc.replace(
      '<div class="user-menu__divider" />',
      '<div class="user-menu__divider" /><div class="user-menu__divider" />' +
        '<div class="user-menu__divider" /><div class="user-menu__divider" />',
    )
    const r = checkStructure(entry, broken)
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/出现 4 次.*期望 1 次/)
  })

  it('分隔线被整条删掉也红（另一个方向）', () => {
    const broken = sfc.replace('<div class="user-menu__divider" />', '')
    expect(checkStructure(entry, broken).violations.length).toBe(1)
  })

  it('取不到 <template> 时不静默放行', () => {
    const r = checkStructure(entry, '<script setup></script>')
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/取不到 <template>/)
  })
})

describe('S2 声明闸', () => {
  const entry = {
    component: 'Fake',
    declarations: [
      { selector: '.p', property: 'width', value: '360px', figmaNodeId: '1:1' },
    ],
  }
  const sfcOf = (css: string) => `<template><div/></template><style>${css}</style>`

  it('值一致 → 绿', () => {
    expect(checkDeclarations(entry, sfcOf('.p { width: 360px }')).violations).toEqual([])
  })

  it('值漂移 → 红，并把两个值都印出来', () => {
    const r = checkDeclarations(entry, sfcOf('.p { width: 240px }'))
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/240px/)
    expect(r.violations[0]).toMatch(/360px/)
  })

  it('min-width 换掉 width（F82 里 owner 特意否掉的那个形态）→ 红', () => {
    const r = checkDeclarations(entry, sfcOf('.p { min-width: 360px }'))
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/声明不见了/)
  })

  it('选择器整个消失 → 红且措辞不同于「声明不见了」', () => {
    const r = checkDeclarations(entry, sfcOf('.q { width: 360px }'))
    expect(r.violations[0]).toMatch(/选择器不见了/)
  })
})

describe('S3 分母 — fail closed', () => {
  const snapshot = {
    _meta: { notComponentExemptions: { 'APP Icons (file-local)': '无 code 组件' } },
    components: [{ file: 'src/components/UserMenu/UserMenu.vue', catalogHeading: 'User Menu (file-local)' }],
  }

  it('已登记的组件 + 已豁免的 catalog 条目 → 绿', () => {
    const r = checkDenominator(
      snapshot,
      [{ file: 'src/components/UserMenu/UserMenu.vue', fileKey: 'DtZcMkhNy6qh6jbQQnhreQ' }],
      ['APP Icons (file-local)', 'User Menu (file-local)'],
    )
    expect(r.violations).toEqual([])
  })

  it('新出现一个带非 DS fileKey 的组件但没登记 → 红', () => {
    const r = checkDenominator(
      snapshot,
      [
        { file: 'src/components/UserMenu/UserMenu.vue', fileKey: 'DtZcMkhNy6qh6jbQQnhreQ' },
        { file: 'src/components/Widget/Widget.vue', fileKey: 'SomeOtherProductFileKey1' },
      ],
      ['APP Icons (file-local)', 'User Menu (file-local)'],
    )
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/Widget\.vue/)
  })

  it('catalog 新增一个 file-local 条目、既没快照也没豁免 → 红', () => {
    const r = checkDenominator(
      snapshot,
      [{ file: 'src/components/UserMenu/UserMenu.vue', fileKey: 'DtZcMkhNy6qh6jbQQnhreQ' }],
      ['APP Icons (file-local)', 'User Menu (file-local)', 'Brand New Thing (file-local)'],
    )
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/Brand New Thing/)
  })

  // ── INFRA-F108 回归钉：Windows 路径分隔符 ──────────────────────────────
  // 这三条一起才算数：第 1 条证明**故障真的存在**（不归一化就红），第 2 条证明修法生效，
  // 第 3 条是阴性对照（POSIX 路径不能被这个 replace 改坏）。少了第 1 条，后两条可能是空过。
  it('故障存在性探针：反斜杠路径不归一化 → checkDenominator 误报「未登记」', () => {
    const r = checkDenominator(
      snapshot,
      [{ file: String.raw`src\components\UserMenu\UserMenu.vue`, fileKey: 'DtZcMkhNy6qh6jbQQnhreQ' }],
      ['APP Icons (file-local)', 'User Menu (file-local)'],
    )
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/UserMenu\.vue/)
  })

  it('repoRelative 把反斜杠归一化 → 同一份快照转绿（F108 的修法）', () => {
    const repoRoot = '/repo'
    const rel = repoRelative(String.raw`/repo\src\components\UserMenu\UserMenu.vue`, repoRoot)
    expect(rel).toBe('src/components/UserMenu/UserMenu.vue')
    const r = checkDenominator(
      snapshot,
      [{ file: rel, fileKey: 'DtZcMkhNy6qh6jbQQnhreQ' }],
      ['APP Icons (file-local)', 'User Menu (file-local)'],
    )
    expect(r.violations).toEqual([])
  })

  it('阴性对照：POSIX 路径经 repoRelative 后逐字不变', () => {
    expect(repoRelative('/repo/src/components/UserMenu/UserMenu.vue', '/repo')).toBe(
      'src/components/UserMenu/UserMenu.vue',
    )
  })
})

describe('S4 新鲜度', () => {
  const ok = {
    _meta: {
      ack: {
        addedAt: '2026-07-31',
        verifiedAt: '2026-07-30',
        reviewDueBy: '2026-10-28',
        evidence: ['commit 5a2df7ae'],
      },
    },
  }

  it('未到期 → 绿', () => {
    expect(checkFreshness(ok, '2026-08-01').violations).toEqual([])
  })

  it('到期即红', () => {
    const r = checkFreshness(ok, '2026-10-29')
    expect(r.violations.length).toBe(1)
    expect(r.violations[0]).toMatch(/复核已到期/)
  })

  // ⚠️ 这三条是本闸自己 S4 语义写反后补的。首版要求 addedAt <= verifiedAt，方向正好反
  // 了：owner 07-30 签核、07-31 才落快照恰恰是合法情形（本闸就是这么诞生的）。当时两个
  // 日期写成同一天，不等式从未真正约束过 —— 绿是同义反复，不是通过。
  it('前一天签核、次日落快照 → 绿（方向别写反）', () => {
    expect(checkFreshness(ok, '2026-07-31').violations).toEqual([])
  })

  it('verifiedAt 晚于 addedAt（登记时声称核验发生在登记之后 = 承诺不是证据）→ 红', () => {
    const bad = { _meta: { ack: { ...ok._meta.ack, verifiedAt: '2026-08-05' } } }
    const r = checkFreshness(bad, '2026-08-06')
    expect(r.violations.some(v => /晚于 addedAt/.test(v))).toBe(true)
  })

  it('verifiedAt 在未来 → 红', () => {
    const bad = { _meta: { ack: { ...ok._meta.ack, addedAt: '2026-09-01', verifiedAt: '2026-09-01' } } }
    const r = checkFreshness(bad, '2026-08-01')
    expect(r.violations.some(v => /在未来/.test(v))).toBe(true)
  })

  it('evidence 为空 → 红（ack 必须带可复核证据）', () => {
    const bad = { _meta: { ack: { ...ok._meta.ack, evidence: [] } } }
    expect(checkFreshness(bad, '2026-08-01').violations[0]).toMatch(/evidence/)
  })

  it('完全没有 ack → 红', () => {
    expect(checkFreshness({ _meta: {} }, '2026-08-01').violations.length).toBe(1)
  })
})

describe('分母探测器', () => {
  it('DS 库 fileKey 不算 foreign（那些走 render-verification-manifest）', () => {
    expect(detectForeignFileKey(`fileKey ${DS_LIBRARY_FILE_KEY}`)).toEqual([])
  })

  it('产品文件 fileKey 被认出来 —— 两种书写形态都认', () => {
    expect(detectForeignFileKey('fileKey DtZcMkhNy6qh6jbQQnhreQ')).toContain('DtZcMkhNy6qh6jbQQnhreQ')
    expect(
      detectForeignFileKey('https://www.figma.com/design/DtZcMkhNy6qh6jbQQnhreQ/Micro-Apps'),
    ).toContain('DtZcMkhNy6qh6jbQQnhreQ')
  })

  it('catalog 标题解析去掉 ⚠️ 并归一空白', () => {
    expect(parseCatalogFileLocalHeadings('### User Menu ⚠️ (file-local) ⟨账号菜单⟩')).toEqual([
      'User Menu (file-local)',
    ])
  })
})

describe('真实快照文件自身合规（形状 + 与活源一致）', () => {
  const snapshot = JSON.parse(
    readFileSync(resolve(__dirname, '../figma-data/file-local-fidelity-snapshot.json'), 'utf8'),
  )

  it('每个条目都带 figma 溯源三件套', () => {
    for (const c of snapshot.components) {
      expect(c.figma.fileKey).toBeTruthy()
      expect(c.figma.nodeId).toBeTruthy()
      expect(c.figma.url).toContain(c.figma.fileKey)
      expect(c.figma.fileKey).not.toBe(DS_LIBRARY_FILE_KEY)
    }
  })

  it('每条断言都指名一个 figmaNodeId（漂移了才知道回哪里核）', () => {
    for (const c of snapshot.components) {
      for (const s of c.structure) expect(s.figmaNodeId).toBeTruthy()
      for (const d of c.declarations) expect(d.figmaNodeId).toBeTruthy()
    }
  })

  it('template / style 两区都真解析得出来（否则 28 条断言是空过）', () => {
    for (const c of snapshot.components) {
      const src = readFileSync(resolve(__dirname, '..', c.file), 'utf8')
      expect(extractTemplate(src).length).toBeGreaterThan(0)
      expect(extractStyle(src).length).toBeGreaterThan(0)
    }
  })
})
