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

import { SceneGraph } from '@open-pencil/core'

function pageId(graph: SceneGraph) {
  return graph.getPages()[0].id
}

function rect(
  graph: SceneGraph,
  parentId: string,
  props: { name?: string; x?: number; y?: number; width?: number; height?: number } = {}
) {
  return graph.createNode('RECTANGLE', parentId, {
    name: props.name ?? 'Rect',
    x: props.x ?? 0,
    y: props.y ?? 0,
    width: props.width ?? 50,
    height: props.height ?? 50
  })
}

describe('flip', () => {
  test('flipX defaults to false', () => {
    const graph = new SceneGraph()
    const node = rect(graph, pageId(graph))
    expect(node.flipX).toBe(false)
    expect(node.flipY).toBe(false)
  })

  test('flipX can be set via updateNode', () => {
    const graph = new SceneGraph()
    const node = rect(graph, pageId(graph))
    graph.updateNode(node.id, { flipX: true })
    expect(graph.getNode(node.id)!.flipX).toBe(true)
    expect(graph.getNode(node.id)!.flipY).toBe(false)
  })

  test('flipY can be set via updateNode', () => {
    const graph = new SceneGraph()
    const node = rect(graph, pageId(graph))
    graph.updateNode(node.id, { flipY: true })
    expect(graph.getNode(node.id)!.flipX).toBe(false)
    expect(graph.getNode(node.id)!.flipY).toBe(true)
  })

  test('flip toggles', () => {
    const graph = new SceneGraph()
    const node = rect(graph, pageId(graph))
    graph.updateNode(node.id, { flipX: true })
    expect(graph.getNode(node.id)!.flipX).toBe(true)
    graph.updateNode(node.id, { flipX: false })
    expect(graph.getNode(node.id)!.flipX).toBe(false)
  })
})

describe('single-node alignment to parent', () => {
  function setup() {
    const graph = new SceneGraph()
    const frame = graph.createNode('FRAME', pageId(graph), {
      name: 'Container',
      x: 0,
      y: 0,
      width: 400,
      height: 300
    })
    const child = rect(graph, frame.id, { name: 'Child', x: 50, y: 50, width: 100, height: 80 })
    return { graph, frame, child }
  }

  test('align left within parent', () => {
    const { graph, frame, child } = setup()
    graph.updateNode(child.id, { x: 0 })
    expect(graph.getNode(child.id)!.x).toBe(0)
  })

  test('align right within parent', () => {
    const { graph, frame, child } = setup()
    const targetX = frame.width - child.width
    graph.updateNode(child.id, { x: targetX })
    expect(graph.getNode(child.id)!.x).toBe(300)
  })

  test('align center horizontal within parent', () => {
    const { graph, frame, child } = setup()
    const targetX = (frame.width - child.width) / 2
    graph.updateNode(child.id, { x: targetX })
    expect(graph.getNode(child.id)!.x).toBe(150)
  })

  test('align top within parent', () => {
    const { graph, child } = setup()
    graph.updateNode(child.id, { y: 0 })
    expect(graph.getNode(child.id)!.y).toBe(0)
  })

  test('align bottom within parent', () => {
    const { graph, frame, child } = setup()
    const targetY = frame.height - child.height
    graph.updateNode(child.id, { y: targetY })
    expect(graph.getNode(child.id)!.y).toBe(220)
  })

  test('align center vertical within parent', () => {
    const { graph, frame, child } = setup()
    const targetY = (frame.height - child.height) / 2
    graph.updateNode(child.id, { y: targetY })
    expect(graph.getNode(child.id)!.y).toBe(110)
  })
})

describe('multi-node alignment', () => {
  function setup() {
    const graph = new SceneGraph()
    const page = pageId(graph)
    const a = rect(graph, page, { name: 'A', x: 10, y: 20, width: 50, height: 30 })
    const b = rect(graph, page, { name: 'B', x: 100, y: 80, width: 60, height: 40 })
    const c = rect(graph, page, { name: 'C', x: 200, y: 50, width: 40, height: 50 })
    return { graph, a, b, c }
  }

  test('align left — all nodes move to min x', () => {
    const { graph, a, b, c } = setup()
    const nodes = [a, b, c].map((n) => graph.getNode(n.id)!)
    const abs = nodes.map((n) => graph.getAbsolutePosition(n.id))
    const minX = Math.min(...abs.map((p, i) => p.x))

    for (const n of nodes) {
      const nodeAbs = graph.getAbsolutePosition(n.id)
      graph.updateNode(n.id, { x: n.x + (minX - nodeAbs.x) })
    }

    expect(graph.getNode(a.id)!.x).toBe(10)
    expect(graph.getNode(b.id)!.x).toBe(10)
    expect(graph.getNode(c.id)!.x).toBe(10)
  })

  test('align right — all nodes align to max right edge', () => {
    const { graph, a, b, c } = setup()
    const nodes = [a, b, c].map((n) => graph.getNode(n.id)!)
    const abs = nodes.map((n) => graph.getAbsolutePosition(n.id))
    const maxX = Math.max(...abs.map((p, i) => p.x + nodes[i].width))

    for (const n of nodes) {
      const nodeAbs = graph.getAbsolutePosition(n.id)
      const targetX = maxX - n.width
      graph.updateNode(n.id, { x: n.x + (targetX - nodeAbs.x) })
    }

    expect(graph.getNode(a.id)!.x).toBe(190)
    expect(graph.getNode(b.id)!.x).toBe(180)
    expect(graph.getNode(c.id)!.x).toBe(200)
  })

  test('align top — all nodes move to min y', () => {
    const { graph, a, b, c } = setup()
    const nodes = [a, b, c].map((n) => graph.getNode(n.id)!)
    const abs = nodes.map((n) => graph.getAbsolutePosition(n.id))
    const minY = Math.min(...abs.map((p) => p.y))

    for (const n of nodes) {
      const nodeAbs = graph.getAbsolutePosition(n.id)
      graph.updateNode(n.id, { y: n.y + (minY - nodeAbs.y) })
    }

    expect(graph.getNode(a.id)!.y).toBe(20)
    expect(graph.getNode(b.id)!.y).toBe(20)
    expect(graph.getNode(c.id)!.y).toBe(20)
  })
})

