import { describe, expect, setDefaultTimeout, test } from 'bun:test'

import {
  exportFigFile,
  FigmaAPI,
  initCodec,
  parseFigFile,
  SceneGraph,
  type Color
} from '@open-pencil/core'

import { expectDefined } from '#tests/helpers/assert'
import { parseFixture } from '#tests/helpers/fig-fixtures'
import { runsHeavyTests } from '#tests/helpers/test-utils'

setDefaultTimeout(60_000)

describe('variable roundtrip', () => {
  test('variables and collections survive export → re-import', async () => {
    await initCodec()

    const graph = new SceneGraph()
    const col = graph.createCollection('Design Tokens')
    graph.createVariable('color/primary', 'COLOR', col.id, { r: 0.23, g: 0.51, b: 0.96, a: 1 })
    graph.createVariable('spacing/base', 'FLOAT', col.id, 8)
    graph.createVariable('visible', 'BOOLEAN', col.id, true)
    graph.createVariable('label', 'STRING', col.id, 'Hello')

    const exported = await exportFigFile(graph)
    const reimported = await parseFigFile(exported.buffer as ArrayBuffer)

    expect(reimported.variables.size).toBe(4)
    expect(reimported.variableCollections.size).toBe(1)

    const reimportedCol = [...reimported.variableCollections.values()][0]
    expect(reimportedCol.name).toBe('Design Tokens')
    expect(reimportedCol.variableIds).toHaveLength(4)

    const vars = [...reimported.variables.values()]
    const colorVar = vars.find((v) => v.name === 'color/primary')
    expect(colorVar).toBeDefined()
    expect(expectDefined(colorVar, 'colorVar').type).toBe('COLOR')
    const colorVal = Object.values(colorVar.valuesByMode)[0] as Color
    expect(colorVal.r).toBeCloseTo(0.23, 1)

    const floatVar = vars.find((v) => v.name === 'spacing/base')
    expect(floatVar).toBeDefined()
    expect(expectDefined(floatVar, 'floatVar').type).toBe('FLOAT')
    expect(Object.values(floatVar.valuesByMode)[0]).toBe(8)

    const boolVar = vars.find((v) => v.name === 'visible')
    expect(boolVar).toBeDefined()
    expect(expectDefined(boolVar, 'boolVar').type).toBe('BOOLEAN')
    expect(Object.values(boolVar.valuesByMode)[0]).toBe(true)

    const strVar = vars.find((v) => v.name === 'label')
    expect(strVar).toBeDefined()
    expect(expectDefined(strVar, 'strVar').type).toBe('STRING')
    expect(Object.values(strVar.valuesByMode)[0]).toBe('Hello')
  })

  test('variable bindings survive export → re-import', async () => {
    await initCodec()

    const graph = new SceneGraph()
    const col = graph.createCollection('Tokens')
    const floatVar = graph.createVariable('radius', 'FLOAT', col.id, 12)
    const fillVar = graph.createVariable('surface', 'COLOR', col.id, { r: 1, g: 1, b: 1, a: 1 })
    const strokeVar = graph.createVariable('border', 'COLOR', col.id, {
      r: 0.1,
      g: 0.2,
      b: 0.3,
      a: 1
    })

    const page = graph.getPages()[0]
    const rect = graph.createNode('RECTANGLE', page.id, {
      name: 'Bound Rect',
      width: 100,
      height: 100,
      cornerRadius: 12,
      fills: [
        {
          type: 'SOLID',
          color: { r: 1, g: 1, b: 1, a: 1 },
          opacity: 1,
          visible: true,
          blendMode: 'NORMAL'
        }
      ],
      strokes: [
        {
          color: { r: 0.1, g: 0.2, b: 0.3, a: 1 },
          weight: 1,
          opacity: 1,
          visible: true,
          align: 'INSIDE',
          cap: 'NONE',
          join: 'MITER',
          dashPattern: []
        }
      ]
    })
    graph.bindVariable(rect.id, 'cornerRadius', floatVar.id)
    graph.bindVariable(rect.id, 'fills/0/color', fillVar.id)
    graph.bindVariable(rect.id, 'strokes/0/color', strokeVar.id)

    const exported = await exportFigFile(graph)
    const reimported = await parseFigFile(exported.buffer as ArrayBuffer)

    const reimportedRect = [...reimported.getAllNodes()].find((n) => n.name === 'Bound Rect')
    expect(reimportedRect).toBeDefined()
    expect(Object.keys(expectDefined(reimportedRect, 'reimportedRect').boundVariables)).toContain(
      'cornerRadius'
    )
    expect(Object.keys(reimportedRect.boundVariables)).toContain('fills/0/color')
    expect(Object.keys(reimportedRect.boundVariables)).toContain('strokes/0/color')
  })

  test.if(runsHeavyTests)(
    'material3.fig variables survive round-trip',
    async () => {
      const original = await parseFixture('material3.fig')

      const exported = await exportFigFile(original)
      const reimported = await parseFigFile(exported.buffer as ArrayBuffer)

      expect(reimported.variables.size).toBe(original.variables.size)
      expect(reimported.variableCollections.size).toBeGreaterThanOrEqual(
        [...original.variableCollections.values()].filter((c) => c.variableIds.length > 0).length
      )
    },
    120_000
  )

  test('pluginID casing is consistent across full codec pipeline', async () => {
    await initCodec()

    // Create a graph with multiple nodes having pluginData entries
    const graph = new SceneGraph()
    const api = new FigmaAPI(graph)
    const frame = api.createFrame()
    frame.name = 'PluginID Test'
    frame.setPluginData('key1', 'value1')
    frame.setPluginData('key2', 'value2')

    const rect = api.createRectangle()
    rect.name = 'Plugin Rect'
    rect.setPluginData('testKey', 'testValue')

    // Round-trip through the codec
    const exported = await exportFigFile(graph)
    const reimported = await parseFigFile(exported.buffer as ArrayBuffer)

    // Find all nodes with pluginData
    let nodesWithPluginData = 0
    let totalEntries = 0
    for (const node of reimported.getAllNodes()) {
      if (node.pluginData && node.pluginData.length > 0) {
        nodesWithPluginData++
        for (const entry of node.pluginData) {
          totalEntries++
          // Every entry MUST use pluginId (lowercase d, matching SceneGraph type)
          expect(entry).toHaveProperty('pluginId')
          expect(typeof entry.pluginId).toBe('string')
          expect(entry.pluginId.length).toBeGreaterThan(0)
        }
      }
      // Also check pluginRelaunchData entries
      if (node.pluginRelaunchData && node.pluginRelaunchData.length > 0) {
        for (const entry of node.pluginRelaunchData) {
          expect(entry).toHaveProperty('pluginId')
          expect(typeof entry.pluginId).toBe('string')
        }
      }
    }

    // Should have at least 2 nodes with plugin data (frame + rect)
    expect(nodesWithPluginData).toBeGreaterThanOrEqual(2)
    expect(totalEntries).toBeGreaterThanOrEqual(3)
  })
})
