// tests/figma-file-chunked.test.ts
// -----------------------------------------------------------------------------
// [[INFRA-F103]] 修法 A 的单测。
//
// 🔑 **本文件的核心是「等价证明」那一组**：分片这条路的唯一新风险是「拼回去拼错」，而拼错的
// 后果是**静默读错一棵树**（mockup 审计的输入）。所以它必须被证，不能只被断言。
// 证法 = 拿一份**真实的 `getFile()` 产物**（`figma-data/mockup/jnlIId30lS3xjRmZnZTWQy.json`，
// 由本仓 sync 管线成功产出、非手造 fixture），按 Figma 的 depth=1 / `nodes?ids=` 语义切成片，
// 再用 `assembleChunkedFile()` 拼回，断言与原件 **deep-equal**。
//
// ⚠️ 那份 dump 是 gitignored 未跟踪产物、**只存在于主工作树**（worktree 里靠软链）。取不到时
// 本组测试 **skip 并打印原因**，⛔ 不是静默通过 —— 「取证环境造的假绿」与「假缺陷」是同一个坑。
// -----------------------------------------------------------------------------
import { describe, it, expect } from 'vitest'
import { existsSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  assembleChunkedFile,
  fetchFileChunked,
  FILE_LEVEL_MAPS,
} from '../scripts/lib/figma-file-chunked.mjs'

const REPO_ROOT = resolve(__dirname, '..')
const REAL_DUMP = resolve(REPO_ROOT, 'figma-data/mockup/jnlIId30lS3xjRmZnZTWQy.json')

/** 按 Figma 的 depth=1 语义造外壳：顶层字段全在，各 page 只留自身属性、不带 children。 */
function makeShell(file: any) {
  const shell = structuredClone(file)
  shell.document = {
    ...shell.document,
    children: shell.document.children.map((p: any) => {
      const { children, ...rest } = p
      return rest
    }),
  }
  return shell
}

/** 按 `/files/{key}/nodes?ids=<pageId>` 语义造逐 page 响应。 */
function makeNodeResponses(file: any) {
  return file.document.children.map((page: any) => ({
    nodes: { [page.id]: { document: structuredClone(page) } },
  }))
}

describe('assembleChunkedFile — 等价证明（对着真实 getFile() 产物）', () => {
  const have = existsSync(REAL_DUMP)
  if (!have) {
    it.skip('真实 dump 取不到 —— figma-data/mockup 是 gitignored 未跟踪产物，只在主工作树', () => {})
  }

  it.skipIf(!have)('切片再拼回 === 原件（deep-equal，含顶层全部字段）', () => {
    const real = JSON.parse(readFileSync(REAL_DUMP, 'utf8')).file
    const { file, missingPages } = assembleChunkedFile(makeShell(real), makeNodeResponses(real))
    expect(missingPages).toEqual([])
    expect(file).toEqual(real)
  })

  it.skipIf(!have)('致败探针：外壳少一个 page 的子树 → 必须报进 missingPages 而不是悄悄少一页', () => {
    const real = JSON.parse(readFileSync(REAL_DUMP, 'utf8')).file
    const responses = makeNodeResponses(real)
    const dropped = responses.pop() // 故意丢掉最后一页
    const droppedId = Object.keys(dropped!.nodes)[0]
    const { file, missingPages } = assembleChunkedFile(makeShell(real), responses)
    expect(missingPages).toEqual([droppedId])
    // 那一页仍在（保留外壳里的浅版本），⛔ 不是被删掉
    expect(file.document.children.map((p: any) => p.id)).toEqual(
      real.document.children.map((p: any) => p.id),
    )
    // 且它确实是浅的 —— 证明这个探针真的造成了差异（不是空过）
    const shallow = file.document.children.find((p: any) => p.id === droppedId)
    expect(shallow.children).toBeUndefined()
  })

  it.skipIf(!have)('阴性对照：那份 dump 确实是「真 getFile() 产物」而不是我造的形态', () => {
    const raw = JSON.parse(readFileSync(REAL_DUMP, 'utf8'))
    expect(raw._meta?.source).toBe('figma-api')
    expect(raw.file.document.type).toBe('DOCUMENT')
    expect(raw.file.document.children.length).toBeGreaterThan(0)
    // 顶层字段是 Figma 的真实集合，不是三五个手写 key
    expect(Object.keys(raw.file).length).toBeGreaterThanOrEqual(10)
  })
})

