// INFRA-F114 —— `pnpm audit:exports-types-contract` 的判据单测。
//
// 每条判据都配 must-fire / must-not-fire 一对（只测「红」会漏掉「永远红」这种坏闸）。
// fixture 全部写在临时目录，**不复用仓库真 package.json** —— 真仓库那份会随时间变，
// 拿它当 fixture 等于让单测跟着漂（[[feedback_plan-fixture-is-not-the-criterion-source]]）。
// 另配一条真仓库回归钉：当前树必须绿，防止判据被改成「永远绿」。

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve, dirname } from 'node:path'
import {
  audit,
  isCodeEntry,
  resolveRelative,
  extractRelativeSpecifiers,
  REPO_ROOT,
  EXEMPTIONS,
} from '../scripts/audit-exports-types-contract.mjs'

let root: string

function write(rel: string, content: string) {
  const abs = resolve(root, rel)
  mkdirSync(dirname(abs), { recursive: true })
  writeFileSync(abs, content, 'utf8')
}

function pkg(obj: Record<string, unknown>) {
  write('package.json', JSON.stringify(obj, null, 2))
}

beforeEach(() => {
  root = mkdtempSync(resolve(tmpdir(), 'f114-'))
})
afterEach(() => {
  rmSync(root, { recursive: true, force: true })
})

describe('S1 分母 fail closed', () => {
  it('缺 exports → 红', () => {
    pkg({ name: 'x' })
    const r = audit({ root, exemptions: [] })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/S1 分母 fail closed/)
  })

  it('exports 是空对象 → 红（不许「扫到 0 个所以通过」）', () => {
    pkg({ name: 'x', exports: {} })
    expect(audit({ root, exemptions: [] }).ok).toBe(false)
  })

  it('exports 只有资产条目、无代码出口 → 红', () => {
    pkg({ name: 'x', exports: { './style.css': './dist/style.css' } })
    const r = audit({ root, exemptions: [] })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/一个「代码出口」.*都没有/)
  })

  it('package.json 读不到 → 红，不抛', () => {
    const r = audit({ root: resolve(root, 'nope'), exemptions: [] })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/S1 读不到 package.json/)
  })
})

describe('S2 types 存在性', () => {
  it('must-fire：代码出口缺 types → 红并点名该 subpath', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({ name: 'x', exports: { '.': { import: './dist/i.js' } } })
    const r = audit({ root, exemptions: [] })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/exports\["\."\].*没有 types 条件/)
  })

  it('must-not-fire：有 types → 绿', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({ name: 'x', exports: { '.': { types: './dist/i.d.ts', import: './dist/i.js' } } })
    expect(audit({ root, exemptions: [] }).ok).toBe(true)
  })

  it('must-not-fire：字符串形态的资产条目不受检（它不是代码出口）', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({
      name: 'x',
      exports: {
        '.': { types: './dist/i.d.ts', import: './dist/i.js' },
        './style.css': './dist/style.css',
        './icons/svg/*': './dist/icons/svg/*',
      },
    })
    expect(audit({ root, exemptions: [] }).ok).toBe(true)
  })
})

describe('S3 链路可解析', () => {
  it('must-fire：d.ts 断在不存在的相对路径 → 红（F114 ② 的形态）', () => {
    write('dist/index.d.ts', "export { A } from './icons/index';\n")
    pkg({ name: 'x', exports: { '.': { types: './dist/index.d.ts', import: './dist/i.js' } } })
    const r = audit({ root, exemptions: [] })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/icons\/index.*解析不到/)
    // 命中 icons 时要带上根因指路，否则下一个人还得重查一遍 icon-artifacts.mjs
    expect(r.failures.join()).toMatch(/icon-artifacts\.mjs/)
  })

  it('must-not-fire：相对路径解析得到 <base>/index.d.ts → 绿', () => {
    write('dist/index.d.ts', "export { A } from './icons/index';\n")
    write('dist/icons/index.d.ts', 'export declare const A: number;\n')
    pkg({ name: 'x', exports: { '.': { types: './dist/index.d.ts', import: './dist/i.js' } } })
    expect(audit({ root, exemptions: [] }).ok).toBe(true)
  })

  it('must-not-fire：`Chart.vue` 形态解析到 `Chart.vue.d.ts` → 绿', () => {
    write('dist/chart.d.ts', "export { default as Chart } from './canonical/Chart.vue';\n")
    write('dist/canonical/Chart.vue.d.ts', 'declare const _d: unknown; export default _d;\n')
    pkg({ name: 'x', exports: { './chart': { types: './dist/chart.d.ts', import: './dist/chart.js' } } })
    expect(audit({ root, exemptions: [] }).ok).toBe(true)
  })

  it('must-not-fire：裸包名 / node: 内建不参与解析', () => {
    write('dist/index.d.ts', "import type { Ref } from 'vue';\nimport 'node:fs';\nexport type X = Ref<number>;\n")
    pkg({ name: 'x', exports: { '.': { types: './dist/index.d.ts', import: './dist/i.js' } } })
    expect(audit({ root, exemptions: [] }).ok).toBe(true)
  })

  it('types 目标不在磁盘（dist gitignored）→ SKIP 且大声说明，不当成通过也不红', () => {
    pkg({ name: 'x', exports: { '.': { types: './dist/gone.d.ts', import: './dist/i.js' } } })
    const r = audit({ root, exemptions: [] })
    expect(r.ok).toBe(true)
    expect(r.skippedS3).toBe(true)
    expect(r.notes.join()).toMatch(/不保证 consumer 拿得到类型/)
  })

  it('subpath-pattern（含 `*`）的 types 不算「取不到产物」，不该触发 SKIP 警告', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({
      name: 'x',
      exports: {
        '.': { types: './dist/i.d.ts', import: './dist/i.js' },
        './icons/esm/*': { types: './dist/icons/esm/*.d.ts', import: './dist/icons/esm/*.js' },
      },
    })
    const r = audit({ root, exemptions: [] })
    expect(r.ok).toBe(true)
    expect(r.skippedS3).toBe(false)
  })
})