describe('multi-node property merging', () => {
  test('same values merge to single value', () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    const a = rect(graph, page, { width: 100, height: 50 })
    const b = rect(graph, page, { width: 100, height: 50 })
    const nodes = [graph.getNode(a.id)!, graph.getNode(b.id)!]

    const widths = new Set(nodes.map((n) => n.width))
    expect(widths.size).toBe(1)
    expect([...widths][0]).toBe(100)
  })

  test('different values are detected', () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    const a = rect(graph, page, { width: 100 })
    const b = rect(graph, page, { width: 200 })
    const nodes = [graph.getNode(a.id)!, graph.getNode(b.id)!]

    const widths = new Set(nodes.map((n) => n.width))
    expect(widths.size).toBe(2)
  })

  test('update applies to all selected nodes', () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    const a = rect(graph, page, { width: 100 })
    const b = rect(graph, page, { width: 200 })
    const c = rect(graph, page, { width: 150 })

    for (const id of [a.id, b.id, c.id]) {
      graph.updateNode(id, { width: 300 })
    }

    expect(graph.getNode(a.id)!.width).toBe(300)
    expect(graph.getNode(b.id)!.width).toBe(300)
    expect(graph.getNode(c.id)!.width).toBe(300)
  })

  test('opacity update applies uniformly', () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    const a = rect(graph, page)
    const b = rect(graph, page)

    graph.updateNode(a.id, { opacity: 0.5 })
    graph.updateNode(b.id, { opacity: 0.8 })

    for (const id of [a.id, b.id]) {
      graph.updateNode(id, { opacity: 0.75 })
    }

    expect(graph.getNode(a.id)!.opacity).toBe(0.75)
    expect(graph.getNode(b.id)!.opacity).toBe(0.75)
  })

  test('fills comparison detects mixed', () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    const a = rect(graph, page)
    const b = rect(graph, page)

    graph.updateNode(a.id, {
      fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
    })
    graph.updateNode(b.id, {
      fills: [{ type: 'SOLID', color: { r: 0, g: 1, b: 0, a: 1 }, opacity: 1, visible: true }]
    })

    const nodeA = graph.getNode(a.id)!
    const nodeB = graph.getNode(b.id)!
    expect(JSON.stringify(nodeA.fills)).not.toBe(JSON.stringify(nodeB.fills))
  })

  test('fills comparison detects same', () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    const fill = { type: 'SOLID' as const, color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }
    const a = rect(graph, page)
    const b = rect(graph, page)

    graph.updateNode(a.id, { fills: [{ ...fill }] })
    graph.updateNode(b.id, { fills: [{ ...fill }] })

    const nodeA = graph.getNode(a.id)!
    const nodeB = graph.getNode(b.id)!
    expect(JSON.stringify(nodeA.fills)).toBe(JSON.stringify(nodeB.fills))
  })
})

describe('flip roundtrip via kiwi', () => {
  async function roundtrip(graph: SceneGraph) {
    const { initCodec } = await import('../../packages/core/src/kiwi/codec')
    const { parseFigFile } = await import('../../packages/core/src/kiwi/fig-file')
    const { exportFigFile } = await import('../../packages/core/src/io/formats/fig/export')
    await initCodec()
    const buf = await exportFigFile(graph)
    return parseFigFile(buf)
  }

  function findChild(graph: SceneGraph, name: string) {
    const pages = graph.getPages()
    const children = graph.getChildren(pages[0].id)
    return children.find((c) => c.name === name)
  }

  test('flipX preserved through export/import', async () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    rect(graph, page, { name: 'Flipped', x: 50, y: 50, width: 100, height: 80 })
    graph.updateNode(graph.getChildren(page).find((n) => n.name === 'Flipped')!.id, { flipX: true })

    const imported = await roundtrip(graph)
    const found = findChild(imported, 'Flipped')
    expect(found).toBeTruthy()
    expect(found!.flipX).toBe(true)
  })

  test('non-flipped node stays non-flipped', async () => {
    const graph = new SceneGraph()
    rect(graph, pageId(graph), { name: 'Normal', x: 100, y: 50, width: 200, height: 100 })

    const imported = await roundtrip(graph)
    const found = findChild(imported, 'Normal')
    expect(found).toBeTruthy()
    expect(found!.flipX).toBe(false)
  })

  test('flipX with rotation preserved', async () => {
    const graph = new SceneGraph()
    const page = pageId(graph)
    rect(graph, page, { name: 'RotFlip', x: 0, y: 0, width: 100, height: 100 })
    graph.updateNode(graph.getChildren(page).find((n) => n.name === 'RotFlip')!.id, {
      flipX: true,
      rotation: 45
    })

    const imported = await roundtrip(graph)
    const found = findChild(imported, 'RotFlip')
    expect(found).toBeTruthy()
    expect(found!.flipX).toBe(true)
    expect(Math.round(found!.rotation)).toBe(45)
  })
})
