/**
 * [[INFRA-F131]] ① — `audit:shipped-import-closure` 的判据单测。
 *
 * 两层：
 *   · 注入 fixture 测判据的每一条分支（纯函数，零 I/O）
 *   · **真仓库非空过钉** —— 最后一个 describe 拿真实 `package.json` + `git ls-files` 跑一遍，
 *     断言入口集与边数都 > 0。少了这一钉，上面全部 fixture 断言可以在闸对真仓库**零覆盖**的
 *     情况下照样全绿（本仓已为「fixture 绿而真树空扫」付过代价）。
 */
import { describe, it, expect } from 'vitest'
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { checkImportClosure, resolveSpecifier } from '../scripts/audit-shipped-import-closure.mjs'
import { isCoveredByFiles, filesPatternToRegExp } from '../scripts/lib/files-coverage.mjs'

const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')

/** 造一个最小仓库：tracked 列表 + 源码表。probeExists 由源码表推出。 */
function harness(sources: Record<string, string>, files: string[], knownGaps: any[] = []) {
  const tracked = Object.keys(sources)
  return checkImportClosure({
    files,
    tracked,
    readSource: (rel: string) => (rel in sources ? sources[rel] : null),
    probeExists: (rel: string) => rel in sources,
    knownGaps,
  })
}

describe('files-coverage — npm files[] 语义', () => {
  it('裸目录名 = 整个目录', () => {
    expect(isCoveredByFiles('dist/icons/esm/index.js', ['dist'])).toBe(true)
    expect(isCoveredByFiles('dist-wc/x.js', ['dist'])).toBe(false)
  })

  it('glob 的 * 不跨 /，** 跨', () => {
    expect(isCoveredByFiles('scripts/audit-mockup-colors.mjs', ['scripts/audit-mockup-*.mjs'])).toBe(true)
    expect(isCoveredByFiles('scripts/lib/audit-mockup-x.mjs', ['scripts/audit-mockup-*.mjs'])).toBe(false)
    expect(isCoveredByFiles('scripts/lib/audit-mockup-x.mjs', ['scripts/**'])).toBe(true)
  })

  it('这正是抽出 lib 前会假红的那一格 —— 旧实现只做 equals / 父目录前缀', () => {
    // 旧实现对 glob 一律返回 false；新实现判对。用真实的那条 pattern 钉住。
    expect(isCoveredByFiles('scripts/audit-mockup-integrity.mjs', ['scripts/audit-mockup-*.mjs'])).toBe(true)
  })

  it('! 取反，且后出现的匹配覆盖先出现的', () => {
    expect(isCoveredByFiles('scripts/audit-mockup-x.mjs', ['scripts/audit-mockup-*.mjs', '!scripts/audit-mockup-x.mjs'])).toBe(false)
    // 顺序反过来 ⇒ 后面那条 include 生效
    expect(isCoveredByFiles('scripts/audit-mockup-x.mjs', ['!scripts/audit-mockup-x.mjs', 'scripts/audit-mockup-*.mjs'])).toBe(true)
  })

  it('pattern 编译不把正则元字符当元字符', () => {
    expect(filesPatternToRegExp('a.b*.mjs').test('a.bXY.mjs')).toBe(true)
    expect(filesPatternToRegExp('a.b*.mjs').test('aXbYZ.mjs')).toBe(false)
  })
})

describe('resolveSpecifier — CJS 省扩展名', () => {
  const probe = (rel: string) => ['pkg/lib/x.js', 'pkg/dir/index.js'].includes(rel)
  it('裸路径补 .js', () => {
    expect(resolveSpecifier('pkg/main.js', './lib/x', probe)).toEqual({ rel: 'pkg/lib/x.js', exists: true })
  })
  it('目录补 index.js', () => {
    expect(resolveSpecifier('pkg/main.js', './dir', probe)).toEqual({ rel: 'pkg/dir/index.js', exists: true })
  })
  it('解析不到时 exists=false 并回报猜测路径', () => {
    expect(resolveSpecifier('pkg/main.js', './nope', probe)).toEqual({ rel: 'pkg/nope', exists: false })
  })
})

