// tests/audit-typecheck-scope.test.ts — INFRA-F130
//
// 这份测试的两个职责：
//   ① 判据本体（S1-S6）逐条**造故障**验它真的会红 —— 全 PASS 若不带致败探针，就分不清
//      「判据成立」和「判据空过」（memory `regression-pass-needs-fault-proof`）。
//   ② 钉住 F130 的落地形态：`react-pilot/harness/**` 必须同时在**根** tsconfig 的 include 里
//      （根 `vue-tsc` 在 pre-commit 是无条件跑的 = 最勤的那一层）和 `react-pilot/tsconfig.json` 里
//      （那份才开 `noUnusedLocals`/`noUnusedParameters`）。谁把它悄悄删了，这里当场红。
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  CONFIGS,
  ERROR_EXEMPTIONS,
  UNCOVERED_EXEMPTIONS,
  evaluate,
  globToRegExp,
  isDeclarationFile,
  parseTscErrors,
} from '../scripts/audit-typecheck-scope.mjs'

const REPO_ROOT = resolve(__dirname, '..')
const readJson = (rel: string) => JSON.parse(readFileSync(resolve(REPO_ROOT, rel), 'utf-8'))

/** 一份最小的健康输入：两份 config 各覆盖一个文件，未覆盖面为空，无类型错误。 */
function healthy() {
  return {
    tracked: ['src/a.ts', 'react-pilot/harness/RenderHarness.tsx'],
    scopes: {
      'tsconfig.json': new Set(['src/a.ts', 'react-pilot/harness/RenderHarness.tsx']),
      'react-pilot/tsconfig.json': new Set(['react-pilot/harness/RenderHarness.tsx']),
    },
    tsErrors: [] as { file: string; code: string; message: string }[],
    uncoveredExemptions: [] as typeof UNCOVERED_EXEMPTIONS,
    errorExemptions: [] as typeof ERROR_EXEMPTIONS,
  }
}

const codes = (r: { failures: { code: string }[] }) => r.failures.map((f) => f.code)

describe('globToRegExp', () => {
  it('`playwright*.config.ts` 命中 playwright.config.ts —— 落地当天踩过的坑', () => {
    expect(globToRegExp('playwright*.config.ts').test('playwright.config.ts')).toBe(true)
    expect(globToRegExp('playwright*.config.ts').test('playwright.a11y.config.ts')).toBe(true)
  })

  it('阴性对照：`playwright.*.config.ts` 漏掉 playwright.config.ts（就是本闸第一次运行抓到的那条）', () => {
    expect(globToRegExp('playwright.*.config.ts').test('playwright.config.ts')).toBe(false)
  })

  it('`*` 不跨目录、`**` 跨目录', () => {
    expect(globToRegExp('scripts/*.ts').test('scripts/x.ts')).toBe(true)
    expect(globToRegExp('scripts/*.ts').test('scripts/lib/x.ts')).toBe(false)
    expect(globToRegExp('tests/**').test('tests/a/b/c.ts')).toBe(true)
    expect(globToRegExp('tests/**').test('testsuite/a.ts')).toBe(false)
  })

  it('声明文件不进分母', () => {
    expect(isDeclarationFile('a/b.d.ts')).toBe(true)
    expect(isDeclarationFile('a/b.d.mts')).toBe(true)
    expect(isDeclarationFile('a/b.ts')).toBe(false)
  })
})

describe('parseTscErrors', () => {
  it('解析真实形态的 tsc 输出', () => {
    const errs = parseTscErrors(
      "react-pilot/src/demos/Chart.tsx(25,7): error TS6133: 'demoSalesByQuarter' is declared but its value is never read.\n" +
        'react-pilot/harness/RenderHarness.tsx(288,7): error TS2322: Type \'string\' is not assignable to type \'number\'.\n'
    )
    expect(errs).toHaveLength(2)
    expect(errs[0]).toMatchObject({ file: 'react-pilot/src/demos/Chart.tsx', code: 'TS6133' })
    expect(errs[1].code).toBe('TS2322')
  })

  it('阴性对照：非错误行不产出条目', () => {
    expect(parseTscErrors('Found 0 errors.\n')).toHaveLength(0)
  })
})

