// tests/audit-figma-env-single-source.test.ts
import { describe, it, expect, afterEach } from 'vitest'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { auditFigmaEnvSingleSource } from '../scripts/audit-figma-env-single-source.mjs'

// `package.json` 是 type:module —— 显式派生，不依赖 vitest 注入 __dirname
const HERE = dirname(fileURLToPath(import.meta.url))

// ⛔ **本文件自己在闸的内容面扫描面内**（它是 `.ts`，且不在闸的结构性自排除表里）⇒ 两个 Figma
// REST 标记**必须拼接构造，源码里不许出现连续字面量**，否则真仓库那条钉会把本文件收进内容面
// 分母、报 S6「含标记却没 import 共享模块」—— 探针名污染判据的经典形态。
// 落地当日实证：拼接后真仓库内容面 = 12 份，本文件不在其中。
const MARK_HEADER = ['X-Figma', '-Token'].join('')
const MARK_HOST = ['api', 'figma', 'com'].join('.')

const dirs: string[] = []
const SHARED_OK = [
  'export function loadDotEnv() {}',
  'export function getFigmaToken() {}',
  'export function requireFigmaToken() {}',
].join('\n')

// 内容面的合规种子：既满足 S5（分母非 0），又不触发 S6（真 import 了共享模块）。
// 没有它，所有既有「期望绿」的 fixture 都会被 S5 分母 fail-closed 判红。
const CONTENT_SEED = [
  "import { requireFigmaToken } from './lib/figma-env.mjs'",
  `const r = await fetch('https://${MARK_HOST}/v1/files/x', {`,
  `  headers: { '${MARK_HEADER}': requireFigmaToken() },`,
  '})',
].join('\n')

function fixture({
  audits = {},
  shared = SHARED_OK as string | null,
  extra = {} as Record<string, string>,
  contentSeed = true,
} = {}): string {
  const root = mkdtempSync(join(tmpdir(), 'f98gate-'))
  dirs.push(root)
  mkdirSync(join(root, 'scripts', 'lib'), { recursive: true })
  if (shared !== null) writeFileSync(join(root, 'scripts', 'lib', 'figma-env.mjs'), shared)
  for (const [name, body] of Object.entries(audits)) {
    writeFileSync(join(root, 'scripts', name), body as string)
  }
  if (contentSeed) writeFileSync(join(root, 'scripts', 'figma-rest-seed.mjs'), CONTENT_SEED)
  for (const [rel, body] of Object.entries(extra)) {
    const abs = join(root, rel)
    mkdirSync(dirname(abs), { recursive: true })
    writeFileSync(abs, body)
  }
  return root
}
afterEach(() => { while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true }) })

describe('S1 分母 fail closed', () => {
  it('fails when no audit-mockup-*.mjs is found at all', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {} }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S1/)
  })
})

describe('S2 不得直读 process.env.FIGMA_', () => {
  it('passes when every audit script goes through the shared module', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': "import { requireFigmaToken } from './lib/figma-env.mjs'\nconst T = requireFigmaToken()\n",
    } }) })
    expect(r.ok).toBe(true)
    expect(r.scanned).toHaveLength(1)
  })

  it('fails and names the file when one reads process.env.FIGMA_ directly', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = process.env.FIGMA_PERSONAL_ACCESS_TOKEN\n',
    } }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
    expect(r.failures.join('\n')).toContain('audit-mockup-a.mjs')
  })

  it('also catches the alias name', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = process.env.FIGMA_TOKEN\n',
    } }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
  })

  it('does not flag the variable name appearing in a comment or message', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': "// Requires: FIGMA_PERSONAL_ACCESS_TOKEN\nconsole.error('Missing FIGMA_PERSONAL_ACCESS_TOKEN (or FIGMA_TOKEN)')\n",
    } }) })
    expect(r.ok).toBe(true)
  })

  it('flags bracket access with single quotes', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': "const T = process.env['FIGMA_TOKEN']\n",
    } }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
    expect(r.failures.join('\n')).toContain('audit-mockup-a.mjs')
  })

  it('flags bracket access with double quotes', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = process.env["FIGMA_PERSONAL_ACCESS_TOKEN"]\n',
    } }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
  })

  it('flags single-line destructuring off process.env', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const { FIGMA_PERSONAL_ACCESS_TOKEN } = process.env\n',
    } }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
  })

  it('still does not flag comment/error-message bare names after broadening (no bare-name regression)', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': "// FIGMA_TOKEN or FIGMA_PERSONAL_ACCESS_TOKEN\nconsole.error('need FIGMA_TOKEN')\n",
    } }) })
    expect(r.ok).toBe(true)
  })

  // review round 2 finding 4 — two escapes a reader would assume the header already covers.
  it('flags optional-chaining dot access: process.env?.FIGMA_TOKEN', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = process.env?.FIGMA_TOKEN\n',
    } }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
    expect(r.failures.join('\n')).toContain('audit-mockup-a.mjs')
  })

  it('flags bracket access with a literal backtick key: process.env[`FIGMA_TOKEN`]', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = process.env[`FIGMA_TOKEN`]\n',
    } }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
    expect(r.failures.join('\n')).toContain('audit-mockup-a.mjs')
  })
})