describe('S4 豁免 shrink-only', () => {
  const ex = [{ subpath: './wc', since: '2026-08-13', reason: 'r', fixDirection: 'f' }]

  it('must-not-fire：被豁免的出口缺 types → 绿', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({
      name: 'x',
      exports: {
        '.': { types: './dist/i.d.ts', import: './dist/i.js' },
        './wc': { import: './dist-wc/w.js' },
      },
    })
    expect(audit({ root, exemptions: ex }).ok).toBe(true)
  })

  it('must-fire：该出口已经有 types 了 → 红，要求删豁免行', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({
      name: 'x',
      exports: {
        '.': { types: './dist/i.d.ts', import: './dist/i.js' },
        './wc': { types: './dist/i.d.ts', import: './dist-wc/w.js' },
      },
    })
    const r = audit({ root, exemptions: ex })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/shrink-only.*删掉这一行/)
  })

  it('must-fire：豁免指向一个已不存在的出口 → 红', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({ name: 'x', exports: { '.': { types: './dist/i.d.ts', import: './dist/i.js' } } })
    const r = audit({ root, exemptions: ex })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/已不是代码出口或已不存在/)
  })

  it('must-fire：豁免缺 fixDirection / since 形态错 → 红', () => {
    write('dist/i.d.ts', 'export {}\n')
    pkg({
      name: 'x',
      exports: { '.': { types: './dist/i.d.ts', import: './dist/i.js' }, './wc': { import: './w.js' } },
    })
    const bad = [{ subpath: './wc', since: '13/08/2026', reason: 'r' }]
    const r = audit({ root, exemptions: bad })
    expect(r.ok).toBe(false)
    expect(r.failures.join()).toMatch(/since 不是 YYYY-MM-DD/)
    expect(r.failures.join()).toMatch(/缺 fixDirection/)
  })
})

describe('纯函数', () => {
  it('isCodeEntry 只认含 import/require 的对象', () => {
    expect(isCodeEntry({ import: './a.js' })).toBe(true)
    expect(isCodeEntry({ require: './a.cjs' })).toBe(true)
    expect(isCodeEntry({ types: './a.d.ts' })).toBe(false)
    expect(isCodeEntry('./a.css')).toBe(false)
    expect(isCodeEntry(null)).toBe(false)
    expect(isCodeEntry(['./a.js'])).toBe(false)
  })

  it('extractRelativeSpecifiers 只收相对路径', () => {
    const s = "export { A } from './a';\nimport type { B } from 'vue';\nexport * from '../b';\n"
    expect(extractRelativeSpecifiers(s).sort()).toEqual(['../b', './a'])
  })

  it('resolveRelative 依次试 .d.ts / .ts / index.d.ts', () => {
    write('dist/x/index.d.ts', 'export {}\n')
    write('dist/y.d.ts', 'export {}\n')
    const from = resolve(root, 'dist/index.d.ts')
    expect(resolveRelative(from, './x')).toBe(resolve(root, 'dist/x/index.d.ts'))
    expect(resolveRelative(from, './y')).toBe(resolve(root, 'dist/y.d.ts'))
    expect(resolveRelative(from, './zzz')).toBeNull()
  })
})

describe('真仓库回归钉', () => {
  it('当前树必须绿 —— 防止判据被改成「永远绿」之外，也防止 F114 复发', () => {
    const r = audit({ root: REPO_ROOT, exemptions: EXEMPTIONS })
    expect(r.failures).toEqual([])
    expect(r.ok).toBe(true)
    // 分母非空：闸真的在看东西，不是空转
    expect(r.total).toBeGreaterThan(0)
  })
})
