import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'

import {
  comfyPageFixture as test,
  testComfySnapToGridGridSize
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions'
import { TestIds } from '@e2e/fixtures/selectors'
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
import type { WorkspaceStore } from '@e2e/types/globals'

test.beforeEach(async ({ comfyPage }) => {
  await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
  // Wait for the legacy menu to appear and canvas to settle after layout shift.
  await comfyPage.page.locator('.comfy-menu').waitFor({ state: 'visible' })
  await comfyPage.nextFrame()
})

test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
  test('Can select/delete all items', async ({ comfyPage }) => {
    await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items')
    await comfyPage.canvas.press('Control+a')
    await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
    await comfyPage.canvas.press('Delete')
    await expect(comfyPage.canvas).toHaveScreenshot('deleted-all.png')
  })

  test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
    await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items')
    await comfyPage.canvas.press('Control+a')
    await comfyPage.keyboard.press('KeyP')
    await expect(comfyPage.canvas).toHaveScreenshot('pinned-all.png')
    await comfyPage.keyboard.press('KeyP')
    await expect(comfyPage.canvas).toHaveScreenshot('unpinned-all.png')
  })
})

test.describe('Node Interaction', () => {
  test('Can enter prompt', async ({ comfyPage }) => {
    const textBox = comfyPage.widgetTextBox
    await textBox.click()
    await textBox.fill('Hello World')
    await expect(textBox).toHaveValue('Hello World')
    await textBox.fill('Hello World 2')
    await expect(textBox).toHaveValue('Hello World 2')
  })

  test.describe('Node Selection', () => {
    const multiSelectModifiers = ['Control', 'Shift', 'Meta'] as const

    multiSelectModifiers.forEach((modifier) => {
      test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
        comfyPage
      }) => {
        const clipNodes =
          await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
        for (const node of clipNodes) {
          await node.click('title', { modifiers: [modifier] })
        }
        await expect
          .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
          .toBe(clipNodes.length)
      })
    })

    test(
      '@2x Can highlight selected',
      { tag: '@screenshot' },
      async ({ comfyPage }) => {
        await expect(comfyPage.canvas).toHaveScreenshot('default.png')
        await comfyPage.canvas.click({
          position: DefaultGraphPositions.textEncodeNode1
        })
        await comfyPage.expectScreenshot(comfyPage.canvas, 'selected-node1.png')
        await comfyPage.canvas.click({
          position: DefaultGraphPositions.textEncodeNode2
        })
        await comfyPage.expectScreenshot(comfyPage.canvas, 'selected-node2.png')
      }
    )

    const dragSelectNodes = async (
      comfyPage: ComfyPage,
      clipNodes: NodeReference[]
    ) => {
      const clipNode1Pos = await clipNodes[0].getPosition()
      const clipNode2Pos = await clipNodes[1].getPosition()
      const offset = 64
      await comfyPage.page.keyboard.down('Meta')
      await comfyPage.canvasOps.dragAndDrop(
        {
          x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
          y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
        },
        {
          x: Math.max(clipNode1Pos.x, clipNode2Pos.x) + offset,
          y: Math.max(clipNode1Pos.y, clipNode2Pos.y) + offset
        }
      )
      await comfyPage.page.keyboard.up('Meta')
    }

    test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => {
      const clipNodes =
        await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
      await dragSelectNodes(comfyPage, clipNodes)
      await expect
        .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
        .toBe(clipNodes.length)
    })

    test('Can move selected nodes using the Comfy.Canvas.MoveSelectedNodes.{Up|Down|Left|Right} commands', async ({
      comfyPage
    }) => {
      const clipNodes =
        await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
      const getPositions = () =>
        Promise.all(clipNodes.map((node) => node.getPosition()))
      const testDirection = async ({
        direction,
        expectedPosition
      }: {
        direction: string
        expectedPosition: (originalPosition: Position) => Position
      }) => {
        const originalPositions = await getPositions()
        await dragSelectNodes(comfyPage, clipNodes)
        await comfyPage.command.executeCommand(
          `Comfy.Canvas.MoveSelectedNodes.${direction}`
        )
        await comfyPage.canvas.press(`Control+Arrow${direction}`)
        const newPositions = await getPositions()
        expect(newPositions).toEqual(originalPositions.map(expectedPosition))
      }
      await testDirection({
        direction: 'Down',
        expectedPosition: (originalPosition) => ({
          ...originalPosition,
          y: originalPosition.y + testComfySnapToGridGridSize
        })
      })
      await testDirection({
        direction: 'Right',
        expectedPosition: (originalPosition) => ({
          ...originalPosition,
          x: originalPosition.x + testComfySnapToGridGridSize
        })
      })
      await testDirection({
        direction: 'Up',
        expectedPosition: (originalPosition) => ({
          ...originalPosition,
          y: originalPosition.y - testComfySnapToGridGridSize
        })
      })
      await testDirection({
        direction: 'Left',
        expectedPosition: (originalPosition) => ({
          ...originalPosition,
          x: originalPosition.x - testComfySnapToGridGridSize
        })
      })
    })
  })

  test.describe('Node Duplication', () => {
    test.beforeEach(async ({ comfyPage }) => {
      // Pin this suite to the legacy canvas path so Alt+drag exercises
      // LGraphCanvas, not the Vue node drag handler.
      await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
    })

    test('Can duplicate a regular node via Alt+drag', async ({ comfyPage }) => {
      const before = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
      expect(
        before,
        'Expected exactly 2 CLIPTextEncode nodes in default graph'
      ).toHaveLength(2)

      const target = before[0]
      const pos = await target.getPosition()
      const src = { x: pos.x + 16, y: pos.y + 16 }

      await comfyPage.page.mouse.move(src.x, src.y)
      await comfyPage.page.keyboard.down('Alt')
      try {
        await comfyPage.page.mouse.down()
        await comfyPage.nextFrame()
        await comfyPage.page.mouse.move(src.x + 120, src.y + 80, { steps: 20 })
        await comfyPage.page.mouse.up()
        await comfyPage.nextFrame()
      } finally {
        await comfyPage.page.keyboard.up('Alt')
      }
      await comfyPage.canvasOps.moveMouseToEmptyArea()

      await expect
        .poll(
          async () =>
            (await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')).length
        )
        .toBe(3)
      expect(await target.exists()).toBe(true)
    })
  })

  test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
    test.beforeEach(async ({ comfyPage }) => {
      await comfyPage.settings.setSetting(
        'Comfy.LinkRelease.Action',
        'no action'
      )
      await comfyPage.settings.setSetting(
        'Comfy.LinkRelease.ActionShift',
        'no action'
      )
    })

    // Test both directions of edge connection.
    ;[{ reverse: false }, { reverse: true }].forEach(({ reverse }) => {
      test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
        comfyPage
      }) => {
        await comfyPage.canvasOps.disconnectEdge()
        await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
        await comfyPage.canvasOps.connectEdge({ reverse })
        // Move mouse to empty area to avoid slot highlight.
        await comfyPage.canvasOps.moveMouseToEmptyArea()
        // Litegraph renders edge with a slight offset.
        await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
          maxDiffPixels: 50
        })
      })
    })

    test('Can move link', async ({ comfyPage }) => {
      await comfyPage.canvasOps.dragAndDrop(
        DefaultGraphPositions.clipTextEncodeNode1InputSlot,
        DefaultGraphPositions.emptySpace
      )
      await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
      await comfyPage.canvasOps.dragAndDrop(
        DefaultGraphPositions.clipTextEncodeNode2InputSlot,
        DefaultGraphPositions.clipTextEncodeNode1InputSlot
      )
      await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png')
    })

    test('Can copy link by shift-drag existing link', async ({ comfyPage }) => {
      await comfyPage.canvasOps.dragAndDrop(
        DefaultGraphPositions.clipTextEncodeNode1InputSlot,
        DefaultGraphPositions.emptySpace
      )
      await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
      await comfyPage.page.keyboard.down('Shift')
      await comfyPage.canvasOps.dragAndDrop(
        DefaultGraphPositions.clipTextEncodeNode2InputLinkPath,
        DefaultGraphPositions.clipTextEncodeNode1InputSlot
      )
      await comfyPage.page.keyboard.up('Shift')
      await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
    })

    test('Auto snap&highlight when dragging link over node', async ({
      comfyPage,
      comfyMouse
    }) => {
      await comfyPage.settings.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
      await comfyPage.settings.setSetting('Comfy.Node.SnapHighlightsNode', true)

      await comfyMouse.move(DefaultGraphPositions.clipTextEncodeNode1InputSlot)
      await comfyMouse.drag(DefaultGraphPositions.clipTextEncodeNode2InputSlot)
      await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
    })
  })

  test(
    'Can adjust widget value',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      await comfyPage.nodeOps.adjustEmptyLatentWidth()
      await expect(comfyPage.canvas).toHaveScreenshot(
        'adjusted-widget-value.png'
      )
    }
  )

  test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => {
    await comfyPage.workflow.loadWorkflow('links/snap_to_slot')
    await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')

    const outputSlotPos = {
      x: 406,
      y: 333
    }
    const samplerNodeCenterPos = {
      x: 748,
      y: 77
    }
    await comfyPage.canvasOps.dragAndDrop(outputSlotPos, samplerNodeCenterPos)

    await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
  })

  test(
    'Can batch move links by drag with shift',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      await comfyPage.workflow.loadWorkflow('links/batch_move_links')
      await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')

      const outputSlot1Pos = {
        x: 304,
        y: 127
      }
      const outputSlot2Pos = {
        x: 307,
        y: 310
      }

      await comfyPage.page.keyboard.down('Shift')
      await comfyPage.canvasOps.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
      await comfyPage.page.keyboard.up('Shift')

      await expect(comfyPage.canvas).toHaveScreenshot(
        'batch_move_links_moved.png'
      )
    }
  )

  test(
    'Can batch disconnect links with ctrl+alt+click',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      const loadCheckpointClipSlotPos = {
        x: 332,
        y: 508
      }
      await comfyPage.canvas.click({
        modifiers: ['Control', 'Alt'],
        position: loadCheckpointClipSlotPos
      })
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'batch-disconnect-links-disconnected.png'
      )
    }
  )

  test(
    'Can toggle dom widget node open/closed',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      // Find the node whose collapse toggler matches the hardcoded position.
      // getNodeRefsByType order is non-deterministic, so identify by proximity.
      const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
      const togglerPos = DefaultGraphPositions.textEncodeNodeToggler
      let targetNode = nodes[0]
      let minDist = Infinity
      for (const n of nodes) {
        const pos = await n.getPosition()
        const dist = Math.hypot(pos.x - togglerPos.x, pos.y - togglerPos.y)
        if (dist < minDist) {
          minDist = dist
          targetNode = n
        }
      }

      await expect(comfyPage.canvas).toHaveScreenshot('default.png')
      await comfyPage.canvas.click({
        position: togglerPos
      })
      await expect.poll(() => targetNode.isCollapsed()).toBe(true)
      await expect(comfyPage.canvas).toHaveScreenshot(
        'text-encode-toggled-off.png'
      )
      // Wait for the double-click window (300ms) to expire so the next
      // click at the same position isn't interpreted as a double-click.
      await expect
        .poll(() =>
          comfyPage.page.evaluate(() => {
            const pointer = window.app!.canvas.pointer
            if (!pointer.eLastDown) return true
            return performance.now() - pointer.eLastDown.timeStamp > 300
          })
        )
        .toBe(true)
      await comfyPage.canvas.click({
        position: togglerPos
      })
      await expect.poll(() => targetNode.isCollapsed()).toBe(false)
      // Move mouse away to avoid hover highlight differences.
      await comfyPage.canvasOps.moveMouseToEmptyArea()
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'text-encode-toggled-back-open.png'
      )
    }
  )

  test('Can close prompt dialog with canvas click (number widget)', async ({
    comfyPage
  }) => {
    const numberWidgetPos = {
      x: 724,
      y: 645
    }
    await comfyPage.canvas.click({
      position: numberWidgetPos
    })
    const legacyPrompt = comfyPage.page.locator('.graphdialog')
    await expect(legacyPrompt).toBeVisible()
    // LiteGraph's graphdialog has a 256ms dismiss guard (Date.now() - clickTime > 256).
    // Retry the canvas click until the dialog actually closes.
    await expect(async () => {
      await comfyPage.canvas.click({
        position: {
          x: 10,
          y: 10
        }
      })
      await expect(legacyPrompt).toBeHidden({ timeout: 500 })
    }).toPass({ timeout: 5000 })
  })

  test('Can close prompt dialog with canvas click (text widget)', async ({
    comfyPage
  }) => {
    const textWidgetPos = {
      x: 167,
      y: 143
    }
    await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node')
    await comfyPage.canvas.click({
      position: textWidgetPos
    })
    const legacyPrompt = comfyPage.page.locator('.graphdialog')
    await expect(legacyPrompt).toBeVisible()
    // LiteGraph's graphdialog has a 256ms dismiss guard (Date.now() - clickTime > 256).
    // Retry the canvas click until the dialog actually closes.
    await expect(async () => {
      await comfyPage.canvas.click({
        position: {
          x: 10,
          y: 10
        }
      })
      await expect(legacyPrompt).toBeHidden({ timeout: 500 })
    }).toPass({ timeout: 5000 })
  })

  test(
    'Can double click node title to edit',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
      await comfyPage.canvas.dblclick({
        position: {
          x: 50,
          y: 10
        },
        delay: 5
      })
      await comfyPage.page.keyboard.type('Hello World')
      await comfyPage.page.keyboard.press('Enter')
      await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
    }
  )

  test('Double click node body does not trigger edit', async ({
    comfyPage
  }) => {
    await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
    await comfyPage.canvas.dblclick({
      position: {
        x: 50,
        y: 50
      },
      delay: 5
    })
    await expect(comfyPage.page.locator('.node-title-editor')).toHaveCount(0)
  })

  test(
    'Can group selected nodes',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      await comfyPage.settings.setSetting(
        'Comfy.GroupSelectedNodes.Padding',
        10
      )
      await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
      await comfyPage.page.keyboard.down('Control')
      await comfyPage.page.keyboard.press('KeyG')
      await comfyPage.page.keyboard.up('Control')
      await comfyPage.nextFrame()
      // Confirm group title
      await comfyPage.keyboard.press('Enter')
      await expect(comfyPage.canvas).toHaveScreenshot(
        'group-selected-nodes.png'
      )
    }
  )

  test(
    'Can fit group to contents',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      await comfyPage.workflow.loadWorkflow('groups/oversized_group')
      await expect
        .poll(() =>
          comfyPage.page.evaluate(() => {
            const group = window.app!.graph.groups[0]
            return group ? [group.size[0], group.size[1]] : null
          })
        )
        .not.toBeNull()

      const initialGroupSize = await comfyPage.page.evaluate(() => {
        const group = window.app!.graph.groups[0]
        return group ? [group.size[0], group.size[1]] : null
      })

      await comfyPage.keyboard.selectAll()
      await comfyPage.nextFrame()
      await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')
      await expect
        .poll(() =>
          comfyPage.page.evaluate(() => {
            const group = window.app!.graph.groups[0]
            return group ? [group.size[0], group.size[1]] : null
          })
        )
        .not.toEqual(initialGroupSize)
      await expect(comfyPage.canvas).toHaveScreenshot(
        'group-fit-to-contents.png'
      )
    }
  )

  test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
    await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
    const nodeRef = (
      await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
    )[0]

    await comfyPage.command.executeCommand(
      'Comfy.Canvas.ToggleSelectedNodes.Pin'
    )
    await expect.poll(() => nodeRef.isPinned()).toBe(true)
    await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png')
    await comfyPage.command.executeCommand(
      'Comfy.Canvas.ToggleSelectedNodes.Pin'
    )
    await expect.poll(() => nodeRef.isPinned()).toBe(false)
    await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
  })

  test(
    'Can bypass/unbypass nodes with keyboard shortcut',
    { tag: '@screenshot' },
    async ({ comfyPage }) => {
      await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
      const nodeRef = (
        await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
      )[0]

      await comfyPage.canvas.press('Control+b')
      await expect.poll(() => nodeRef.isBypassed()).toBe(true)
      await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
      await comfyPage.canvas.press('Control+b')
      await expect.poll(() => nodeRef.isBypassed()).toBe(false)
      await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
    }
  )
})

