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

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

import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert'
import { getTool, setupToolTest, type ToolResult } from '#tests/helpers/tools'

describe('set_fill', () => {
  test('sets solid fill', () => {
    const { figma } = setupToolTest()
    const frame = figma.createFrame()
    frame.resize(100, 100)

    const tool = getTool('set_fill')
    tool.execute(figma, { id: frame.id, color: '#ff0000' })

    const fills = expectDefined(figma.getNodeById(frame.id), 'frame node').fills
    expect(fills.length).toBe(1)
    expect(fills[0].color.r).toBeCloseTo(1)
    expect(fills[0].color.g).toBeCloseTo(0)
    expect(fills[0].color.b).toBeCloseTo(0)
  })

  test('returns error for missing node', () => {
    const { figma } = setupToolTest()
    const tool = getTool('set_fill')
    const result = tool.execute(figma, { id: 'nonexistent', color: '#ff0000' }) as ToolResult
    expect(result.error).toContain('not found')
  })
})

describe('set_stroke', () => {
  test('sets stroke', () => {
    const { figma } = setupToolTest()
    const rect = figma.createRectangle()
    rect.resize(100, 100)

    const tool = getTool('set_stroke')
    tool.execute(figma, { id: rect.id, color: '#0000ff', weight: 2 })

    const strokes = expectDefined(figma.getNodeById(rect.id), 'rectangle node').strokes
    expect(strokes.length).toBe(1)
    expect(strokes[0].color.b).toBeCloseTo(1)
    expect(strokes[0].weight).toBe(2)
  })
})

describe('set_effects', () => {
  test('adds drop shadow', () => {
    const { figma } = setupToolTest()
    const frame = figma.createFrame()
    frame.resize(100, 100)

    const tool = getTool('set_effects')
    tool.execute(figma, {
      id: frame.id,
      type: 'DROP_SHADOW',
      color: '#000000',
      offset_x: 0,
      offset_y: 4,
      radius: 8,
      spread: 0
    })

    const effects = expectDefined(figma.getNodeById(frame.id), 'frame node').effects
    expect(effects.length).toBe(1)
    expect(effects[0].type).toBe('DROP_SHADOW')
  })

  test('adds blur without color', () => {
    const { figma } = setupToolTest()
    const frame = figma.createFrame()
    frame.resize(100, 100)

    const tool = getTool('set_effects')
    tool.execute(figma, { id: frame.id, type: 'BACKGROUND_BLUR', radius: 10 })

    const effects = expectDefined(figma.getNodeById(frame.id), 'frame node').effects
    expect(effects.length).toBe(1)
    expect(effects[0].type).toBe('BACKGROUND_BLUR')
  })
})

describe('update_node', () => {
  test('updates position and size', () => {
    const { figma } = setupToolTest()
    const rect = figma.createRectangle()
    rect.resize(100, 100)

    const tool = getTool('update_node')
    const result = tool.execute(figma, {
      id: rect.id,
      x: 50,
      y: 75,
      width: 200,
      height: 150,
      opacity: 0.5
    }) as ToolResult

    expect(result.updated).toContain('x')
    expect(result.updated).toContain('size')
    expect(result.updated).toContain('opacity')

    const node = expectDefined(figma.getNodeById(rect.id), 'rectangle node')
    expect(node.x).toBe(50)
    expect(node.y).toBe(75)
    expect(node.width).toBe(200)
    expect(node.height).toBe(150)
    expect(node.opacity).toBe(0.5)
  })

  test('updates corner radius', () => {
    const { figma } = setupToolTest()
    const rect = figma.createRectangle()
    rect.resize(100, 100)

    const tool = getTool('update_node')
    tool.execute(figma, { id: rect.id, corner_radius: 12 })

    expect(expectDefined(figma.getNodeById(rect.id), 'rectangle node').cornerRadius).toBe(12)
  })
})

