// INFRA-F99: scripts/lib/render-inputs-fingerprint.mjs 的单测。
//
// 这个模块是**写方与验方共用的唯一实现** —— 它错了不会有人报错，只会让 S9 恒红或恒绿。
// 所以每条正向断言都配一条**反向**断言（"改了就变" 必须配 "没改就不变"），否则
// 「指纹变了」可能只是因为每次调用都返回随机值。
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import {
  FINGERPRINT_ALGO,
  RENDER_INPUT_PATHS,
  computeRenderInputsFingerprint,
} from '../scripts/lib/render-inputs-fingerprint.mjs'

let root: string

/** 搭一棵**最小但形状真实**的输入树：`src/` 子目录 + 那份 manifest。 */
function scaffold() {
  fs.mkdirSync(path.join(root, 'src/components/Button'), { recursive: true })
  fs.mkdirSync(path.join(root, 'figma-data'), { recursive: true })
  fs.writeFileSync(path.join(root, 'src/index.ts'), 'export const a = 1\n')
  fs.writeFileSync(path.join(root, 'src/components/Button/Button.vue'), '<template>b</template>\n')
  fs.writeFileSync(path.join(root, 'figma-data/render-verification-manifest.json'), '[{"id":1}]\n')
}

beforeEach(() => {
  root = fs.mkdtempSync(path.join(os.tmpdir(), 'f99-fp-'))
  scaffold()
})
afterEach(() => {
  fs.rmSync(root, { recursive: true, force: true })
})

describe('render-inputs-fingerprint', () => {
  it('同一棵树连算两次结果相同（阴性对照 —— 若这条不成立，下面所有「变了」都不能证明是内容变了）', () => {
    const a = computeRenderInputsFingerprint(root)
    const b = computeRenderInputsFingerprint(root)
    expect(a.value).toBe(b.value)
    expect(a.algo).toBe(FINGERPRINT_ALGO)
    expect(a.files).toBe(3)
  })

  it('改一个 src 文件的内容 → 指纹变', () => {
    const before = computeRenderInputsFingerprint(root).value
    fs.writeFileSync(path.join(root, 'src/components/Button/Button.vue'), '<template>c</template>\n')
    expect(computeRenderInputsFingerprint(root).value).not.toBe(before)
  })

  it('改 manifest 的内容 → 指纹变', () => {
    const before = computeRenderInputsFingerprint(root).value
    fs.writeFileSync(path.join(root, 'figma-data/render-verification-manifest.json'), '[{"id":2}]\n')
    expect(computeRenderInputsFingerprint(root).value).not.toBe(before)
  })

  it('新增 / 删除文件 → 指纹变，且 files 计数跟着动', () => {
    const before = computeRenderInputsFingerprint(root)
    fs.writeFileSync(path.join(root, 'src/extra.ts'), 'x\n')
    const added = computeRenderInputsFingerprint(root)
    expect(added.value).not.toBe(before.value)
    expect(added.files).toBe(before.files + 1)
    fs.rmSync(path.join(root, 'src/extra.ts'))
    expect(computeRenderInputsFingerprint(root).value).toBe(before.value)
  })

  it('⭐ 只把内容在两个文件之间对调 → 指纹仍必须变（路径进摘要，不只是内容）', () => {
    const p1 = path.join(root, 'src/index.ts')
    const p2 = path.join(root, 'src/extra.ts')
    fs.writeFileSync(p1, 'AAA\n')
    fs.writeFileSync(p2, 'BBB\n')
    const before = computeRenderInputsFingerprint(root).value
    fs.writeFileSync(p1, 'BBB\n')
    fs.writeFileSync(p2, 'AAA\n')
    expect(computeRenderInputsFingerprint(root).value).not.toBe(before)
  })

  it('⭐ 点开头文件（.DS_Store 那一类）不进指纹 —— 配阳性对照：同目录的非点文件照收', () => {
    const before = computeRenderInputsFingerprint(root)
    fs.writeFileSync(path.join(root, 'src/.DS_Store'), 'macos junk\n')
    fs.writeFileSync(path.join(root, 'src/components/.DS_Store'), 'more junk\n')
    const withJunk = computeRenderInputsFingerprint(root)
    expect(withJunk.value).toBe(before.value)
    expect(withJunk.files).toBe(before.files)
    // 阳性对照：同一个目录里放一个**非**点开头的文件 → 必须被收进去。
    // 少了这条，上面那个 toBe 也可能是因为整个枚举坏了（恒定返回同一值）。
    fs.writeFileSync(path.join(root, 'src/components/notjunk.ts'), 'real\n')
    const withReal = computeRenderInputsFingerprint(root)
    expect(withReal.value).not.toBe(before.value)
    expect(withReal.files).toBe(before.files + 1)
  })

  it('点开头的**目录**整棵不进指纹（形态同上，但走的是递归那一支）', () => {
    const before = computeRenderInputsFingerprint(root).value
    fs.mkdirSync(path.join(root, 'src/.cache/deep'), { recursive: true })
    fs.writeFileSync(path.join(root, 'src/.cache/deep/x.js'), 'junk\n')
    expect(computeRenderInputsFingerprint(root).value).toBe(before)
  })

  it('输入路径缺失 → throw（⛔ fail closed，不返回空指纹）', () => {
    fs.rmSync(path.join(root, 'figma-data/render-verification-manifest.json'))
    expect(() => computeRenderInputsFingerprint(root)).toThrow(/输入路径不存在/)
  })

  it('src 整个空掉（0 个文件）→ throw（「没有输入」≠「输入没变」）', () => {
    fs.rmSync(path.join(root, 'src'), { recursive: true })
    fs.mkdirSync(path.join(root, 'src'))
    fs.rmSync(path.join(root, 'figma-data/render-verification-manifest.json'))
    fs.mkdirSync(path.join(root, 'figma-data'), { recursive: true })
    expect(() => computeRenderInputsFingerprint(root)).toThrow()
  })

  it('输入面就是 RENDER_INPUT_PATHS 那两条，改它以外的路径不影响指纹（最窄读法的可执行形式）', () => {
    expect([...RENDER_INPUT_PATHS].sort()).toEqual([
      'figma-data/render-verification-manifest.json',
      'src',
    ])
    const before = computeRenderInputsFingerprint(root).value
    // harness / docs CSS 那一类 —— 如实登记的边界：它**能**改变渲出的数值，但 S9 不看。
    fs.mkdirSync(path.join(root, 'playground/docs'), { recursive: true })
    fs.writeFileSync(path.join(root, 'playground/docs/docs.css'), '.x{color:red}\n')
    expect(computeRenderInputsFingerprint(root).value).toBe(before)
  })

  it('真仓库上跑得动，且不是空过（files 远大于 0、value 是 64 位 hex）', () => {
    // ⚠️ 非空过钉：上面全部用临时目录，压缩样本可能把真树的形态问题掩掉
    //（本仓已实证过 plan 内联 fixture 与真判据不同的坑）。这条打真仓库。
    const real = computeRenderInputsFingerprint(process.cwd())
    expect(real.files).toBeGreaterThan(50)
    expect(real.value).toMatch(/^[0-9a-f]{64}$/)
  })
})
