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

import { FigmaAPI, SceneGraph, matchByXPath, nodeToXPath, queryByXPath } from '@open-pencil/core'

function setup() {
  const graph = new SceneGraph()
  const figma = new FigmaAPI(graph)
  return { graph, figma }
}

describe('queryByXPath', () => {
  test('finds nodes by type', async () => {
    const { graph, figma } = setup()
    figma.createRectangle()
    figma.createRectangle()
    figma.createText()

    const results = await queryByXPath(graph, '//RECTANGLE')
    expect(results.length).toBe(2)
    expect(results.every((n) => n.type === 'RECTANGLE')).toBe(true)
  })

  test('filters by attribute comparison', async () => {
    const { graph, figma } = setup()
    const small = figma.createRectangle()
    small.resize(50, 50)
    small.name = 'Small'

    const big = figma.createRectangle()
    big.resize(300, 300)
    big.name = 'Big'

    const results = await queryByXPath(graph, '//RECTANGLE[@width < 200]')
    expect(results.length).toBe(1)
    expect(results[0].name).toBe('Small')
  })

  test('handles descendant axis //FRAME//TEXT', async () => {
    const { graph, figma } = setup()
    const frame = figma.createFrame()
    frame.resize(400, 400)
    frame.name = 'Container'

    const text = figma.createText()
    text.name = 'Inside'
    frame.appendChild(text)

    const outsideText = figma.createText()
    outsideText.name = 'Outside'

    const results = await queryByXPath(graph, '//FRAME//TEXT')
    expect(results.length).toBe(1)
    expect(results[0].name).toBe('Inside')
  })

  test('respects limit option', async () => {
    const { graph, figma } = setup()
    for (let i = 0; i < 10; i++) {
      const r = figma.createRectangle()
      r.name = `Rect ${i}`
    }

    const results = await queryByXPath(graph, '//RECTANGLE', { limit: 3 })
    expect(results.length).toBe(3)
  })

  test('filters by page option', async () => {
    const { graph, figma } = setup()
    const rect1 = figma.createRectangle()
    rect1.name = 'Page1Rect'
    const page1Name = figma.currentPage.name

    const page2 = figma.createPage()
    page2.name = 'Page 2'
    figma.currentPage = page2
    const rect2 = figma.createRectangle()
    rect2.name = 'Page2Rect'

    const page1Results = await queryByXPath(graph, '//RECTANGLE', { page: page1Name })
    expect(page1Results.length).toBe(1)
    expect(page1Results[0].name).toBe('Page1Rect')

    const page2Results = await queryByXPath(graph, '//RECTANGLE', { page: 'Page 2' })
    expect(page2Results.length).toBe(1)
    expect(page2Results[0].name).toBe('Page2Rect')
  })

  test('returns empty for no matches', async () => {
    const { graph, figma } = setup()
    figma.createRectangle()

    const results = await queryByXPath(graph, '//ELLIPSE')
    expect(results.length).toBe(0)
  })

  test('handles invalid selector gracefully (throws)', async () => {
    const { graph } = setup()
    expect(queryByXPath(graph, '///invalid[[[[')).rejects.toThrow()
  })
})

describe('nodeToXPath', () => {
  test('returns null for missing node', () => {
    const { graph } = setup()
    expect(nodeToXPath(graph, 'nonexistent')).toBeNull()
  })

  test('uses name predicate for unique name among siblings', () => {
    const { graph, figma } = setup()
    const rect = figma.createRectangle()
    rect.name = 'Header'
    const text = figma.createText()
    text.name = 'Title'

    const xpath = nodeToXPath(graph, rect.id)
    expect(xpath).toBe("//RECTANGLE[@name='Header']")
  })

  test('includes name even when sole child of that type', () => {
    const { graph, figma } = setup()
    const frame = figma.createFrame()
    frame.name = 'Card'
    const text = figma.createText()
    text.name = 'Label'
    frame.appendChild(text)

    const xpath = nodeToXPath(graph, text.id)
    expect(xpath).toBe("//FRAME[@name='Card']/TEXT[@name='Label']")
  })

  test('uses positional predicate for duplicate names', () => {
    const { graph, figma } = setup()
    const r1 = figma.createRectangle()
    r1.name = 'Item'
    const r2 = figma.createRectangle()
    r2.name = 'Item'

    const xpath1 = nodeToXPath(graph, r1.id)
    const xpath2 = nodeToXPath(graph, r2.id)
    expect(xpath1).toBe('//RECTANGLE[1]')
    expect(xpath2).toBe('//RECTANGLE[2]')
  })

  test('builds multi-level path', () => {
    const { graph, figma } = setup()
    const frame = figma.createFrame()
    frame.name = 'Container'
    const inner = figma.createFrame()
    inner.name = 'Row'
    frame.appendChild(inner)
    const text = figma.createText()
    text.name = 'Label'
    inner.appendChild(text)

    const xpath = nodeToXPath(graph, text.id)
    expect(xpath).toBe("//FRAME[@name='Container']/FRAME[@name='Row']/TEXT[@name='Label']")
  })

  test('round-trips with queryByXPath', async () => {
    const { graph, figma } = setup()
    const frame = figma.createFrame()
    frame.name = 'Sidebar'
    const btn = figma.createRectangle()
    btn.name = 'Button'
    frame.appendChild(btn)

    const xpath = nodeToXPath(graph, btn.id)!
    const results = await queryByXPath(graph, xpath)
    expect(results.length).toBe(1)
    expect(results[0].id).toBe(btn.id)
  })

  test('handles names with single quotes', () => {
    const { graph, figma } = setup()
    const rect = figma.createRectangle()
    rect.name = "it's a test"

    const xpath = nodeToXPath(graph, rect.id)
    expect(xpath).toBe('//RECTANGLE[@name="it\'s a test"]')
  })
})

describe('matchByXPath', () => {
  test('returns true for matching node', async () => {
    const { graph, figma } = setup()
    const rect = figma.createRectangle()
    rect.resize(100, 100)
    rect.name = 'TestRect'

    const sceneNode = graph.getNode(rect.id)!
    const result = await matchByXPath(graph, '@name = "TestRect"', sceneNode)
    expect(result).toBe(true)
  })

  test('returns false for non-matching node', async () => {
    const { graph, figma } = setup()
    const rect = figma.createRectangle()
    rect.resize(100, 100)
    rect.name = 'Other'

    const sceneNode = graph.getNode(rect.id)!
    const result = await matchByXPath(graph, '@name = "TestRect"', sceneNode)
    expect(result).toBe(false)
  })
})
