import type { Locator } from '@playwright/test'

import {
  comfyExpect as expect,
  comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import type { Position } from '@e2e/fixtures/types'

test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
  const getHeaderPos = async (
    comfyPage: ComfyPage,
    title: string
  ): Promise<{ x: number; y: number; width: number; height: number }> => {
    const box = await comfyPage.vueNodes
      .getNodeByTitle(title)
      .getByTestId('node-title')
      .first()
      .boundingBox()
    if (!box) throw new Error(`${title} header not found`)
    return box
  }

  const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) =>
    getHeaderPos(comfyPage, 'Load Checkpoint')

  const expectPosChanged = async (pos1: Position, pos2: Position) => {
    const diffX = Math.abs(pos2.x - pos1.x)
    const diffY = Math.abs(pos2.y - pos1.y)
    expect(diffX).toBeGreaterThan(0)
    expect(diffY).toBeGreaterThan(0)
  }

  const deltaBetween = (before: Position, after: Position) => ({
    x: after.x - before.x,
    y: after.y - before.y
  })

  const expectSameDelta = (a: Position, b: Position, tol = 2) => {
    expect(Math.abs(a.x - b.x)).toBeLessThanOrEqual(tol)
    expect(Math.abs(a.y - b.y)).toBeLessThanOrEqual(tol)
  }

  const dragFromTabButton = async (comfyPage: ComfyPage, button: Locator) => {
    const box = await button.boundingBox()
    if (!box) throw new Error('Tab button has no bounding box')
    const start = {
      x: box.x + box.width / 2,
      y: box.y + box.height * 0.75
    }
    await comfyPage.canvasOps.dragAndDrop(start, {
      x: start.x + 120,
      y: start.y + 80
    })
  }

  test('should allow moving nodes by dragging', async ({ comfyPage }) => {
    const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
    await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, {
      x: 256,
      y: 256
    })

    const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
    await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
  })

  test('should not move node when pointer moves less than drag threshold', async ({
    comfyPage
  }) => {
    const headerPos = await getLoadCheckpointHeaderPos(comfyPage)

    // Move only 2px — below the 3px drag threshold in useNodePointerInteractions
    await comfyPage.page.mouse.move(headerPos.x, headerPos.y)
    await comfyPage.page.mouse.down()
    await comfyPage.page.mouse.move(headerPos.x + 2, headerPos.y + 1, {
      steps: 5
    })
    await comfyPage.page.mouse.up()
    await comfyPage.nextFrame()

    const afterPos = await getLoadCheckpointHeaderPos(comfyPage)
    expect(afterPos.x).toBeCloseTo(headerPos.x, 0)
    expect(afterPos.y).toBeCloseTo(headerPos.y, 0)

    // The small movement should have selected the node, not dragged it
    await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(1)
  })

  test('should move node when pointer moves beyond drag threshold', async ({
    comfyPage
  }) => {
    const headerPos = await getLoadCheckpointHeaderPos(comfyPage)

    // Move 50px — well beyond the 3px drag threshold
    await comfyPage.page.mouse.move(headerPos.x, headerPos.y)
    await comfyPage.page.mouse.down()
    await comfyPage.page.mouse.move(headerPos.x + 50, headerPos.y + 50, {
      steps: 20
    })
    await comfyPage.page.mouse.up()
    await comfyPage.nextFrame()

    const afterPos = await getLoadCheckpointHeaderPos(comfyPage)
    await expectPosChanged(headerPos, afterPos)
  })

  test('should not toggle advanced inputs when dragging by the Advanced button', async ({
    comfyPage
  }) => {
    await comfyPage.settings.setSetting(
      'Comfy.Node.AlwaysShowAdvancedWidgets',
      false
    )
    await comfyPage.nodeOps.addNode(
      'ModelSamplingFlux',
      {},
      {
        x: 500,
        y: 200
      }
    )
    await comfyPage.vueNodes.waitForNodes()

    const node = comfyPage.vueNodes.getNodeByTitle('ModelSamplingFlux')
    const showButton = node.getByText('Show advanced inputs')
    const widgets = node.locator('.lg-node-widget')

    await expect(showButton).toBeVisible()
    await expect(widgets).toHaveCount(2)

    const beforePos = await node.boundingBox()
    if (!beforePos) throw new Error('Node has no bounding box')

    await dragFromTabButton(comfyPage, showButton)

    await expect(showButton).toBeVisible()
    await expect(node.getByText('Hide advanced inputs')).toBeHidden()
    await expect(widgets).toHaveCount(2)

    const afterPos = await node.boundingBox()
    if (!afterPos) throw new Error('Node missing after drag')
    await expectPosChanged(beforePos, afterPos)
  })

  test('should not enter subgraph when dragging by the Enter Subgraph button', async ({
    comfyPage
  }) => {
    await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')

    const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
    const beforePos = await subgraphNode.getPosition()

    await dragFromTabButton(
      comfyPage,
      comfyPage.vueNodes.getSubgraphEnterButton('2')
    )

    expect(await comfyPage.subgraph.isInSubgraph()).toBe(false)

    const afterPos = await subgraphNode.getPosition()
    await expectPosChanged(beforePos, afterPos)
  })

  test('should move all selected nodes together when dragging one with Meta held', async ({
    comfyPage
  }) => {
    const checkpointBefore = await getHeaderPos(comfyPage, 'Load Checkpoint')
    const ksamplerBefore = await getHeaderPos(comfyPage, 'KSampler')
    const latentBefore = await getHeaderPos(comfyPage, 'Empty Latent Image')

    const dx = 120
    const dy = 80

    const clickNodeTitleWithMeta = async (title: string) => {
      await comfyPage.vueNodes
        .getNodeByTitle(title)
        .getByTestId('node-title')
        .first()
        .click({ modifiers: ['Meta'] })
    }

    await comfyPage.page.keyboard.down('Meta')
    try {
      await clickNodeTitleWithMeta('Load Checkpoint')
      await clickNodeTitleWithMeta('KSampler')
      await clickNodeTitleWithMeta('Empty Latent Image')
      await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(3)

      // Re-fetch drag source after clicks in case the header reflowed.
      const dragSrc = await getHeaderPos(comfyPage, 'Load Checkpoint')
      const centerX = dragSrc.x + dragSrc.width / 2
      const centerY = dragSrc.y + dragSrc.height / 2

      await comfyPage.page.mouse.move(centerX, centerY)
      await comfyPage.page.mouse.down()
      await comfyPage.nextFrame()
      await comfyPage.page.mouse.move(centerX + dx, centerY + dy, {
        steps: 20
      })
      await comfyPage.page.mouse.up()
      await comfyPage.nextFrame()
    } finally {
      await comfyPage.page.keyboard.up('Meta')
      await comfyPage.nextFrame()
    }

    await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(3)

    const checkpointAfter = await getHeaderPos(comfyPage, 'Load Checkpoint')
    const ksamplerAfter = await getHeaderPos(comfyPage, 'KSampler')
    const latentAfter = await getHeaderPos(comfyPage, 'Empty Latent Image')

    // All three nodes should have moved together by the same delta.
    // We don't assert the exact screen delta equals the dragged pixel delta,
    // because canvas scaling and snap-to-grid can introduce offsets.
    const checkpointDelta = deltaBetween(checkpointBefore, checkpointAfter)
    const ksamplerDelta = deltaBetween(ksamplerBefore, ksamplerAfter)
    const latentDelta = deltaBetween(latentBefore, latentAfter)

    // Confirm an actual drag happened (not zero movement).
    expect(Math.abs(checkpointDelta.x)).toBeGreaterThan(10)
    expect(Math.abs(checkpointDelta.y)).toBeGreaterThan(10)

    // Confirm all selected nodes moved by the same delta.
    expectSameDelta(checkpointDelta, ksamplerDelta)
    expectSameDelta(checkpointDelta, latentDelta)

    await comfyPage.canvasOps.moveMouseToEmptyArea()
  })

  test(
    '@mobile should allow moving nodes by dragging on touch devices',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      // Disable minimap (gets in way of the node on small screens)
      await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false)

      const loadCheckpointHeaderPos =
        await getLoadCheckpointHeaderPos(comfyPage)
      await comfyPage.canvasOps.panWithTouch(
        {
          x: 64,
          y: 64
        },
        loadCheckpointHeaderPos
      )

      const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
      expect(newHeaderPos.x).toBeCloseTo(loadCheckpointHeaderPos.x + 64)
      expect(newHeaderPos.y).toBeCloseTo(loadCheckpointHeaderPos.y + 64)
    }
  )
})