describe('checkImportClosure — 缺口判定', () => {
  it('shipped 脚本 import 一个没进 files[] 的同级文件 ⇒ 缺口', () => {
    const r = harness(
      {
        'scripts/audit-mockup-a.mjs': "import { x } from './helper.mjs'\n",
        'scripts/helper.mjs': 'export const x = 1\n',
      },
      ['scripts/audit-mockup-*.mjs']
    )
    expect(r.entries).toEqual(['scripts/audit-mockup-a.mjs'])
    expect(r.gaps).toHaveLength(1)
    expect(r.gaps[0]).toMatchObject({ from: 'scripts/audit-mockup-a.mjs', target: 'scripts/helper.mjs' })
    expect(r.unexcused).toHaveLength(1)
    expect(r.problems).toEqual([])
  })

  it('target 也在 files[] 里 ⇒ 无缺口', () => {
    const r = harness(
      {
        'scripts/audit-mockup-a.mjs': "import { x } from './helper.mjs'\n",
        'scripts/helper.mjs': 'export const x = 1\n',
      },
      ['scripts/audit-mockup-*.mjs', 'scripts/helper.mjs']
    )
    expect(r.gaps).toHaveLength(0)
    expect(r.edgeTotal).toBe(1)
  })

  it('走**传递**闭包：缺口藏在第二跳也要抓到', () => {
    const r = harness(
      {
        'scripts/audit-mockup-a.mjs': "import { x } from './lib/one.mjs'\n",
        'scripts/lib/one.mjs': "export { y as x } from './two.mjs'\n",
        'scripts/lib/two.mjs': 'export const y = 1\n',
      },
      ['scripts/audit-mockup-*.mjs', 'scripts/lib/one.mjs']
    )
    expect(r.gaps.map((g: any) => g.target)).toEqual(['scripts/lib/two.mjs'])
  })

  it('三种静态形态都算边：from / side-effect / require', () => {
    const r = harness(
      {
        'scripts/audit-mockup-a.mjs': "import './side.mjs'\nimport { x } from './from.mjs'\n",
        'eslint-plugin/index.js': "const r = require('./rules/r.js')\n",
        'scripts/side.mjs': '',
        'scripts/from.mjs': 'export const x = 1\n',
        'eslint-plugin/rules/r.js': 'module.exports = {}\n',
      },
      ['scripts/audit-mockup-*.mjs', 'scripts/side.mjs', 'scripts/from.mjs', 'eslint-plugin']
    )
    expect(r.counts).toEqual({ from: 1, side: 1, require: 1 })
    expect(r.gaps).toHaveLength(0)
  })

  it('入口集不只 scripts/ —— templates 里发给 consumer 的副本同样算入口', () => {
    const r = harness(
      {
        'templates/consumer-product/.githooks/hook.mjs': "import { x } from '../../../scripts/internal.mjs'\n",
        'scripts/internal.mjs': 'export const x = 1\n',
      },
      ['templates']
    )
    expect(r.entries).toEqual(['templates/consumer-product/.githooks/hook.mjs'])
    expect(r.gaps.map((g: any) => g.target)).toEqual(['scripts/internal.mjs'])
  })
})

describe('checkImportClosure — 注释噪声只 report-only，不造假缺陷', () => {
  it('注释里长得像 require 的字样解析不到实物 ⇒ 落 unresolved，不进 gaps', () => {
    const r = harness(
      {
        'eslint-plugin/index.js': "// require('...') and dynamic import('...')\nmodule.exports = {}\n",
      },
      ['eslint-plugin']
    )
    expect(r.gaps).toHaveLength(0)
    expect(r.unresolved.length).toBeGreaterThan(0)
    expect(r.unresolved[0]).toMatchObject({ spec: '...' })
  })

  it('⚠️ 但它照样被打印出来，不静默丢 —— unresolved 不是空数组就是可见的', () => {
    const r = harness({ 'eslint-plugin/index.js': "require('./gone.js')\n" }, ['eslint-plugin'])
    expect(r.unresolved).toHaveLength(1)
    expect(r.gaps).toHaveLength(0)
  })
})

describe('checkImportClosure — 登记盲区只测量不阻塞', () => {
  it('dynamic import / new URL 计入 blind，不进 gaps', () => {
    const r = harness(
      {
        'scripts/audit-mockup-a.mjs':
          "const m = await import('./lazy.mjs')\nconst u = new URL('./data.json', import.meta.url)\n",
        'scripts/lazy.mjs': '',
        'scripts/data.json': '{}',
      },
      ['scripts/audit-mockup-*.mjs']
    )
    expect(r.gaps).toHaveLength(0)
    expect(r.blind).toHaveLength(2)
    // 两个 target 都不在 files[] ⇒ 盲区"开始承重"的信号会被标出来，但不阻塞
    expect(r.blind.filter((b: any) => b.uncovered)).toHaveLength(2)
  })
})