describe('evaluate — 健康态', () => {
  it('健康输入 = 零违例', () => {
    expect(evaluate(healthy()).failures).toHaveLength(0)
  })
})

describe('evaluate — 逐条造故障（判据不空过的证据）', () => {
  it('S1：分母为空 → 红', () => {
    const inp = healthy()
    inp.tracked = []
    expect(codes(evaluate(inp))).toContain('S1-empty-denominator')
  })

  it('S2：某份 config 解析面为 0 → 红（防把 include 改坏让闸空转）', () => {
    const inp = healthy()
    inp.scopes['react-pilot/tsconfig.json'] = new Set()
    expect(codes(evaluate(inp))).toContain('S2-empty-config-scope')
  })

  it('S3：harness 掉出所有作用面且无豁免 → 红（F130 回归保护）', () => {
    const inp = healthy()
    inp.scopes['tsconfig.json'] = new Set(['src/a.ts'])
    inp.scopes['react-pilot/tsconfig.json'] = new Set(['src/a.ts'])
    const r = evaluate(inp)
    expect(codes(r)).toContain('S3-uncovered-file')
    expect(r.failures.find((f) => f.code === 'S3-uncovered-file')!.message).toContain('RenderHarness.tsx')
  })

  it('S4：blocking 行长大 → 红', () => {
    const inp = healthy()
    inp.tracked = ['x/a.ts', 'x/b.ts']
    inp.scopes = { 'tsconfig.json': new Set(['x/a.ts']), 'react-pilot/tsconfig.json': new Set(['x/a.ts']) }
    inp.uncoveredExemptions = [{ glob: 'x/**', count: 0.5, growth: 'blocking', addedAt: 'T', reason: '', fixDirection: '' }] as never
    expect(codes(evaluate(inp))).toContain('S4-exemption-grew')
  })

  it('S4：report 行长大 → 不拦，但要印出来', () => {
    const inp = healthy()
    inp.tracked = ['tests/a.ts', 'tests/b.ts']
    inp.scopes = { 'tsconfig.json': new Set(['tests/a.ts']), 'react-pilot/tsconfig.json': new Set(['tests/a.ts']) }
    inp.uncoveredExemptions = [{ glob: 'tests/**', count: 0.5, growth: 'report', addedAt: 'T', reason: '', fixDirection: '' }] as never
    const r = evaluate(inp)
    expect(codes(r)).not.toContain('S4-exemption-grew')
    expect(r.reports.join(' ')).toContain('tests/**')
  })

  it('S4：覆盖变好但数字没改小 → 红（shrink-only 棘轮，落地当天真撞过一次）', () => {
    const inp = healthy()
    inp.tracked = ['tests/a.ts']
    inp.scopes = { 'tsconfig.json': new Set([]), 'react-pilot/tsconfig.json': new Set(['x']) }
    inp.uncoveredExemptions = [{ glob: 'tests/**', count: 9, growth: 'report', addedAt: 'T', reason: '', fixDirection: '' }] as never
    expect(codes(evaluate(inp))).toContain('S4-shrink-not-recorded')
  })

  it('S4：豁免行 0 命中 → 红要求删行（表空 = 终态）', () => {
    const inp = healthy()
    inp.uncoveredExemptions = [{ glob: 'gone/**', count: 3, growth: 'report', addedAt: 'T', reason: '', fixDirection: '' }] as never
    expect(codes(evaluate(inp))).toContain('S4-stale-exemption')
  })

  it('S5：react-pilot 新类型错误 → 红', () => {
    const inp = healthy()
    inp.tsErrors = [{ file: 'react-pilot/harness/RenderHarness.tsx', code: 'TS2322', message: "Type 'string' is not assignable to type 'number'." }]
    expect(codes(evaluate(inp))).toContain('S5-new-typecheck-error')
  })

  it('S5：命中具名豁免（file+code+symbol）→ 不红；换个 symbol 就红（不按行号认）', () => {
    const inp = healthy()
    inp.errorExemptions = [{ file: 'a.tsx', code: 'TS6133', symbol: 'foo', addedAt: 'T', reason: '', fixDirection: '' }] as never
    inp.tsErrors = [{ file: 'a.tsx', code: 'TS6133', message: "'foo' is declared but its value is never read." }]
    expect(codes(evaluate(inp))).not.toContain('S5-new-typecheck-error')

    inp.tsErrors = [{ file: 'a.tsx', code: 'TS6133', message: "'bar' is declared but its value is never read." }]
    expect(codes(evaluate(inp))).toContain('S5-new-typecheck-error')
  })

  it('S6：错误豁免不再命中 → 红要求删行', () => {
    const inp = healthy()
    inp.errorExemptions = [{ file: 'a.tsx', code: 'TS6133', symbol: 'foo', addedAt: 'T', reason: '', fixDirection: '' }] as never
    expect(codes(evaluate(inp))).toContain('S6-stale-error-exemption')
  })
})

