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

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

import { expectDefined } from '#tests/helpers/assert'
import { collectAllNodes, countByType } from '#tests/helpers/fig-traversal'

setDefaultTimeout(60_000)

describe('roundtrip: export → re-import', () => {
  let reImported: SceneGraph
  let reImportedNodes: SceneNode[]

  beforeAll(async () => {
    await initCodec()

    const graph = new SceneGraph()
    const page1 = graph.getPages()[0]
    const page2 = graph.addPage('Second Page')
    const internalPage = graph.addPage('Internal Only Canvas')
    internalPage.internalOnly = true
    graph.createNode('RECTANGLE', internalPage.id, {
      name: 'Internal Rect',
      width: 50,
      height: 50
    })

    graph.createNode('FRAME', page1.id, {
      name: 'Container',
      x: 0,
      y: 0,
      width: 400,
      height: 300,
      layoutMode: 'VERTICAL',
      itemSpacing: 16,
      paddingTop: 24,
      paddingRight: 24,
      paddingBottom: 24,
      paddingLeft: 24,
      cornerRadius: 12,
      fills: [
        {
          type: 'SOLID',
          color: { r: 1, g: 1, b: 1, a: 1 },
          opacity: 1,
          visible: true,
          blendMode: 'NORMAL'
        }
      ]
    })

    const container = graph.getChildren(page1.id)[0]

    graph.createNode('RECTANGLE', container.id, {
      name: 'Header BG',
      x: 0,
      y: 0,
      width: 400,
      height: 80,
      cornerRadius: 8,
      topLeftRadius: 8,
      topRightRadius: 8,
      bottomRightRadius: 0,
      bottomLeftRadius: 0,
      independentCorners: true,
      fills: [
        {
          type: 'SOLID',
          color: { r: 0.2, g: 0.4, b: 0.8, a: 1 },
          opacity: 1,
          visible: true,
          blendMode: 'NORMAL'
        },
        {
          type: 'GRADIENT_LINEAR',
          color: { r: 0, g: 0, b: 0, a: 0 },
          opacity: 0.5,
          visible: true,
          blendMode: 'NORMAL',
          gradientStops: [
            { color: { r: 1, g: 1, b: 1, a: 1 }, position: 0 },
            { color: { r: 0, g: 0, b: 0, a: 0 }, position: 1 }
          ],
          gradientTransform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
        }
      ],
      effects: [
        {
          type: 'DROP_SHADOW',
          color: { r: 0, g: 0, b: 0, a: 0.25 },
          offset: { x: 0, y: 4 },
          radius: 8,
          spread: 0,
          visible: true,
          blendMode: 'NORMAL'
        }
      ]
    })

    graph.createNode('TEXT', container.id, {
      name: 'Title',
      x: 24,
      y: 100,
      width: 352,
      height: 24,
      text: 'Hello World',
      fontSize: 18,
      fontFamily: 'Inter',
      fontWeight: 700,
      textAlignHorizontal: 'CENTER'
    })

    graph.createNode('ELLIPSE', container.id, {
      name: 'Circle',
      x: 176,
      y: 140,
      width: 48,
      height: 48,
      fills: [
        {
          type: 'SOLID',
          color: { r: 0.9, g: 0.1, b: 0.3, a: 1 },
          opacity: 1,
          visible: true,
          blendMode: 'NORMAL'
        }
      ]
    })

    graph.createNode('RECTANGLE', page2.id, {
      name: 'Page2 Rect',
      x: 50,
      y: 50,
      width: 100,
      height: 100
    })

    const figBytes = await exportFigFile(graph)
    reImported = await parseFigFile(figBytes.buffer as ArrayBuffer)
    reImportedNodes = collectAllNodes(reImported)
  })

  test('preserves page count', () => {
    expect(reImported.getPages().length).toBe(2)
  })

  test('preserves page names', () => {
    const names = reImported.getPages().map((p) => p.name)
    expect(names).toContain('Page 1')
    expect(names).toContain('Second Page')
  })

  test('preserves internal pages', () => {
    const allPages = reImported.getPages(true)
    const publicPages = reImported.getPages(false)
    expect(allPages.length).toBe(3)
    expect(publicPages.length).toBe(2)
    const internal = allPages.find((p) => p.internalOnly)
    expect(internal).toBeDefined()
    expect(internal?.name).toBe('Internal Only Canvas')
    expect(reImported.getChildren(internal?.id ?? '').length).toBe(1)
  })

  test('preserves node count', () => {
    // 1 frame + 1 rect + 1 text + 1 ellipse + 1 rect on page 2 = 5
    expect(reImportedNodes.length).toBe(5)
  })

  test('preserves node types', () => {
    const types = countByType(reImportedNodes)
    expect(types.get('FRAME')).toBe(1)
    expect(types.get('RECTANGLE')).toBe(2)
    expect(types.get('TEXT')).toBe(1)
    expect(types.get('ELLIPSE')).toBe(1)
  })

  test('preserves component sets', async () => {
    const graph = new SceneGraph()
    const page = graph.getPages()[0]
    const componentSet = graph.createNode('COMPONENT_SET', page.id, {
      name: 'Button',
      width: 240,
      height: 80
    })
    graph.createNode('COMPONENT', componentSet.id, {
      name: 'Primary',
      x: 16,
      y: 16,
      width: 96,
      height: 40
    })

    const figBytes = await exportFigFile(graph)
    const parsed = await parseFigFile(figBytes.buffer as ArrayBuffer)
    const parsedSet = parsed.getAllNodes().find((node) => node.name === 'Button')

    expect(parsedSet).toBeDefined()
    expect(expectDefined(parsedSet, 'parsedSet').type).toBe('COMPONENT_SET')
  })

  test('preserves node names', () => {
    const names = new Set(reImportedNodes.map((n) => n.name))
    expect(names.has('Container')).toBe(true)
    expect(names.has('Header BG')).toBe(true)
    expect(names.has('Title')).toBe(true)
    expect(names.has('Circle')).toBe(true)
    expect(names.has('Page2 Rect')).toBe(true)
  })

  test('preserves fills', () => {
    const headerBg = reImportedNodes.find((n) => n.name === 'Header BG')
    expect(headerBg).toBeDefined()
    expect(expectDefined(headerBg, 'headerBg').fills).toHaveLength(2)
    expect(headerBg.fills[0].type).toBe('SOLID')
    expect(headerBg.fills[0].color.r).toBeCloseTo(0.2, 1)
    expect(headerBg.fills[1].type).toBe('GRADIENT_LINEAR')
    expect(headerBg.fills[1].opacity).toBeCloseTo(0.5, 1)
    expect(headerBg.fills[1].gradientStops).toHaveLength(2)
  })

  test('preserves text content', () => {
    const title = reImportedNodes.find((n) => n.name === 'Title')
    expect(title).toBeDefined()
    expect(expectDefined(title, 'title').text).toBe('Hello World')
  })

  test('preserves text properties', () => {
    const title = reImportedNodes.find((n) => n.name === 'Title')
    expect(title).toBeDefined()
    expect(expectDefined(title, 'title').fontSize).toBe(18)
    expect(title.fontFamily).toBe('Inter')
    expect(title.fontWeight).toBe(700)
    expect(title.textAlignHorizontal).toBe('CENTER')
  })

  test('preserves layout mode', () => {
    const container = reImportedNodes.find((n) => n.name === 'Container')
    expect(container).toBeDefined()
    expect(expectDefined(container, 'container').layoutMode).toBe('VERTICAL')
  })

  test('preserves layout spacing', () => {
    const container = reImportedNodes.find((n) => n.name === 'Container')
    expect(container).toBeDefined()
    expect(expectDefined(container, 'container').itemSpacing).toBe(16)
    expect(container.paddingTop).toBe(24)
    expect(container.paddingRight).toBe(24)
    expect(container.paddingBottom).toBe(24)
    expect(container.paddingLeft).toBe(24)
  })

  test('preserves corner radius', () => {
    const container = reImportedNodes.find((n) => n.name === 'Container')
    expect(container).toBeDefined()
    expect(expectDefined(container, 'container').cornerRadius).toBe(12)
  })

  test('preserves independent corner radii', () => {
    const headerBg = reImportedNodes.find((n) => n.name === 'Header BG')
    expect(headerBg).toBeDefined()
    expect(expectDefined(headerBg, 'headerBg').independentCorners).toBe(true)
    expect(headerBg.topLeftRadius).toBe(8)
    expect(headerBg.topRightRadius).toBe(8)
    expect(headerBg.bottomRightRadius).toBe(0)
    expect(headerBg.bottomLeftRadius).toBe(0)
  })

  test('preserves effects', () => {
    const headerBg = reImportedNodes.find((n) => n.name === 'Header BG')
    expect(headerBg).toBeDefined()
    expect(expectDefined(headerBg, 'headerBg').effects).toHaveLength(1)
    expect(headerBg.effects[0].type).toBe('DROP_SHADOW')
    expect(headerBg.effects[0].radius).toBe(8)
    expect(headerBg.effects[0].offset.y).toBe(4)
  })

  test('preserves dimensions', () => {
    const container = reImportedNodes.find((n) => n.name === 'Container')
    expect(container).toBeDefined()
    expect(expectDefined(container, 'container').width).toBe(400)
    expect(container.height).toBe(300)
  })
})