describe('checkImportClosure — 豁免表 shrink-only', () => {
  const sources = {
    'scripts/audit-mockup-a.mjs': "import { x } from './helper.mjs'\n",
    'scripts/helper.mjs': 'export const x = 1\n',
  }
  const gap = { from: 'scripts/audit-mockup-a.mjs', target: 'scripts/helper.mjs', since: '2026-08-20', why: 'x' }

  it('具名豁免命中 ⇒ 该缺口不算未豁免', () => {
    const r = harness(sources, ['scripts/audit-mockup-*.mjs'], [gap])
    expect(r.gaps).toHaveLength(1)
    expect(r.unexcused).toHaveLength(0)
    expect(r.staleExemptions).toHaveLength(0)
  })

  it('缺口修好后豁免变 stale ⇒ 必须报出来要求删行（否则留永久空洞）', () => {
    const r = harness(sources, ['scripts/audit-mockup-*.mjs', 'scripts/helper.mjs'], [gap])
    expect(r.gaps).toHaveLength(0)
    expect(r.staleExemptions).toHaveLength(1)
  })

  it('豁免只挡它自己那一对，别的缺口照样红', () => {
    const r = harness(
      { ...sources, 'scripts/audit-mockup-b.mjs': "import { y } from './other.mjs'\n", 'scripts/other.mjs': 'export const y = 1\n' },
      ['scripts/audit-mockup-*.mjs'],
      [gap]
    )
    expect(r.unexcused.map((g: any) => g.target)).toEqual(['scripts/other.mjs'])
  })
})

describe('checkImportClosure — fail-closed（假绿的三个入口）', () => {
  it('files[] 空 ⇒ problems 非空', () => {
    const r = harness({ 'scripts/a.mjs': '' }, [])
    expect(r.problems.length).toBeGreaterThan(0)
  })

  it('tracked 空 ⇒ problems 非空', () => {
    const r = checkImportClosure({
      files: ['scripts/audit-mockup-*.mjs'],
      tracked: [],
      readSource: () => null,
      probeExists: () => false,
    })
    expect(r.problems.length).toBeGreaterThan(0)
  })

  it('入口集为空 ⇒ 不当"没有缺口"，而是判据形态塌了', () => {
    const r = harness({ 'docs/x.md': '' }, ['docs/x.md'])
    expect(r.entries).toHaveLength(0)
    expect(r.problems.join('\n')).toMatch(/入口集为空/)
  })

  it('入口集非空但 0 条边 ⇒ 形态识别器塌了，fail closed', () => {
    const r = harness({ 'scripts/audit-mockup-a.mjs': 'export const x = 1\n' }, ['scripts/audit-mockup-*.mjs'])
    expect(r.entries).toHaveLength(1)
    expect(r.edgeTotal).toBe(0)
    expect(r.problems.join('\n')).toMatch(/形态识别器塌了/)
  })
})

describe('真仓库非空过钉（少了这段，上面全部 fixture 断言可在真树零覆盖时照样全绿）', () => {
  const result = checkImportClosure({
    files: JSON.parse(readFileSync(resolve(REPO, 'package.json'), 'utf8')).files,
    tracked: execFileSync('git', ['ls-files'], { cwd: REPO, encoding: 'utf8' }).split('\n').filter(Boolean),
    readSource: (rel: string) => {
      const abs = resolve(REPO, rel)
      return existsSync(abs) ? readFileSync(abs, 'utf8') : null
    },
    probeExists: (rel: string) => existsSync(resolve(REPO, rel)),
    knownGaps: [],
  })

  it('真 files[] 下入口集非空，且横跨不止一个顶层目录', () => {
    expect(result.entries.length).toBeGreaterThan(10)
    const tops = new Set(result.entries.map((e: string) => e.split('/')[0]))
    expect(tops.size).toBeGreaterThan(1)
  })

  it('真树上解析出的静态边 > 0（正则真的咬到了东西）', () => {
    expect(result.edgeTotal).toBeGreaterThan(0)
  })

  it('真树上输入面没塌', () => {
    expect(result.problems).toEqual([])
  })
})
