import { describe, it, expect } from 'vitest'
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import {
  extractDeclaredClasses,
  findCollisions,
  checkDenominator,
  findStaleExemptions,
  readDemoCss,
  KNOWN_COLLISIONS,
  NON_PAGE_FILES,
  DEMOS_DIR,
} from '../scripts/audit-demo-css-page-scope.mjs'

const REPO_ROOT = resolve(__dirname, '..')

/** Build a throwaway demos dir so the denominator/collision logic can be driven on fixtures. */
function fixtureRoot(files: Record<string, string>): string {
  const root = mkdtempSync(join(tmpdir(), 'demo-css-scope-'))
  mkdirSync(join(root, DEMOS_DIR), { recursive: true })
  for (const [name, body] of Object.entries(files)) writeFileSync(join(root, DEMOS_DIR, name), body)
  return root
}

describe('extractDeclaredClasses', () => {
  it('collects classes from selector text', () => {
    const got = extractDeclaredClasses('.a, .b__c { color: red } .d.e { color: blue }')
    expect([...got].sort()).toEqual(['a', 'b__c', 'd', 'e'])
  })

  it('does NOT count classes that only appear in comments (would be a false positive)', () => {
    const got = extractDeclaredClasses('/* .member-grid lives in formitem-demo.css */ .own { color: red }')
    expect([...got]).toEqual(['own'])
  })

  it('does NOT count classes pulled in via @import (else every file collides with demo-shared)', () => {
    const got = extractDeclaredClasses("@import './demo-shared.css';\n.own { color: red }")
    expect([...got]).toEqual(['own'])
  })

  it('DOES count rules nested inside @media (they are this file\'s declarations)', () => {
    const got = extractDeclaredClasses('@media (max-width: 700px) { .narrow-only { display: none } }')
    expect([...got]).toEqual(['narrow-only'])
  })

  it('ignores at-rule preludes such as @keyframes step names', () => {
    const got = extractDeclaredClasses('@keyframes spin { from { opacity: 0 } to { opacity: 1 } } .real { color: red }')
    expect([...got]).toEqual(['real'])
  })
})

describe('findCollisions', () => {
  it('must-not-fire when every page declares only its own classes', () => {
    const map = new Map([
      ['a-demo.css', new Set(['a-thing'])],
      ['b-demo.css', new Set(['b-thing'])],
    ])
    expect(findCollisions(map)).toEqual([])
  })

  it('fires and names every owning file when two pages declare the same class', () => {
    const map = new Map([
      ['b-demo.css', new Set(['shared-thing'])],
      ['a-demo.css', new Set(['shared-thing', 'a-only'])],
    ])
    const got = findCollisions(map)
    expect(got).toHaveLength(1)
    expect(got[0].className).toBe('shared-thing')
    expect(got[0].files).toEqual(['a-demo.css', 'b-demo.css']) // sorted, so output is stable
  })

  it('reproduces the historical INFRA-F67 §修② defect (.member-grid in two pages)', () => {
    const root = fixtureRoot({
      'demo-shared.css': '.shared { color: red }',
      'formitem-demo.css': "@import './demo-shared.css';\n.member-grid { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)) }",
      'inputnumber-demo.css': "@import './demo-shared.css';\n.member-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)) }",
    })
    const got = findCollisions(readDemoCss(root).pageFiles)
    expect(got.map((c) => c.className)).toEqual(['member-grid'])
    expect(got[0].files).toEqual(['formitem-demo.css', 'inputnumber-demo.css'])
  })

  it('does not flag a class shared between a page file and demo-shared.css (that is the fix, not the bug)', () => {
    const root = fixtureRoot({
      'demo-shared.css': '.member-grid { display: grid }',
      'formitem-demo.css': "@import './demo-shared.css';\n.formitem-only { color: red }",
    })
    expect(findCollisions(readDemoCss(root).pageFiles)).toEqual([])
  })
})

describe('checkDenominator (fail closed — S3)', () => {
  it('fails when the demos dir is missing instead of scanning zero files and passing', () => {
    const r = checkDenominator({ pageFiles: new Map(), sharedPresent: false, dirExists: false })
    expect(r.ok).toBe(false)
    expect(r.errors.join(' ')).toMatch(/不存在/)
  })

  it('fails when the dir exists but holds no page-specific stylesheet', () => {
    const r = checkDenominator({ pageFiles: new Map(), sharedPresent: true, dirExists: true })
    expect(r.ok).toBe(false)
    expect(r.errors.join(' ')).toMatch(/0 份/)
  })

  it('fails when demo-shared.css is gone (the prescribed fix would have nowhere to land)', () => {
    const r = checkDenominator({ pageFiles: new Map([['a-demo.css', new Set()]]), sharedPresent: false, dirExists: true })
    expect(r.ok).toBe(false)
    expect(r.errors.join(' ')).toMatch(/demo-shared\.css/)
  })

  it('passes on a well-formed dir', () => {
    const r = checkDenominator({ pageFiles: new Map([['a-demo.css', new Set()]]), sharedPresent: true, dirExists: true })
    expect(r).toEqual({ ok: true, errors: [] })
  })
})

describe('findStaleExemptions (shrink-only — S2)', () => {
  it('reports an exemption whose collision is gone so the row gets deleted', () => {
    const stale = findStaleExemptions([{ className: 'still-colliding', files: ['a.css', 'b.css'] }], [
      { className: 'still-colliding', since: 'x', why: 'y' },
      { className: 'already-fixed', since: 'x', why: 'y' },
    ])
    expect(stale.map((s) => s.className)).toEqual(['already-fixed'])
  })

  it('reports nothing while every exemption still collides', () => {
    const stale = findStaleExemptions([{ className: 'a', files: ['x.css', 'y.css'] }], [{ className: 'a', since: 'x', why: 'y' }])
    expect(stale).toEqual([])
  })
})

describe('real repo state', () => {
  const scanned = readDemoCss(REPO_ROOT)

  it('denominator is sane (this is what makes the other assertions mean something)', () => {
    expect(checkDenominator(scanned)).toEqual({ ok: true, errors: [] })
    expect(scanned.pageFiles.size).toBeGreaterThan(10)
  })

  it('every live collision is either exempt or absent — no unregistered ones', () => {
    const known = new Set(KNOWN_COLLISIONS.map((k) => k.className))
    const fresh = findCollisions(scanned.pageFiles).filter((c) => !known.has(c.className))
    expect(fresh).toEqual([])
  })

  it('the exemption table may only shrink', () => {
    expect(findStaleExemptions(findCollisions(scanned.pageFiles))).toEqual([])
  })

  it('demo-shared.css / demo-figma-members.css are excluded from the page-specific set', () => {
    for (const name of NON_PAGE_FILES) expect(scanned.pageFiles.has(name)).toBe(false)
  })
})