describe('S3 共享模块必须在且导出齐全', () => {
  it('fails when the shared module is missing', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = 1\n',
    }, shared: null }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S3/)
  })

  it('fails when an export was renamed away', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = 1\n',
    }, shared: 'export function loadDotEnv() {}\nexport function getFigmaToken() {}\n' }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toContain('requireFigmaToken')
  })

  it('accepts the export const arrow-function form (style refactor, not a behavior change)', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({ audits: {
      'audit-mockup-a.mjs': 'const T = 1\n',
    }, shared: [
      'export const loadDotEnv = () => {}',
      'export const getFigmaToken = () => {}',
      'export const requireFigmaToken = () => {}',
    ].join('\n') }) })
    expect(r.ok).toBe(true)
  })
})

// ─────────────────────────────────────────────────────────────────────────────
// S5 / S6 —— 内容面（INFRA-F101 ①，2026-08-20）。**与 S1/S2/S4 是并集，不是替换。**
// ─────────────────────────────────────────────────────────────────────────────
describe('S5 内容面分母 fail closed', () => {
  it('fails when nothing in the tree carries a Figma REST marker at all', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      contentSeed: false,
    }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S5/)
  })

  it('does not fire S5 once one marker-carrying file exists', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
    }) })
    expect(r.ok).toBe(true)
    expect(r.contentScanned).toContain('scripts/figma-rest-seed.mjs')
  })
})

describe('S6 含 Figma REST 标记者必须 import 共享模块', () => {
  it('flags a marker-carrying file that never imports the shared module', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      extra: { 'figma-sync/rogue.mjs': `fetch('https://${MARK_HOST}/v1/me')\n` },
    }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S6/)
    expect(r.failures.join('\n')).toContain('figma-sync/rogue.mjs')
  })

  it('accepts it once it imports the shared module', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      extra: { 'figma-sync/ok.mjs': [
        "import { getFigmaToken } from '../scripts/lib/figma-env.mjs'",
        `fetch('https://${MARK_HOST}/v1/me', { headers: { '${MARK_HEADER}': getFigmaToken() } })`,
      ].join('\n') },
    }) })
    expect(r.ok).toBe(true)
    expect(r.contentScanned).toContain('figma-sync/ok.mjs')
  })

  it('catches the request-header marker too, not only the API host', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      extra: { 'scripts/hdr-only.mjs': `const h = { '${MARK_HEADER}': 'x' }\n` },
    }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toContain('scripts/hdr-only.mjs')
  })

  // 阴性对照 0 —— import 判据跳注释行：一句注释里提到路径不算 import。
  // 不加这条守卫，真绕过者只要写一行 `// import … figma-env.mjs` 就能过 S6。
  it('a commented-out import does not count as importing the shared module', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      extra: { 'scripts/commented.mjs': [
        "// import { getFigmaToken } from './lib/figma-env.mjs'  ← 只是注释，不算",
        `fetch('https://${MARK_HOST}/v1/me')`,
      ].join('\n') },
    }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S6/)
    expect(r.failures.join('\n')).toContain('scripts/commented.mjs')
  })

  // 阴性对照 1 —— 扩展名过滤。entry 实测：不过滤会有 7 份 markdown 即时假红。
  it('does not pull markdown into the denominator (the 7-false-red guard)', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      extra: { 'docs/internal/backlog.md': `讲的就是 ${MARK_HOST} 和 ${MARK_HEADER} 这两个标记\n` },
    }) })
    expect(r.ok).toBe(true)
    expect(r.contentScanned.join(' ')).not.toContain('backlog.md')
  })

  // 阴性对照 2 —— 共享模块自身在结构性自排除表里：要求它 import 自己是无意义的。
  it('never asks the shared module to import itself', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      shared: `${SHARED_OK}\n// 本模块提到 ${MARK_HOST} / ${MARK_HEADER} 也不该被自己抓\n`,
    }) })
    expect(r.ok).toBe(true)
    expect(r.contentScanned).not.toContain('scripts/lib/figma-env.mjs')
  })

  // 已声明的裁剪边界（不是漏做）：点目录与 dist* 下的文件不入分母。
  it('prunes dot-directories and dist* (declared boundary, keeps other worktrees out)', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      extra: {
        '.claude/worktrees/other/scripts/rogue.mjs': `fetch('https://${MARK_HOST}/v1/me')\n`,
        'dist/bundled.js': `fetch('https://${MARK_HOST}/v1/me')\n`,
      },
    }) })
    expect(r.ok).toBe(true)
  })
})

