// tests/composition-exports.test.ts
import { describe, it, expect } from 'vitest'
import { writeFileSync, mkdirSync, rmSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { buildComposition, emitJson, emitJs, emitDts } from '../figma-sync/generate-composition-exports.mjs'
import { checkCompositionFresh } from '../scripts/audit-composition-exports.mjs'

// Minimal fixture mirroring the shape of docs/internal/component-affordances.json.
const FIXTURE = {
  _meta: {
    title: 'Component Affordances (test)',
    is_source_of_truth: true,
    field_docs: { code_name: 'canonical name' },
  },
  components: [
    {
      code_name: 'InputNumber',
      npm_export: 'InputNumber',
      figma_name: 'InputNumber',
      category: 'form-input',
      summary: '数字步进输入框',
      do_not_hand_compose: '别自拼步进器',
      composition: { contained_by: ['FormItem'], contains: [] },
      built_in_features: [{ feature: '加减步进', controlled_by: 'prop:step' }],
      status: 'verified',
    },
    {
      code_name: 'FormItem',
      npm_export: 'FormItem',
      figma_name: 'FormItem',
      category: 'form-input',
      summary: '表单行容器',
      do_not_hand_compose: '别手拼表单行',
      composition: { contained_by: [], contains: ['InputNumber'] },
      built_in_features: [],
      status: 'verified',
    },
  ],
}

const HERE = dirname(fileURLToPath(import.meta.url))
const TMP_DIR = join(HERE, '__tmp-composition-test__')

describe('buildComposition (pure)', () => {
  const built = buildComposition(FIXTURE)

  it('preserves _meta verbatim', () => {
    expect(built._meta).toEqual(FIXTURE._meta)
    expect(built._meta.is_source_of_truth).toBe(true)
  })

  it('keys components by code_name for O(1) lookup', () => {
    expect(Object.keys(built.components).sort()).toEqual(['FormItem', 'InputNumber'])
    expect(built.components.InputNumber.category).toBe('form-input')
  })

  it('maps composition fields correctly', () => {
    expect(built.components.InputNumber.composition.contained_by).toEqual(['FormItem'])
    expect(built.components.FormItem.composition.contains).toEqual(['InputNumber'])
  })

  it('exposes names list in source order', () => {
    expect(built.names).toEqual(['InputNumber', 'FormItem'])
  })

  it('throws on a duplicate code_name (no silent overwrite)', () => {
    const dup = { _meta: {}, components: [{ code_name: 'X' }, { code_name: 'X' }] }
    expect(() => buildComposition(dup)).toThrow(/duplicate/i)
  })
})

describe('emitJson', () => {
  it('passes the source through verbatim (incl. _meta)', () => {
    const parsed = JSON.parse(emitJson(FIXTURE))
    expect(parsed).toEqual(FIXTURE)
    expect(parsed._meta.is_source_of_truth).toBe(true)
  })
})

describe('emitJs', () => {
  const js = emitJs(FIXTURE)

  it('declares composition + named exports, no TS-only syntax', () => {
    expect(js).toContain('export const composition')
    expect(js).toContain('export const components')
    expect(js).toContain('export const componentNames')
    expect(js).not.toContain('as const')
    expect(js).not.toContain('export type')
  })

  it('is loadable ESM with the resolved runtime object', async () => {
    mkdirSync(TMP_DIR, { recursive: true })
    const file = join(TMP_DIR, 'composition.mjs')
    writeFileSync(file, js)
    try {
      const mod = await import(fileURLToPath(new URL(`file://${file}`)))
      expect(mod.composition.components.InputNumber.composition.contained_by).toEqual(['FormItem'])
      expect(mod.components.FormItem.composition.contains).toEqual(['InputNumber'])
      expect(mod.componentNames).toEqual(['InputNumber', 'FormItem'])
      expect(mod.composition._meta.is_source_of_truth).toBe(true)
    } finally {
      rmSync(TMP_DIR, { recursive: true, force: true })
    }
  })
})

describe('emitDts', () => {
  const dts = emitDts(FIXTURE)

  it('builds a ComponentName union from every code_name', () => {
    expect(dts).toContain('export type ComponentName =')
    expect(dts).toContain("'InputNumber'")
    expect(dts).toContain("'FormItem'")
  })

  it('declares the composition record structure', () => {
    expect(dts).toContain('export declare const composition')
    expect(dts).toContain('export interface CompositionEntry')
    expect(dts).toContain('contained_by')
    expect(dts).toContain('contains')
  })
})

describe('audit-composition-exports drift check', () => {
  it('passes when on-disk matches freshly-built', () => {
    const fresh = emitJson(FIXTURE)
    expect(checkCompositionFresh(fresh, JSON.parse(fresh)).ok).toBe(true)
  })

  it('fails when on-disk drifts from source', () => {
    const stale = JSON.parse(emitJson(FIXTURE))
    stale.components[0].category = 'changed'
    const r = checkCompositionFresh(emitJson(FIXTURE), stale)
    expect(r.ok).toBe(false)
    expect(r.message).toMatch(/drift/i)
  })
})
