import { test, expect } from '@playwright/test'

import { expectDefined } from '#tests/helpers/assert'
import { CanvasHelper } from '#tests/helpers/canvas'

const NODE_COUNT = 200

test.describe('Zoom and pan', () => {
  let helper: CanvasHelper

  test.beforeAll(async ({ browser }) => {
    const page = await browser.newPage()
    helper = new CanvasHelper(page)
    await page.goto('http://localhost:1420/?test&no-chrome&no-rulers')
    await helper.waitForInit()

    await page.evaluate((count: number) => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      const cols = Math.ceil(Math.sqrt(count))
      for (let i = 0; i < count; i++) {
        store.graph.createNode('RECTANGLE', store.state.currentPageId, {
          x: (i % cols) * 60,
          y: Math.floor(i / cols) * 60,
          width: 50,
          height: 50,
          fills: [
            { type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.8, a: 1 }, visible: true, opacity: 1 }
          ]
        })
      }
      store.requestRender()
    }, NODE_COUNT)
    await helper.waitForRender()
  })

  test.afterAll(async () => {
    await helper.page.close()
  })

  test('wheel zoom updates viewport correctly', async () => {
    const before = await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      return { panX: store.state.panX, panY: store.state.panY, zoom: store.state.zoom }
    })

    // Use Playwright's native mouse.wheel with Control key held for zoom
    const box = expectDefined(await helper.canvas.boundingBox(), 'canvas bounds')
    await helper.page.mouse.move(box.x + 400, box.y + 300)
    await helper.page.keyboard.down('Control')
    await helper.page.mouse.wheel(0, -100)
    await helper.page.keyboard.up('Control')
    await helper.waitForRender()
    await helper.page.waitForTimeout(50)

    const after = await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      return { panX: store.state.panX, panY: store.state.panY, zoom: store.state.zoom }
    })

    expect(after.zoom).not.toBe(before.zoom)
    helper.assertNoErrors()
  })

  test('wheel pan updates viewport correctly', async () => {
    const before = await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      return { panX: store.state.panX, panY: store.state.panY }
    })

    const box = expectDefined(await helper.canvas.boundingBox(), 'canvas bounds')
    await helper.page.mouse.move(box.x + 400, box.y + 300)

    // Playwright wheel without ctrl → pan
    await helper.page.mouse.wheel(100, 50)
    await helper.waitForRender()
    await helper.page.waitForTimeout(50)

    const after = await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      return { panX: store.state.panX, panY: store.state.panY }
    })

    expect(after.panX).toBeLessThan(before.panX)
    expect(after.panY).toBeLessThan(before.panY)
    helper.assertNoErrors()
  })

  test('rapid wheel events are coalesced without errors', async () => {
    const box = expectDefined(await helper.canvas.boundingBox(), 'canvas bounds')
    await helper.page.mouse.move(box.x + 400, box.y + 300)
    await helper.page.keyboard.down('Control')
    for (let i = 0; i < 50; i++) {
      await helper.page.mouse.wheel(0, -5)
    }
    await helper.page.keyboard.up('Control')
    await helper.waitForRender()
    await helper.page.waitForTimeout(50)

    const state = await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      return { zoom: store.state.zoom }
    })

    expect(state.zoom).toBeGreaterThan(0)
    expect(state.zoom).toBeLessThan(256)
    helper.assertNoErrors()
  })

  test('shallowReactive: selection replace triggers UI update', async () => {
    const result = await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      const pageNode = store.graph.getNode(store.state.currentPageId)
      if (!pageNode) throw new Error(`Page ${store.state.currentPageId} not found`)
      const firstId = pageNode.childIds[0]
      if (!firstId) throw new Error('First page child not found')

      store.select([firstId])
      const after = store.state.selectedIds.has(firstId)
      const size = store.state.selectedIds.size

      store.clearSelection()
      const cleared = store.state.selectedIds.size

      return { after, size, cleared }
    })

    expect(result.after).toBe(true)
    expect(result.size).toBe(1)
    expect(result.cleared).toBe(0)
  })

  test('useRafFn loop picks up renderVersion changes', async () => {
    // Ensure clean state, wait for any pending renders
    await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      store.state.panX = 0
      store.state.panY = 0
      store.state.zoom = 1
      store.clearSelection()
      store.requestRender()
    })
    await helper.waitForRender()
    await helper.page.waitForTimeout(100)
    const before = await helper.screenshotCanvas()

    // Change fill color — always visible
    await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      const pageNode = store.graph.getNode(store.state.currentPageId)
      if (!pageNode) throw new Error(`Page ${store.state.currentPageId} not found`)
      const firstId = pageNode.childIds[0]
      if (!firstId) throw new Error('First page child not found')
      store.graph.updateNode(firstId, {
        fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, visible: true, opacity: 1 }]
      })
      store.requestRender()
    })
    await helper.waitForRender()
    await helper.page.waitForTimeout(100)

    const after = await helper.screenshotCanvas()
    expect(Buffer.from(before)).not.toEqual(Buffer.from(after))

    // Restore
    await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      const pageNode = store.graph.getNode(store.state.currentPageId)
      if (!pageNode) throw new Error(`Page ${store.state.currentPageId} not found`)
      const firstId = pageNode.childIds[0]
      if (!firstId) throw new Error('First page child not found')
      store.graph.updateNode(firstId, {
        fills: [
          { type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.8, a: 1 }, visible: true, opacity: 1 }
        ]
      })
      store.requestRender()
    })
    await helper.waitForRender()
  })

  test('useRafFn loop picks up selection changes', async () => {
    const before = await helper.screenshotCanvas()

    await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      const pageNode = store.graph.getNode(store.state.currentPageId)
      if (!pageNode) throw new Error(`Page ${store.state.currentPageId} not found`)
      const firstId = pageNode.childIds[0]
      if (!firstId) throw new Error('First page child not found')
      store.select([firstId])
    })
    await helper.waitForRender()
    await helper.page.waitForTimeout(50)

    const after = await helper.screenshotCanvas()
    // Selection border should change the rendering
    expect(Buffer.from(before)).not.toEqual(Buffer.from(after))

    await helper.page.evaluate(() => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')
      store.clearSelection()
    })
    await helper.waitForRender()
  })

  test('benchmark: zoom/pan pipeline throughput', async () => {
    const ITERATIONS = 500

    const results = await helper.page.evaluate((iterations: number) => {
      const store = window.openPencil?.getStore?.()
      if (!store) throw new Error('OpenPencil store not initialized')

      // Reset viewport
      store.state.panX = 0
      store.state.panY = 0
      store.state.zoom = 1
      store.requestRepaint()

      // Benchmark: applyZoom calls (store → state mutation → renderVersion bump)
      const zoomStart = performance.now()
      for (let i = 0; i < iterations; i++) {
        store.applyZoom(i % 2 === 0 ? -1 : 1, 400, 300)
      }
      const zoomMs = performance.now() - zoomStart

      // Reset
      store.state.zoom = 1
      store.state.panX = 0
      store.state.panY = 0

      // Benchmark: pan calls
      const panStart = performance.now()
      for (let i = 0; i < iterations; i++) {
        store.pan(1, 1)
      }
      const panMs = performance.now() - panStart

      // Benchmark: simulated wheel coalescing — many wheel events in one frame
      store.state.zoom = 1
      store.state.panX = 0
      store.state.panY = 0

      const canvas = document.querySelector<HTMLCanvasElement>('[data-ready="1"]')
      if (!canvas) throw new Error('Canvas element not found')
      const wheelStart = performance.now()
      for (let i = 0; i < iterations; i++) {
        canvas.dispatchEvent(
          new WheelEvent('wheel', {
            deltaX: 0,
            deltaY: -2,
            ctrlKey: true,
            clientX: 400,
            clientY: 300,
            bubbles: true,
            cancelable: true
          })
        )
      }
      const wheelMs = performance.now() - wheelStart

      return {
        zoom: {
          ms: Math.round(zoomMs * 100) / 100,
          avg: Math.round((zoomMs / iterations) * 1000) / 1000
        },
        pan: {
          ms: Math.round(panMs * 100) / 100,
          avg: Math.round((panMs / iterations) * 1000) / 1000
        },
        wheel: {
          ms: Math.round(wheelMs * 100) / 100,
          avg: Math.round((wheelMs / iterations) * 1000) / 1000
        }
      }
    }, ITERATIONS)

    console.log(`\n═══ ZOOM/PAN PIPELINE BENCHMARK (${ITERATIONS} iterations) ═══`)
    console.log(`  applyZoom:     ${results.zoom.avg}ms/call  (${results.zoom.ms}ms total)`)
    console.log(`  pan:           ${results.pan.avg}ms/call  (${results.pan.ms}ms total)`)
    console.log(`  wheel events:  ${results.wheel.avg}ms/event  (${results.wheel.ms}ms total)`)
    console.log(`═══════════════════════════════════════════════════════\n`)

    // Each call should be sub-millisecond
    expect(results.zoom.avg).toBeLessThan(1)
    expect(results.pan.avg).toBeLessThan(1)
    expect(results.wheel.avg).toBeLessThan(1)

    helper.assertNoErrors()
  })
})