test.describe('Group Interaction', { tag: '@screenshot' }, () => {
  test('Can double click group title to edit', async ({ comfyPage }) => {
    await comfyPage.workflow.loadWorkflow('groups/single_group')
    await comfyPage.canvas.dblclick({
      position: {
        x: 50,
        y: 10
      },
      delay: 5
    })
    await comfyPage.page.keyboard.type('Hello World')
    await comfyPage.page.keyboard.press('Enter')
    await expect(comfyPage.canvas).toHaveScreenshot('group-title-edited.png')
  })
})

test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
  test('Can zoom in/out', async ({ comfyPage }) => {
    await comfyPage.canvasOps.zoom(-100)
    await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
    await comfyPage.canvasOps.zoom(200)
    await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png')
  })

  test('Can zoom very far out', async ({ comfyPage }) => {
    await comfyPage.canvasOps.zoom(100, 12)
    await expect(comfyPage.canvas).toHaveScreenshot('zoomed-very-far-out.png')
    await comfyPage.canvasOps.zoom(-100, 12)
    await expect(comfyPage.canvas).toHaveScreenshot('zoomed-back-in.png')
  })

  test('Can zoom in/out with ctrl+shift+vertical-drag', async ({
    comfyPage
  }) => {
    await comfyPage.page.keyboard.down('Control')
    await comfyPage.page.keyboard.down('Shift')
    await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 })
    await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
    await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 })
    await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out-ctrl-shift.png')
    await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 })
    await expect(comfyPage.canvas).toHaveScreenshot(
      'zoomed-default-ctrl-shift.png'
    )
    await comfyPage.page.keyboard.up('Control')
    await comfyPage.page.keyboard.up('Shift')
  })

  test('Can zoom in/out after decreasing canvas zoom speed setting', async ({
    comfyPage
  }) => {
    await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.05)
    await comfyPage.canvasOps.zoom(-100, 4)
    await expect(comfyPage.canvas).toHaveScreenshot(
      'zoomed-in-low-zoom-speed.png'
    )
    await comfyPage.canvasOps.zoom(100, 8)
    await expect(comfyPage.canvas).toHaveScreenshot(
      'zoomed-out-low-zoom-speed.png'
    )
    await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
  })

  test('Can zoom in/out after increasing canvas zoom speed', async ({
    comfyPage
  }) => {
    await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.5)
    await comfyPage.canvasOps.zoom(-100, 4)
    await expect(comfyPage.canvas).toHaveScreenshot(
      'zoomed-in-high-zoom-speed.png'
    )
    await comfyPage.canvasOps.zoom(100, 8)
    await expect(comfyPage.canvas).toHaveScreenshot(
      'zoomed-out-high-zoom-speed.png'
    )
    await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
  })

  test('Can pan', async ({ comfyPage }) => {
    await comfyPage.canvasOps.pan({ x: 200, y: 200 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
  })

  test('Cursor style changes when panning', async ({ comfyPage }) => {
    const getCursorStyle = async () => {
      return await comfyPage.page.evaluate(() => {
        return (
          document.getElementById('graph-canvas')!.style.cursor || 'default'
        )
      })
    }

    await comfyPage.page.mouse.move(10, 10)
    await expect.poll(() => getCursorStyle()).toBe('default')
    await comfyPage.page.mouse.down()
    await expect.poll(() => getCursorStyle()).toBe('grabbing')
    // Move mouse should not alter cursor style.
    await comfyPage.page.mouse.move(10, 20)
    await expect.poll(() => getCursorStyle()).toBe('grabbing')
    await comfyPage.page.mouse.up()
    await expect.poll(() => getCursorStyle()).toBe('default')

    await comfyPage.page.keyboard.down('Space')
    await expect.poll(() => getCursorStyle()).toBe('grab')
    await comfyPage.page.mouse.down()
    await expect.poll(() => getCursorStyle()).toBe('grabbing')
    await comfyPage.page.mouse.up()
    await expect.poll(() => getCursorStyle()).toBe('grab')
    await comfyPage.page.keyboard.up('Space')
    await expect.poll(() => getCursorStyle()).toBe('default')
  })

  // https://github.com/Comfy-Org/litegraph.js/pull/424
  test('Properly resets dragging state after pan mode sequence', async ({
    comfyPage
  }) => {
    const getCursorStyle = async () => {
      return await comfyPage.page.evaluate(() => {
        return (
          document.getElementById('graph-canvas')!.style.cursor || 'default'
        )
      })
    }

    // Initial state check
    await comfyPage.page.mouse.move(10, 10)
    await expect.poll(() => getCursorStyle()).toBe('default')

    // Click and hold
    await comfyPage.page.mouse.down()
    await expect.poll(() => getCursorStyle()).toBe('grabbing')

    // Press space while holding click
    await comfyPage.page.keyboard.down('Space')
    await expect.poll(() => getCursorStyle()).toBe('grabbing')

    // Release click while space is still down
    await comfyPage.page.mouse.up()
    await expect.poll(() => getCursorStyle()).toBe('grab')

    // Release space
    await comfyPage.page.keyboard.up('Space')
    await expect.poll(() => getCursorStyle()).toBe('default')

    // Move mouse - cursor should remain default
    await comfyPage.page.mouse.move(20, 20)
    await expect.poll(() => getCursorStyle()).toBe('default')
  })

  test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => {
    const posSlot1 = DefaultGraphPositions.clipTextEncodeNode1InputSlot
    await comfyMouse.move(posSlot1)
    const posEmpty = DefaultGraphPositions.emptySpace
    await comfyMouse.drag(posEmpty)
    await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png')

    await comfyPage.page.keyboard.down('Space')
    await comfyMouse.mouse.move(posEmpty.x + 100, posEmpty.y + 100)
    // Canvas should be panned.
    await expect(comfyPage.canvas).toHaveScreenshot(
      'panning-when-dragging-link.png'
    )
    await comfyPage.page.keyboard.up('Space')
    await comfyMouse.move(posEmpty)
    // Should be back to dragging link mode when space is released.
    await expect(comfyPage.canvas).toHaveScreenshot('dragging-link2.png')
    await comfyMouse.drop()
  })

  test('Can pan very far and back', async ({ comfyPage }) => {
    // intentionally slice the edge of where the clip text encode dom widgets are
    await comfyPage.canvasOps.pan({ x: -800, y: -300 }, { x: 1000, y: 10 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned-step-one.png')
    await comfyPage.canvasOps.pan({ x: -200, y: 0 }, { x: 1000, y: 10 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned-step-two.png')
    await comfyPage.canvasOps.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned-far-away.png')
    await comfyPage.canvasOps.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned-back-from-far.png')
    await comfyPage.canvasOps.pan({ x: 200, y: 0 }, { x: 1000, y: 10 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-two.png')
    await comfyPage.canvasOps.pan({ x: 800, y: 300 }, { x: 1000, y: 10 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png')
  })

  test('@mobile Can pan with touch', async ({ comfyPage }) => {
    await comfyPage.closeMenu()
    await comfyPage.canvasOps.panWithTouch({ x: 200, y: 200 })
    await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png')
  })
})

test.describe('Widget Interaction', () => {
  test('Undo text input', async ({ comfyPage }) => {
    const textBox = comfyPage.widgetTextBox
    await textBox.click()
    await textBox.fill('')
    await expect(textBox).toHaveValue('')
    await textBox.fill('Hello World')
    await expect(textBox).toHaveValue('Hello World')
    await comfyPage.keyboard.undo(null)
    await expect(textBox).toHaveValue('')
  })

  test('Undo attention edit', async ({ comfyPage }) => {
    await comfyPage.settings.setSetting('Comfy.EditAttention.Delta', 0.05)
    const textBox = comfyPage.widgetTextBox
    await textBox.click()
    await textBox.fill('1girl')
    await expect(textBox).toHaveValue('1girl')
    await textBox.selectText()
    await comfyPage.keyboard.moveUp(null)
    await expect(textBox).toHaveValue('(1girl:1.05)')
    await comfyPage.keyboard.undo(null)
    await expect(textBox).toHaveValue('1girl')
  })
})

test.describe('Load workflow', { tag: '@screenshot' }, () => {
  test('Can load workflow with string node id', async ({ comfyPage }) => {
    await comfyPage.workflow.loadWorkflow('nodes/string_node_id')
    await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
  })

  test('Can load workflow with ("STRING",) input node', async ({
    comfyPage
  }) => {
    await comfyPage.workflow.loadWorkflow('inputs/string_input')
    await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
  })

  test('Creates initial workflow tab when persistence is disabled', async ({
    comfyPage
  }) => {
    await comfyPage.settings.setSetting('Comfy.Workflow.Persist', false)
    await comfyPage.setup()

    await expect
      .poll(() =>
        comfyPage.page.evaluate(() => {
          return (window.app!.extensionManager as WorkspaceStore).workflow
            .openWorkflows.length
        })
      )
      .toBeGreaterThanOrEqual(1)
  })

  test('Restore workflow on reload (switch workflow)', async ({
    comfyPage
  }) => {
    await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
    await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
    await comfyPage.setup({ clearStorage: false })
    await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
  })

  test('Restore workflow on reload (modify workflow)', async ({
    comfyPage
  }) => {
    await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
    const node = (await comfyPage.nodeOps.getFirstNodeRef())!
    await node.click('collapse')
    await comfyPage.canvasOps.clickEmptySpace()
    await expect(comfyPage.canvas).toHaveScreenshot(
      'single_ksampler_modified.png'
    )
    // Wait for V2 persistence debounce to save the modified workflow
    const start = Date.now()
    await comfyPage.page.waitForFunction((since) => {
      for (let i = 0; i < window.localStorage.length; i++) {
        const key = window.localStorage.key(i)
        if (!key?.startsWith('Comfy.Workflow.DraftIndex.v2:')) continue
        const json = window.localStorage.getItem(key)
        if (!json) continue
        try {
          const index = JSON.parse(json)
          if (typeof index.updatedAt === 'number' && index.updatedAt >= since) {
            return true
          }
        } catch {
          // ignore
        }
      }
      return false
    }, start)
    await comfyPage.setup({ clearStorage: false })
    await expect(comfyPage.canvas).toHaveScreenshot(
      'single_ksampler_modified.png'
    )
  })

  const generateUniqueFilename = (extension = '') =>
    `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`

  test.describe('Restore all open workflows on reload', () => {
    let workflowA: string
    let workflowB: string

    test.beforeEach(async ({ comfyPage }) => {
      await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
      workflowA = generateUniqueFilename()
      await comfyPage.menu.topbar.saveWorkflow(workflowA)
      workflowB = generateUniqueFilename()
      await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
      await comfyPage.menu.topbar.saveWorkflow(workflowB)

      // Wait for sessionStorage to persist the workflow paths before reloading
      // V2 persistence uses sessionStorage with client-scoped keys
      await comfyPage.page.waitForFunction(() => {
        for (let i = 0; i < window.sessionStorage.length; i++) {
          const key = window.sessionStorage.key(i)
          if (key?.startsWith('Comfy.Workflow.OpenPaths:')) {
            return true
          }
        }
        return false
      })
      await comfyPage.setup({ clearStorage: false })
    })

    test('Restores topbar workflow tabs after reload', async ({
      comfyPage
    }) => {
      await comfyPage.settings.setSetting(
        'Comfy.Workflow.WorkflowTabsPosition',
        'Topbar'
      )

      await expect
        .poll(() => comfyPage.menu.topbar.getTabNames())
        .toEqual(expect.arrayContaining([workflowA, workflowB]))

      await expect
        .poll(async () => {
          const tabs = await comfyPage.menu.topbar.getTabNames()
          return (
            tabs.indexOf(workflowA) < tabs.indexOf(workflowB) &&
            tabs.indexOf(workflowA) >= 0
          )
        })
        .toBe(true)

      await expect(comfyPage.menu.topbar.getActiveTab()).toContainText(
        workflowB
      )
    })

    test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
      await comfyPage.settings.setSetting(
        'Comfy.Workflow.WorkflowTabsPosition',
        'Sidebar'
      )
      await comfyPage.menu.workflowsTab.open()
      await expect
        .poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
        .toEqual(expect.arrayContaining([workflowA, workflowB]))
      await expect
        .poll(async () => {
          const ws = await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
          return ws.indexOf(workflowA) < ws.indexOf(workflowB)
        })
        .toBe(true)
      await expect(comfyPage.menu.workflowsTab.activeWorkflowLabel).toHaveText(
        workflowB
      )
    })
  })

  test.describe('Restore workflow tabs after browser restart', () => {
    let workflowA: string
    let workflowB: string

    test.beforeEach(async ({ comfyPage }) => {
      await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
      workflowA = generateUniqueFilename()
      await comfyPage.menu.topbar.saveWorkflow(workflowA)
      workflowB = generateUniqueFilename()
      await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
      await comfyPage.menu.topbar.saveWorkflow(workflowB)

      // Wait for localStorage fallback pointers to be written
      await comfyPage.page.waitForFunction(() => {
        for (let i = 0; i < window.localStorage.length; i++) {
          const key = window.localStorage.key(i)
          if (key?.startsWith('Comfy.Workflow.LastOpenPaths:')) {
            return true
          }
        }
        return false
      })

      // Simulate browser restart: clear sessionStorage (lost on close)
      // but keep localStorage (survives browser restart)
      await comfyPage.page.evaluate(() => {
        sessionStorage.clear()
      })
      await comfyPage.setup({ clearStorage: false })
    })

    test('Restores topbar workflow tabs after browser restart', async ({
      comfyPage
    }) => {
      await comfyPage.settings.setSetting(
        'Comfy.Workflow.WorkflowTabsPosition',
        'Topbar'
      )
      // Wait for both restored tabs to render (localStorage fallback is async)
      await expect(
        comfyPage.page.locator('.workflow-tabs .workflow-label', {
          hasText: workflowA
        })
      ).toBeVisible()

      await expect
        .poll(async () => {
          const tabs = await comfyPage.menu.topbar.getTabNames()
          return (
            tabs.includes(workflowA) &&
            tabs.includes(workflowB) &&
            tabs.indexOf(workflowA) < tabs.indexOf(workflowB)
          )
        })
        .toBe(true)

      await expect(comfyPage.menu.topbar.getActiveTab()).toContainText(
        workflowB
      )
    })

    test('Restores sidebar workflows after browser restart', async ({
      comfyPage
    }) => {
      await comfyPage.settings.setSetting(
        'Comfy.Workflow.WorkflowTabsPosition',
        'Sidebar'
      )
      await comfyPage.menu.workflowsTab.open()
      await expect
        .poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
        .toEqual(expect.arrayContaining([workflowA, workflowB]))
      await expect
        .poll(async () => {
          const ws = await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
          return ws.indexOf(workflowA) < ws.indexOf(workflowB)
        })
        .toBe(true)
      await expect(comfyPage.menu.workflowsTab.activeWorkflowLabel).toHaveText(
        workflowB
      )
    })
  })

  test('Auto fit view after loading workflow', async ({ comfyPage }) => {
    await comfyPage.settings.setSetting(
      'Comfy.EnableWorkflowViewRestore',
      false
    )
    await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
    await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
  })
})

test.describe('Load duplicate workflow', () => {
  test('A workflow can be loaded multiple times in a row', async ({
    comfyPage
  }) => {
    await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
    await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
    await comfyPage.menu.workflowsTab.open()
    await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
    await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
    await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
  })
})

test.describe('Viewport settings', () => {
  test.beforeEach(async ({ comfyPage }) => {
    await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
    await comfyPage.settings.setSetting(
      'Comfy.Workflow.WorkflowTabsPosition',
      'Topbar'
    )

    await comfyPage.workflow.setupWorkflowsDirectory({})
  })

  test('Keeps viewport settings when changing tabs', async ({
    comfyPage,
    comfyMouse
  }) => {
    const changeTab = async (tab: Locator) => {
      await tab.click()
      await comfyPage.nextFrame()
      await comfyMouse.move(DefaultGraphPositions.emptySpace)

      // If tooltip is visible, wait for it to hide
      await expect(
        comfyPage.page.locator('.workflow-popover-fade')
      ).toHaveCount(0)
    }

    // Screenshot the canvas element
    await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)

    const toggleButton = comfyPage.page.getByTestId(
      TestIds.canvas.toggleMinimapButton
    )
    await toggleButton.click()
    await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false)

    await comfyPage.menu.topbar.saveWorkflow('Workflow A')
    await comfyPage.nextFrame()

    // Save workflow as a new file, then zoom out before screen shot
    await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')

    await comfyPage.nextFrame()
    const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
    await changeTab(tabA)

    const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')

    const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
    await changeTab(tabB)

    await comfyMouse.move(DefaultGraphPositions.emptySpace)
    for (let i = 0; i < 4; i++) {
      await comfyMouse.wheel(0, 60)
    }

    await comfyPage.nextFrame()
    const screenshotB = (await comfyPage.canvas.screenshot()).toString('base64')

    // Ensure that the screenshots are different due to zoom level
    expect(screenshotB).not.toBe(screenshotA)

    // Go back to Workflow A
    await changeTab(tabA)
    expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(
      screenshotA
    )

    // And back to Workflow B
    await changeTab(tabB)
    expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(
      screenshotB
    )
  })
})

test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
  test.describe('Legacy Mode', () => {
    test.beforeEach(async ({ comfyPage }) => {
      await comfyPage.settings.setSetting(
        'Comfy.Canvas.NavigationMode',
        'legacy'
      )
    })

    test('Left-click drag in empty area should pan canvas', async ({
      comfyPage
    }) => {
      await comfyPage.canvasOps.dragAndDrop(
        { x: 50, y: 50 },
        { x: 150, y: 150 }
      )
      await expect(comfyPage.canvas).toHaveScreenshot(
        'legacy-left-drag-pan.png'
      )
    })

    test('Middle-click drag should pan canvas', async ({ comfyPage }) => {
      await comfyPage.page.mouse.move(50, 50)
      await comfyPage.page.mouse.down({ button: 'middle' })
      await comfyPage.page.mouse.move(150, 150)
      await comfyPage.page.mouse.up({ button: 'middle' })
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'legacy-middle-drag-pan.png'
      )
    })

    test('Mouse wheel should zoom in/out', async ({ comfyPage }) => {
      await comfyPage.page.mouse.move(400, 300)
      await comfyPage.page.mouse.wheel(0, -120)
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'legacy-wheel-zoom-in.png'
      )

      await comfyPage.page.mouse.wheel(0, 240)
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'legacy-wheel-zoom-out.png'
      )
    })

    test('Left-click on node should not pan canvas', async ({ comfyPage }) => {
      await comfyPage.canvas.click({
        position: DefaultGraphPositions.textEncodeNode1
      })
      await comfyPage.nextFrame()
      await expect
        .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
        .toBe(1)
      await expect(comfyPage.canvas).toHaveScreenshot(
        'legacy-click-node-select.png'
      )
    })
  })

  test.describe('Standard Mode', () => {
    test.beforeEach(async ({ comfyPage }) => {
      await comfyPage.settings.setSetting(
        'Comfy.Canvas.NavigationMode',
        'standard'
      )
    })

    test('Left-click drag in empty area should select nodes', async ({
      comfyPage
    }) => {
      const clipNodes =
        await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
      const clipNode1Pos = await clipNodes[0].getPosition()
      const clipNode2Pos = await clipNodes[1].getPosition()
      const offset = 64

      await comfyPage.canvasOps.dragAndDrop(
        {
          x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
          y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
        },
        {
          x: Math.max(clipNode1Pos.x, clipNode2Pos.x) + offset,
          y: Math.max(clipNode1Pos.y, clipNode2Pos.y) + offset
        }
      )

      await expect
        .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
        .toBe(clipNodes.length)
      await expect(comfyPage.canvas).toHaveScreenshot(
        'standard-left-drag-select.png'
      )
    })

    test('Middle-click drag should pan canvas', async ({ comfyPage }) => {
      await comfyPage.page.mouse.move(50, 50)
      await comfyPage.page.mouse.down({ button: 'middle' })
      await comfyPage.page.mouse.move(150, 150)
      await comfyPage.page.mouse.up({ button: 'middle' })
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'standard-middle-drag-pan.png'
      )
    })

    test('Ctrl + mouse wheel should zoom in/out', async ({ comfyPage }) => {
      await comfyPage.page.mouse.move(400, 300)
      await comfyPage.page.keyboard.down('Control')
      await comfyPage.page.mouse.wheel(0, -120)
      await comfyPage.page.keyboard.up('Control')
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'standard-ctrl-wheel-zoom-in.png'
      )

      await comfyPage.page.keyboard.down('Control')
      await comfyPage.page.mouse.wheel(0, 240)
      await comfyPage.page.keyboard.up('Control')
      await comfyPage.expectScreenshot(
        comfyPage.canvas,
        'standard-ctrl-wheel-zoom-out.png'
      )
    })

    test('Left-click on node should select node (not start selection box)', async ({
      comfyPage
    }) => {
      await comfyPage.canvas.click({
        position: DefaultGraphPositions.textEncodeNode1
      })
      await comfyPage.nextFrame()
      await expect
        .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
        .toBe(1)
      await expect(comfyPage.canvas).toHaveScreenshot(
        'standard-click-node-select.png'
      )
    })

    test('Space + left-click drag should pan canvas', async ({ comfyPage }) => {
      // Click canvas to focus it
      await comfyPage.canvas.click()
      await comfyPage.nextFrame()

      await comfyPage.page.keyboard.down('Space')
      await comfyPage.canvasOps.dragAndDrop(
        { x: 50, y: 50 },
        { x: 150, y: 150 }
      )
      await comfyPage.page.keyboard.up('Space')
      await expect(comfyPage.canvas).toHaveScreenshot(
        'standard-space-drag-pan.png'
      )
    })

    test('Space key overrides default left-click behavior', async ({
      comfyPage
    }) => {
      const clipNodes =
        await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
      const clipNode1Pos = await clipNodes[0].getPosition()
      const offset = 64

      await comfyPage.canvasOps.dragAndDrop(
        {
          x: clipNode1Pos.x - offset,
          y: clipNode1Pos.y - offset
        },
        {
          x: clipNode1Pos.x + offset,
          y: clipNode1Pos.y + offset
        }
      )

      await expect
        .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
        .toBeGreaterThan(0)

      await comfyPage.canvasOps.clickEmptySpace()
      await expect
        .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
        .toBe(0)

      await comfyPage.page.keyboard.down('Space')
      await comfyPage.canvasOps.dragAndDrop(
        {
          x: clipNode1Pos.x - offset,
          y: clipNode1Pos.y - offset
        },
        {
          x: clipNode1Pos.x + offset,
          y: clipNode1Pos.y + offset
        }
      )
      await comfyPage.page.keyboard.up('Space')

      await expect
        .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
        .toBe(0)
    })
  })

  test('Shift + mouse wheel should pan canvas horizontally', async ({
    comfyPage
  }) => {
    await comfyPage.settings.setSetting(
      'Comfy.Canvas.MouseWheelScroll',
      'panning'
    )

    await comfyPage.canvas.click()
    await comfyPage.expectScreenshot(comfyPage.canvas, 'standard-initial.png')

    await comfyPage.page.mouse.move(400, 300)

    await comfyPage.page.keyboard.down('Shift')
    await comfyPage.page.mouse.wheel(0, 120)
    await comfyPage.page.keyboard.up('Shift')
    await comfyPage.expectScreenshot(
      comfyPage.canvas,
      'standard-shift-wheel-pan-right.png'
    )

    await comfyPage.page.keyboard.down('Shift')
    await comfyPage.page.mouse.wheel(0, -240)
    await comfyPage.page.keyboard.up('Shift')
    await comfyPage.expectScreenshot(
      comfyPage.canvas,
      'standard-shift-wheel-pan-left.png'
    )

    await comfyPage.page.keyboard.down('Shift')
    await comfyPage.page.mouse.wheel(0, 120)
    await comfyPage.page.keyboard.up('Shift')
    await comfyPage.expectScreenshot(
      comfyPage.canvas,
      'standard-shift-wheel-pan-center.png'
    )
  })

  test.describe('Edge Cases', () => {
    test('Multiple modifier keys work correctly in legacy mode', async ({
      comfyPage
    }) => {
      await comfyPage.settings.setSetting(
        'Comfy.Canvas.NavigationMode',
        'legacy'
      )

      await comfyPage.page.keyboard.down('Alt')
      await comfyPage.page.keyboard.down('Shift')
      await comfyPage.canvasOps.dragAndDrop(
        { x: 50, y: 50 },
        { x: 150, y: 150 }
      )
      await comfyPage.page.keyboard.up('Shift')
      await comfyPage.page.keyboard.up('Alt')

      await expect(comfyPage.canvas).toHaveScreenshot(
        'legacy-alt-shift-drag.png'
      )
    })

    test('Cursor changes appropriately in different modes', async ({
      comfyPage
    }) => {
      const getCursorStyle = async () => {
        return await comfyPage.page.evaluate(() => {
          return (
            document.getElementById('graph-canvas')!.style.cursor || 'default'
          )
        })
      }

      await comfyPage.settings.setSetting(
        'Comfy.Canvas.NavigationMode',
        'legacy'
      )
      await comfyPage.page.mouse.move(50, 50)
      await comfyPage.page.mouse.down()
      await expect.poll(() => getCursorStyle()).toBe('grabbing')
      await comfyPage.page.mouse.up()
    })
  })
})