describe('落地形态钉子（F130 —— 谁悄悄改回去这里就红）', () => {
  it('harness 同时在两份 config 的 include 里（2026-08-20 落齐，撤任一层都是回归）', () => {
    const rp = readJson('react-pilot/tsconfig.json')
    const root = readJson('tsconfig.json')
    // 根那份 = pre-commit 里**无条件**跑的 vue-tsc；react-pilot 那份 = 唯一开着 noUnusedLocals 的
    expect(root.include.some((p: string) => p.startsWith('react-pilot/harness'))).toBe(true)
    expect(rp.include).toContain('harness')
    // 两边的 @manifest 别名都必须在，否则 harness 首行 import 直接 TS2307（假红）
    expect(root.compilerOptions.paths['@manifest']).toBeTruthy()
    expect(rp.compilerOptions.paths['@manifest']).toBeTruthy()
  })

  it('harness 里没有失效的 @ts-expect-error 复活（08-20 删掉的那 5 处）', () => {
    const harness = readFileSync(resolve(REPO_ROOT, 'react-pilot/harness/RenderHarness.tsx'), 'utf-8')
    // 不是禁止用这个指令，而是「用了就必须真的抑制着一个错误」—— 失效指令自己就是 TS2578，
    // 而它现在会被根 vue-tsc（无条件层）当场报出来。这条断言只是让回归的报错信息更直白。
    expect(harness).not.toContain('@ts-expect-error slot attr on span')
  })

  it('react-pilot/tsconfig.json 的 include 含 harness，且没有关掉三个 strict 开关', () => {
    const rp = readJson('react-pilot/tsconfig.json')
    expect(rp.include).toContain('harness')
    expect(rp.compilerOptions.strict).toBe(true)
    expect(rp.compilerOptions.noUnusedLocals).toBe(true)
    expect(rp.compilerOptions.noUnusedParameters).toBe(true)
    // vitest globals 是那 77 个假错的根因，声明必须留着
    expect(rp.compilerOptions.types).toContain('vitest/globals')
  })

  it('CONFIGS 里每份 config 都写了 mount（没 mount 的 config 不算覆盖面）', () => {
    expect(CONFIGS.length).toBeGreaterThanOrEqual(2)
    for (const c of CONFIGS) expect(c.mount.length).toBeGreaterThan(10)
    expect(CONFIGS.map((c) => c.config)).toContain('react-pilot/tsconfig.json')
  })

  it('两张豁免表的每一行都具名 + 带日期 + 带修法方向', () => {
    for (const ex of UNCOVERED_EXEMPTIONS) {
      expect(ex.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/)
      expect(ex.reason.length).toBeGreaterThan(20)
      expect(ex.fixDirection.length).toBeGreaterThan(10)
      expect(['blocking', 'report']).toContain(ex.growth)
    }
    for (const ex of ERROR_EXEMPTIONS) {
      expect(ex.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/)
      expect(ex.fixDirection.length).toBeGreaterThan(10)
    }
  })
})