describe('set_layout', () => {
  test('sets auto-layout', () => {
    const { figma } = setupToolTest()
    const frame = figma.createFrame()
    frame.resize(300, 200)

    const tool = getTool('set_layout')
    tool.execute(figma, {
      id: frame.id,
      direction: 'VERTICAL',
      spacing: 16,
      padding: 20
    })

    const node = expectDefined(figma.getNodeById(frame.id), 'frame node')
    expect(node.layoutMode).toBe('VERTICAL')
    expect(node.itemSpacing).toBe(16)
    expect(node.paddingLeft).toBe(20)
    expect(node.paddingTop).toBe(20)
  })

  test('defaults to HUG sizing when enabling auto-layout', () => {
    const { graph, figma } = setupToolTest()
    const frame = figma.createFrame()
    frame.resize(300, 200)

    const rawBefore = getNodeOrThrow(graph, frame.id)
    expect(rawBefore.layoutMode).toBe('NONE')
    expect(rawBefore.primaryAxisSizing).toBe('FIXED')
    expect(rawBefore.counterAxisSizing).toBe('FIXED')

    const tool = getTool('set_layout')
    tool.execute(figma, { id: frame.id, direction: 'VERTICAL' })

    const rawAfter = getNodeOrThrow(graph, frame.id)
    expect(rawAfter.layoutMode).toBe('VERTICAL')
    expect(rawAfter.primaryAxisSizing).toBe('HUG')
    expect(rawAfter.counterAxisSizing).toBe('HUG')
  })

  test('preserves sizing modes when updating existing auto-layout', () => {
    const { graph, figma } = setupToolTest()
    const frame = figma.createFrame()
    frame.resize(300, 200)

    const tool = getTool('set_layout')
    tool.execute(figma, { id: frame.id, direction: 'HORIZONTAL' })

    // Manually set primary axis back to FIXED (user wants fixed width)
    graph.updateNode(frame.id, { primaryAxisSizing: 'FIXED' })
    expect(getNodeOrThrow(graph, frame.id).primaryAxisSizing).toBe('FIXED')

    // Updating spacing should NOT reset sizing modes
    tool.execute(figma, { id: frame.id, spacing: 24 })

    const raw = getNodeOrThrow(graph, frame.id)
    expect(raw.primaryAxisSizing).toBe('FIXED')
    expect(raw.counterAxisSizing).toBe('HUG')
  })

  test('HUG default enables hug-to-fit with 5 children', () => {
    const { graph, figma } = setupToolTest()
    const frame = figma.createFrame()
    frame.resize(320, 100)

    const tool = getTool('set_layout')
    tool.execute(figma, { id: frame.id, direction: 'VERTICAL', spacing: 8, padding: 16 })

    for (let i = 0; i < 5; i++) {
      const child = figma.createRectangle()
      child.resize(280, 50)
      frame.appendChild(child)
    }

    computeAllLayouts(graph)

    const node = getNodeOrThrow(graph, frame.id)
    expect(node.primaryAxisSizing).toBe('HUG')
    // 16 (top pad) + 5*50 (children) + 4*8 (gaps) + 16 (bottom pad) = 314
    expect(node.height).toBe(314)
  })
})

describe('set_constraints', () => {
  test('sets constraints', () => {
    const { figma } = setupToolTest()
    const rect = figma.createRectangle()
    rect.resize(100, 100)

    const tool = getTool('set_constraints')
    tool.execute(figma, { id: rect.id, horizontal: 'CENTER', vertical: 'STRETCH' })

    const node = expectDefined(figma.getNodeById(rect.id), 'rectangle node')
    expect(node.constraints.horizontal).toBe('CENTER')
    expect(node.constraints.vertical).toBe('STRETCH')
  })
})

describe('set_font_range', () => {
  test('applies font style to text range and survives serialization', () => {
    const { figma, graph } = setupToolTest()
    const createText = getTool('create_shape')
    const setText = getTool('set_text')
    const setFontRange = getTool('set_font_range')

    const created = createText.execute(figma, {
      type: 'TEXT',
      x: 0,
      y: 0,
      width: 200,
      height: 20
    }) as ToolResult
    setText.execute(figma, { id: created.id, text: 'Hello World' })
    setFontRange.execute(figma, {
      id: created.id,
      start: 0,
      end: 5,
      family: 'Inter',
      size: 18,
      style: 'Bold'
    })

    const node = getNodeOrThrow(graph, created.id)
    expect(node.styleRuns.length).toBeGreaterThan(0)
    for (const run of node.styleRuns) {
      expect(run.style).toBeDefined()
      expect(typeof run.start).toBe('number')
      expect(typeof run.length).toBe('number')
    }
    const boldRun = node.styleRuns.find((r) => r.style.fontWeight === 700)
    expect(boldRun).toBeDefined()
    expect(expectDefined(boldRun, 'bold style run').start).toBe(0)
    expect(expectDefined(boldRun, 'bold style run').length).toBe(5)
  })

  test('applies color to text range', () => {
    const { figma, graph } = setupToolTest()
    const createText = getTool('create_shape')
    const setText = getTool('set_text')
    const setFontRange = getTool('set_font_range')

    const created = createText.execute(figma, {
      type: 'TEXT',
      x: 0,
      y: 0,
      width: 200,
      height: 20
    }) as ToolResult
    setText.execute(figma, { id: created.id, text: 'Red text' })
    setFontRange.execute(figma, { id: created.id, start: 0, end: 3, color: '#ff0000' })

    const node = getNodeOrThrow(graph, created.id)
    const colorRun = node.styleRuns.find((r) => r.style.fills?.length)
    expect(colorRun).toBeDefined()
    expect(
      expectDefined(expectDefined(colorRun, 'color style run').style.fills, 'color style fills')[0]
        ?.color.r
    ).toBeCloseTo(1)
  })
})