describe('并集证明：两个分母各自抓另一个漏掉的', () => {
  // 内容面抓文件名面漏的：不叫 audit-mockup-* ⇒ S2 看不见它。
  it('content side catches a REST consumer the filename side cannot see', () => {
    const root = fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = 1\n' },
      extra: { 'scripts/ds-health-scan-whatever.mjs': [
        `fetch('https://${MARK_HOST}/v1/me')`,
        'const T = process.env.FIGMA_TOKEN',
      ].join('\n') },
    })
    const r = auditFigmaEnvSingleSource({ root })
    expect(r.ok).toBe(false)
    // 该文件不在文件名面分母里 —— 证明它确实是 S2 抓不到的那一类
    expect(r.scanned).not.toContain('ds-health-scan-whatever.mjs')
    expect(r.failures.join('\n')).toMatch(/S6/)
    expect(r.failures.join('\n')).toContain('scripts/ds-health-scan-whatever.mjs')
  })

  // 文件名面抓内容面漏的：有直读、但**不含**任何 REST 标记 ⇒ S6 看不见它，S2 必须仍在。
  // 这一条钉住「S2 没有被替换掉」—— entry 里 ⛔「必须写成并集」的那半边。
  it('filename side still catches a direct read in a file with no REST marker', () => {
    const r = auditFigmaEnvSingleSource({ root: fixture({
      audits: { 'audit-mockup-a.mjs': 'const T = process.env.FIGMA_PERSONAL_ACCESS_TOKEN\n' },
    }) })
    expect(r.ok).toBe(false)
    expect(r.failures.join('\n')).toMatch(/S2/)
    // 且它没被内容面看见 —— 否则这条测不出「并集」，只测出「有人抓到了」
    expect(r.contentScanned).not.toContain('scripts/audit-mockup-a.mjs')
  })
})

// ⛔ 与 MARK_HEADER / MARK_HOST 同理：**本文件也在 S8 的扫描面内**（`.ts`，且不在
// ENV_PARSER_SELF_EXCLUDE 里）⇒ 判据的字面形态一律拼接构造，源码里不许出现连续字面量，
// 否则下方真仓库那条钉会把本文件自己报成 S8 违规 —— 探针名污染判据的经典形态。
const DOT_ENV = ['.', 'env'].join('')
const DOTENV_PKG = ['dot', 'env'].join('')

describe('S7 / S8 — `.env` parser 单一化（INFRA-F101 ②，严格字面档）', () => {
  it('S8 抓 dotenv 依赖', () => {
    const r = auditFigmaEnvSingleSource({
      root: fixture({ extra: { 'scripts/rogue.mjs': `import '${DOTENV_PKG}/config'\n` } }),
    })
    expect(r.failures.filter(f => f.startsWith('S8 ')).length).toBe(1)
    expect(r.failures.join('\n')).toContain('scripts/rogue.mjs:1')
  })

  it('S8 抓引号包住的路径字面量', () => {
    const r = auditFigmaEnvSingleSource({
      root: fixture({ extra: { 'scripts/rogue.mjs': `const t = readFileSync('${DOT_ENV}', 'utf8')\n` } }),
    })
    expect(r.failures.filter(f => f.startsWith('S8 ')).length).toBe(1)
  })

  it('S8 抓模板串收尾形态（上一条判据抓不到、而它是真读取）', () => {
    const r = auditFigmaEnvSingleSource({
      root: fixture({ extra: { 'scripts/rogue.mjs': 'const p = `${root}/' + DOT_ENV + '`\n' } }),
    })
    expect(r.failures.filter(f => f.startsWith('S8 ')).length).toBe(1)
  })

  it('阴性对照：注释行里提到它不算违规', () => {
    const r = auditFigmaEnvSingleSource({
      root: fixture({ extra: { 'scripts/ok.mjs': `// \`${DOT_ENV}\` 解析器全仓只有一份\n` } }),
    })
    expect(r.failures.filter(f => f.startsWith('S8 '))).toEqual([])
  })

  it('阴性对照：严格档看不见 .local / .example（这是档位边界，不是漏抓）', () => {
    const r = auditFigmaEnvSingleSource({
      root: fixture({
        extra: {
          'scripts/a.mjs': `readFileSync('${DOT_ENV}.local')\n`,
          'scripts/b.mjs': `readFileSync('${DOT_ENV}.example')\n`,
        },
      }),
    })
    expect(r.failures.filter(f => f.startsWith('S8 '))).toEqual([])
  })

  it('阴性对照：非注释行里的反引号文案不算（判据刻意不认反引号）', () => {
    const r = auditFigmaEnvSingleSource({
      root: fixture({
        extra: { 'scripts/ok.mjs': 'console.log(`成因：\\`' + DOT_ENV + '\\` 只在主工作树`)\n' },
      }),
    })
    expect(r.failures.filter(f => f.startsWith('S8 '))).toEqual([])
  })

  it('结构性自排除：共享模块本体自己解析不算违规（它就是那唯一一份）', () => {
    const r = auditFigmaEnvSingleSource({
      root: fixture({ shared: SHARED_OK + `\nconst p = resolve(root, '${DOT_ENV}')\n` }),
    })
    expect(r.failures.filter(f => f.startsWith('S8 '))).toEqual([])
  })

  it('S7 分母 fail closed：扫描面为空必须红，不能静静地空过', () => {
    const empty = mkdtempSync(join(tmpdir(), 'f98gate-empty-'))
    dirs.push(empty)
    const r = auditFigmaEnvSingleSource({ root: empty })
    expect(r.failures.some(f => f.startsWith('S7 '))).toBe(true)
  })
})