describe('assembleChunkedFile — 三条刻意行为', () => {
  const shell = {
    name: 'F',
    document: {
      id: '0:0',
      type: 'DOCUMENT',
      children: [
        { id: '1:1', type: 'CANVAS', name: 'A' },
        { id: '2:2', type: 'CANVAS', name: 'B' },
      ],
    },
    components: { c1: { key: 'shell' } },
  }

  it('① page 顺序以 shell 为准，不随响应到达顺序变', () => {
    const responses = [
      { nodes: { '2:2': { document: { id: '2:2', type: 'CANVAS', name: 'B', children: [2] } } } },
      { nodes: { '1:1': { document: { id: '1:1', type: 'CANVAS', name: 'A', children: [1] } } } },
    ]
    const { file } = assembleChunkedFile(shell, responses)
    expect(file.document.children.map((p: any) => p.id)).toEqual(['1:1', '2:2'])
  })

  it('② 没取到的 page 保持原样并进 missingPages（⛔ 不静默丢页）', () => {
    const { file, missingPages } = assembleChunkedFile(shell, [
      { nodes: { '1:1': { document: { id: '1:1', type: 'CANVAS', name: 'A', children: [1] } } } },
    ])
    expect(missingPages).toEqual(['2:2'])
    expect(file.document.children).toHaveLength(2)
  })

  it('③ 文件级 map 取并集且 shell 优先', () => {
    const { file } = assembleChunkedFile(shell, [
      {
        nodes: {
          '1:1': {
            document: { id: '1:1', type: 'CANVAS', name: 'A' },
            components: { c1: { key: 'from-node' }, c2: { key: 'new' } },
            styles: { s1: {} },
          },
        },
      },
    ])
    expect(file.components).toEqual({ c1: { key: 'shell' }, c2: { key: 'new' } })
    expect(file.styles).toEqual({ s1: {} })
  })

  it('shell 上本来没有的 map key 不凭空造（保持与整包响应同形）', () => {
    const { file } = assembleChunkedFile(shell, [])
    expect('styles' in file).toBe(false)
    expect('componentSets' in file).toBe(false)
    expect(FILE_LEVEL_MAPS).toContain('styles')
  })

  it('外壳形态不对 → 抛错，⛔ 不返回一个半成品', () => {
    expect(() => assembleChunkedFile(null as any, [])).toThrow(/必须是对象/)
    expect(() => assembleChunkedFile({ document: {} } as any, [])).toThrow(/depth=1/)
  })
})

describe('fetchFileChunked — 请求形态', () => {
  const mkShell = () => ({
    document: {
      id: '0:0',
      type: 'DOCUMENT',
      children: [
        { id: '1:1', type: 'CANVAS', name: 'A' },
        { id: '2:2', type: 'CANVAS', name: 'B' },
      ],
    },
  })

  it('depth=1 一次 + 逐 page 各一次，且顺序是串行的', async () => {
    const calls: string[] = []
    const getJson = async (path: string) => {
      calls.push(path)
      if (path.includes('depth=1')) return mkShell()
      const id = decodeURIComponent(path.split('ids=')[1])
      return { nodes: { [id]: { document: { id, type: 'CANVAS', children: [] } } } }
    }
    const r = await fetchFileChunked({ getJson, fileKey: 'K' })
    expect(calls).toEqual([
      '/files/K?depth=1',
      '/files/K/nodes?ids=1%3A1',
      '/files/K/nodes?ids=2%3A2',
    ])
    expect(r.requests).toBe(3)
    expect(r.missingPages).toEqual([])
  })

  it('pageIds 过滤时只拉那一页，且被过滤掉的不算「漏」', async () => {
    const getJson = async (path: string) => {
      if (path.includes('depth=1')) return mkShell()
      const id = decodeURIComponent(path.split('ids=')[1])
      return { nodes: { [id]: { document: { id, type: 'CANVAS', children: [] } } } }
    }
    const r = await fetchFileChunked({ getJson, fileKey: 'K', pageIds: ['2:2'] })
    expect(r.requests).toBe(2)
    expect(r.missingPages).toEqual([])
    expect(r.file.document.children.find((p: any) => p.id === '2:2').children).toEqual([])
  })
})