describe('真仓库非空过钉', () => {
  it('real repository: `.env` parser 面非空且零违规，本文件不在违规里', () => {
    const r = auditFigmaEnvSingleSource({ root: resolve(HERE, '..') })
    // 落地当日实测 857。用下界：新增源文件是正常增长，不该把这条钉变红。
    expect(r.envScanned).toBeGreaterThanOrEqual(500)
    expect(r.envViolations).toEqual([])
    // 自引用钉：本文件用拼接构造判据形态，必须不落进违规面
    expect(r.envViolations.map(v => v.file)).not.toContain('tests/audit-figma-env-single-source.test.ts')
    // 闸自身也不在自排除表里 —— 它不该靠豁免过关，而该靠判据不命中自己
    expect(r.envViolations.map(v => v.file)).not.toContain('scripts/audit-figma-env-single-source.mjs')
  })

  it('passes on the real repository and scans all ten audit-mockup scripts that need a token', () => {
    const r = auditFigmaEnvSingleSource({ root: resolve(HERE, '..') })
    expect(r.failures).toEqual([])
    expect(r.ok).toBe(true)
    expect(r.scanned.length).toBeGreaterThanOrEqual(10)
  })

  it('real repository: content side is non-empty and this test file is not in it', () => {
    const r = auditFigmaEnvSingleSource({ root: resolve(HERE, '..') })
    // 落地当日实测 12。用下界而非等号：新增 REST 脚本是正常增长，不该把这条钉变红。
    expect(r.contentScanned.length).toBeGreaterThanOrEqual(12)
    expect(r.contentScanned).toContain('figma-sync/api.mjs')
    expect(r.contentScanned).toContain('scripts/ds-health-scan-mockups.mjs')
    // 自引用钉：本文件用拼接构造标记，必须不落进分母
    expect(r.contentScanned).not.toContain('tests/audit-figma-env-single-source.test.ts')
    // 结构性自排除的两份也必须不在
    expect(r.contentScanned).not.toContain('scripts/audit-figma-env-single-source.mjs')
    expect(r.contentScanned).not.toContain('scripts/lib/figma-env.mjs')
  })

  it('real repository: the two denominators are genuinely different sets (union, not replacement)', () => {
    const r = auditFigmaEnvSingleSource({ root: resolve(HERE, '..') })
    const contentBasenames = new Set(r.contentScanned.map(f => f.split('/').pop()!))
    // 文件名面里有内容面看不见的（落地当日 = handoff-evidence / html-conformance / library-binding）
    const onlyFilename = r.scanned.filter(n => !contentBasenames.has(n))
    expect(onlyFilename.length).toBeGreaterThan(0)
    // 内容面里有文件名面看不见的（落地当日 = figma-sync/api.mjs + ds-health-scan-mockups.mjs）
    const onlyContent = r.contentScanned.filter(f => !r.scanned.includes(f.split('/').pop()!))
    expect(onlyContent.length).toBeGreaterThan(0)
  })
})
