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 { TestIds } from '@e2e/fixtures/selectors'

const BYPASS_CLASS = /before:bg-bypass\/60/

async function clickExactMenuItem(comfyPage: ComfyPage, name: string) {
  await comfyPage.contextMenu.clickMenuItemExact(name)
  await expect(comfyPage.contextMenu.primeVueMenu).toBeHidden()
}

async function openContextMenu(comfyPage: ComfyPage, nodeTitle: string) {
  const fixture = await comfyPage.vueNodes.getFixtureByTitle(nodeTitle)
  await comfyPage.contextMenu.openForVueNode(fixture.header)
  return comfyPage.contextMenu.primeVueMenu
}

async function openMultiNodeContextMenu(
  comfyPage: ComfyPage,
  titles: string[]
) {
  // deselectAll via evaluate — clearSelection() clicks at a fixed position
  // which can hit nodes or the toolbar overlay
  await comfyPage.page.evaluate(() => window.app!.canvas.deselectAll())
  await comfyPage.nextFrame()

  for (const title of titles) {
    const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
    await fixture.header.click({ modifiers: ['ControlOrMeta'] })
  }
  await comfyPage.nextFrame()

  const firstFixture = await comfyPage.vueNodes.getFixtureByTitle(titles[0])
  const box = await firstFixture.header.boundingBox()
  if (!box) throw new Error(`Header for "${titles[0]}" not found`)
  await comfyPage.page.mouse.click(
    box.x + box.width / 2,
    box.y + box.height / 2,
    { button: 'right' }
  )

  const menu = comfyPage.contextMenu.primeVueMenu
  await menu.waitFor({ state: 'visible' })
  return menu
}

function getNodeWrapper(comfyPage: ComfyPage, nodeTitle: string): Locator {
  return comfyPage.vueNodes
    .getNodeByTitle(nodeTitle)
    .getByTestId(TestIds.node.innerWrapper)
}

async function getNodeRef(comfyPage: ComfyPage, nodeTitle: string) {
  const refs = await comfyPage.nodeOps.getNodeRefsByTitle(nodeTitle)
  return refs[0]
}

test.describe('Vue Node Context Menu', { tag: '@vue-nodes' }, () => {
  test.describe('Single Node Actions', () => {
    test('should rename node via context menu', async ({ comfyPage }) => {
      await openContextMenu(comfyPage, 'KSampler')
      await clickExactMenuItem(comfyPage, 'Rename')

      await comfyPage.titleEditor.expectVisible()
      await comfyPage.titleEditor.setTitle('My Renamed Sampler')
      await comfyPage.nextFrame()

      const renamedNode =
        comfyPage.vueNodes.getNodeByTitle('My Renamed Sampler')
      await expect(renamedNode).toBeVisible()
    })

    test('should open node info in the right side panel via context menu', async ({
      comfyPage
    }) => {
      await comfyPage.settings.setSetting('Comfy.RightSidePanel.IsOpen', false)
      await expect(comfyPage.menu.propertiesPanel.root).toBeHidden()

      await openContextMenu(comfyPage, 'KSampler')
      await clickExactMenuItem(comfyPage, 'Node Info')

      const panel = comfyPage.menu.propertiesPanel.root
      await expect(panel).toBeVisible()
      await expect(panel.getByTestId('panel-tab-info')).toHaveAttribute(
        'aria-selected',
        'true'
      )
      await expect(comfyPage.menu.nodeLibraryTab.selectedTabButton).toBeHidden()
    })

    test('should copy and paste node via context menu', async ({
      comfyPage
    }) => {
      const initialCount = await comfyPage.nodeOps.getGraphNodesCount()

      await openContextMenu(comfyPage, 'Load Checkpoint')
      await clickExactMenuItem(comfyPage, 'Copy')

      // Internal clipboard paste (menu Copy uses canvas clipboard, not OS)
      await comfyPage.page.evaluate(() => {
        window.app!.canvas.pasteFromClipboard({ connectInputs: false })
      })
      await comfyPage.nextFrame()

      await expect
        .poll(() => comfyPage.nodeOps.getGraphNodesCount())
        .toBe(initialCount + 1)
    })

    test('should duplicate node via context menu', async ({ comfyPage }) => {
      const initialCount = await comfyPage.nodeOps.getGraphNodesCount()

      await openContextMenu(comfyPage, 'Load Checkpoint')
      await clickExactMenuItem(comfyPage, 'Duplicate')

      await expect
        .poll(() => comfyPage.nodeOps.getGraphNodesCount())
        .toBe(initialCount + 1)
    })

    test('should pin and unpin node via context menu', async ({
      comfyPage
    }) => {
      const nodeTitle = 'Load Checkpoint'
      const nodeRef = await getNodeRef(comfyPage, nodeTitle)

      // Pin via context menu
      await openContextMenu(comfyPage, nodeTitle)
      await clickExactMenuItem(comfyPage, 'Pin')

      const fixture = await comfyPage.vueNodes.getFixtureByTitle(nodeTitle)
      await expect(fixture.pinIndicator).toBeVisible()
      await expect.poll(() => nodeRef.isPinned()).toBe(true)

      // Verify drag blocked
      const header = fixture.header
      const posBeforeDrag = await header.boundingBox()
      if (!posBeforeDrag) throw new Error('Header not found')
      await comfyPage.canvasOps.dragAndDrop(
        { x: posBeforeDrag.x + 10, y: posBeforeDrag.y + 10 },
        { x: posBeforeDrag.x + 256, y: posBeforeDrag.y + 256 }
      )
      await expect
        .poll(async () => await header.boundingBox())
        .toEqual(posBeforeDrag)

      // Unpin via context menu
      await openContextMenu(comfyPage, nodeTitle)
      await clickExactMenuItem(comfyPage, 'Unpin')

      await expect(fixture.pinIndicator).toBeHidden()
      await expect.poll(() => nodeRef.isPinned()).toBe(false)
    })

    test('should bypass node and remove bypass via context menu', async ({
      comfyPage
    }) => {
      const nodeTitle = 'Load Checkpoint'
      const nodeRef = await getNodeRef(comfyPage, nodeTitle)

      await openContextMenu(comfyPage, nodeTitle)
      await clickExactMenuItem(comfyPage, 'Bypass')

      await expect(nodeRef).toBeBypassed()
      await expect(getNodeWrapper(comfyPage, nodeTitle)).toHaveClass(
        BYPASS_CLASS
      )

      await openContextMenu(comfyPage, nodeTitle)
      await clickExactMenuItem(comfyPage, 'Remove Bypass')

      await expect(nodeRef).not.toBeBypassed()
      await expect(getNodeWrapper(comfyPage, nodeTitle)).not.toHaveClass(
        BYPASS_CLASS
      )
    })

    test('shows exactly one bypass menu item per state (FE-720 regression)', async ({
      comfyPage
    }) => {
      const nodeTitle = 'Load Checkpoint'
      const nodeRef = await getNodeRef(comfyPage, nodeTitle)
      const bypassItem = comfyPage.contextMenu.menuItem('Bypass')
      const removeBypassItem = comfyPage.contextMenu.menuItem('Remove Bypass')

      await openContextMenu(comfyPage, nodeTitle)
      await expect(bypassItem).toHaveCount(1)
      await expect(removeBypassItem).toHaveCount(0)
      await clickExactMenuItem(comfyPage, 'Bypass')
      await expect(nodeRef).toBeBypassed()

      await openContextMenu(comfyPage, nodeTitle)
      await expect(removeBypassItem).toHaveCount(1)
      await expect(bypassItem).toHaveCount(0)
      await clickExactMenuItem(comfyPage, 'Remove Bypass')
      await expect(nodeRef).not.toBeBypassed()
    })

    test('should minimize and expand node via context menu', async ({
      comfyPage
    }) => {
      const fixture = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
      await expect(fixture.body).toBeVisible()

      await openContextMenu(comfyPage, 'KSampler')
      await clickExactMenuItem(comfyPage, 'Minimize Node')
      await expect(fixture.body).toBeHidden()

      await openContextMenu(comfyPage, 'KSampler')
      await clickExactMenuItem(comfyPage, 'Expand Node')
      await expect(fixture.body).toBeVisible()
    })

    test('should convert node to subgraph via context menu', async ({
      comfyPage
    }) => {
      await openContextMenu(comfyPage, 'KSampler')
      await clickExactMenuItem(comfyPage, 'Convert to Subgraph')

      const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
      await expect(subgraphNode).toBeVisible()

      await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
    })
  })

  test.describe('Image Node Actions', () => {
    test.beforeEach(async ({ comfyPage }) => {
      await comfyPage.page
        .context()
        .grantPermissions(['clipboard-read', 'clipboard-write'])
      await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
      await comfyPage.vueNodes.waitForNodes(1)
      await comfyPage.page
        .locator('[data-node-id] img')
        .first()
        .waitFor({ state: 'visible' })

      const [loadImageNode] =
        await comfyPage.nodeOps.getNodeRefsByTitle('Load Image')
      if (!loadImageNode) throw new Error('Load Image node not found')

      await expect
        .poll(() =>
          comfyPage.page.evaluate(
            (nodeId) =>
              window.app!.graph.getNodeById(nodeId)?.imgs?.length ?? 0,
            loadImageNode.id
          )
        )
        .toBeGreaterThan(0)
    })

    test('should copy image to clipboard via context menu', async ({
      comfyPage
    }) => {
      await openContextMenu(comfyPage, 'Load Image')
      await clickExactMenuItem(comfyPage, 'Copy Image')

      // Verify the clipboard contains an image
      await expect
        .poll(async () => {
          return comfyPage.page.evaluate(async () => {
            const items = await navigator.clipboard.read()
            return items.some((item) =>
              item.types.some((t) => t.startsWith('image/'))
            )
          })
        })
        .toBe(true)
    })

    test('should paste image to LoadImage node via context menu', async ({
      comfyPage
    }) => {
      // Capture the original image src from the node's preview
      const imagePreview = comfyPage.vueNodes
        .getNodeByTitle('Load Image')
        .getByTestId(TestIds.node.mainImage)
      const originalSrc = await imagePreview.getAttribute('src')

      // Write a test image into the browser clipboard
      await comfyPage.page.evaluate(async () => {
        const resp = await fetch('/api/view?filename=example.png&type=input')
        const blob = await resp.blob()
        await navigator.clipboard.write([
          new ClipboardItem({ [blob.type]: blob })
        ])
      })

      // Right-click and select Paste Image
      await openContextMenu(comfyPage, 'Load Image')
      await clickExactMenuItem(comfyPage, 'Paste Image')

      // Verify the image preview src changed
      await expect(imagePreview).not.toHaveAttribute('src', originalSrc!)
    })

    test('should open image in new tab via context menu', async ({
      comfyPage
    }) => {
      await openContextMenu(comfyPage, 'Load Image')

      const popupPromise = comfyPage.page.waitForEvent('popup')
      await clickExactMenuItem(comfyPage, 'Open Image')
      const popup = await popupPromise

      expect(popup.url()).toContain('/api/view')
      expect(popup.url()).toContain('filename=')
      await popup.close()
    })

    test('should download image via Save Image context menu', async ({
      comfyPage
    }) => {
      await openContextMenu(comfyPage, 'Load Image')

      const downloadPromise = comfyPage.page.waitForEvent('download')
      await clickExactMenuItem(comfyPage, 'Save Image')
      const download = await downloadPromise

      expect(download.suggestedFilename()).toBeTruthy()
    })
  })

  test.describe('Subgraph Actions', () => {
    test('should convert to subgraph and unpack back', async ({
      comfyPage
    }) => {
      // Convert KSampler to subgraph
      await openContextMenu(comfyPage, 'KSampler')
      await clickExactMenuItem(comfyPage, 'Convert to Subgraph')

      const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
      await expect(subgraphNode).toBeVisible()
      await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()

      // Unpack the subgraph
      await openContextMenu(comfyPage, 'New Subgraph')
      await clickExactMenuItem(comfyPage, 'Unpack Subgraph')

      await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeVisible()
      await expect(
        comfyPage.vueNodes.getNodeByTitle('New Subgraph')
      ).toBeHidden()
    })

    test('should open properties panel via Edit Subgraph Widgets', async ({
      comfyPage
    }) => {
      // Convert to subgraph first
      await openContextMenu(comfyPage, 'Empty Latent Image')
      await clickExactMenuItem(comfyPage, 'Convert to Subgraph')
      await expect(
        comfyPage.vueNodes.getNodeByTitle('New Subgraph')
      ).toBeVisible()

      // Right-click subgraph and edit widgets
      await openContextMenu(comfyPage, 'New Subgraph')
      await clickExactMenuItem(comfyPage, 'Edit Subgraph Widgets')

      await expect(comfyPage.page.getByTestId('properties-panel')).toBeVisible()
    })

    test('should add subgraph to library and find in node library', async ({
      comfyPage
    }) => {
      // Convert to subgraph first
      await openContextMenu(comfyPage, 'KSampler')
      await clickExactMenuItem(comfyPage, 'Convert to Subgraph')
      await expect(
        comfyPage.vueNodes.getNodeByTitle('New Subgraph')
      ).toBeVisible()

      // Add to library
      await openContextMenu(comfyPage, 'New Subgraph')
      await clickExactMenuItem(comfyPage, 'Add Subgraph to Library')

      // Fill the blueprint name
      await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'visible' })
      await comfyPage.nodeOps.fillPromptDialog('TestBlueprint')

      // Open node library sidebar and search for the blueprint
      await comfyPage.menu.nodeLibraryTab.tabButton.click()
      const searchBox = comfyPage.page.getByRole('combobox', {
        name: 'Search'
      })
      await searchBox.waitFor({ state: 'visible' })
      await searchBox.fill('TestBlueprint')

      await expect(comfyPage.page.getByText('TestBlueprint')).toBeVisible()
    })
  })

  test.describe('Multi-Node Actions', () => {
    const nodeTitles = ['Load Checkpoint', 'KSampler']

    test('should batch rename selected nodes via context menu', async ({
      comfyPage
    }) => {
      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Rename')

      await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'visible' })
      await comfyPage.nodeOps.fillPromptDialog('MyNode')

      await expect(comfyPage.vueNodes.getNodeByTitle('MyNode 1')).toBeVisible()
      await expect(comfyPage.vueNodes.getNodeByTitle('MyNode 2')).toBeVisible()
    })

    test('should copy and paste selected nodes via context menu', async ({
      comfyPage
    }) => {
      const initialCount = await comfyPage.nodeOps.getGraphNodesCount()

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Copy')

      await comfyPage.page.evaluate(() => {
        window.app!.canvas.pasteFromClipboard({ connectInputs: false })
      })
      await comfyPage.nextFrame()

      await expect
        .poll(() => comfyPage.nodeOps.getGraphNodesCount())
        .toBe(initialCount + nodeTitles.length)
    })

    test('should duplicate selected nodes via context menu', async ({
      comfyPage
    }) => {
      const initialCount = await comfyPage.nodeOps.getGraphNodesCount()

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Duplicate')

      await expect
        .poll(() => comfyPage.nodeOps.getGraphNodesCount())
        .toBe(initialCount + nodeTitles.length)
    })

    test('should pin and unpin selected nodes via context menu', async ({
      comfyPage
    }) => {
      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Pin')

      for (const title of nodeTitles) {
        const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
        await expect(fixture.pinIndicator).toBeVisible()
      }

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Unpin')

      for (const title of nodeTitles) {
        const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
        await expect(fixture.pinIndicator).toBeHidden()
      }
    })

    test('should bypass and remove bypass on selected nodes via context menu', async ({
      comfyPage
    }) => {
      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Bypass')

      for (const title of nodeTitles) {
        const nodeRef = await getNodeRef(comfyPage, title)
        await expect(nodeRef).toBeBypassed()
        await expect(getNodeWrapper(comfyPage, title)).toHaveClass(BYPASS_CLASS)
      }

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Remove Bypass')

      for (const title of nodeTitles) {
        const nodeRef = await getNodeRef(comfyPage, title)
        await expect(nodeRef).not.toBeBypassed()
        await expect(getNodeWrapper(comfyPage, title)).not.toHaveClass(
          BYPASS_CLASS
        )
      }
    })

    test('should minimize and expand selected nodes via context menu', async ({
      comfyPage
    }) => {
      const fixture1 =
        await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
      const fixture2 = await comfyPage.vueNodes.getFixtureByTitle('KSampler')

      await expect(fixture1.body).toBeVisible()
      await expect(fixture2.body).toBeVisible()

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Minimize Node')

      await expect(fixture1.body).toBeHidden()
      await expect(fixture2.body).toBeHidden()

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Expand Node')

      await expect(fixture1.body).toBeVisible()
      await expect(fixture2.body).toBeVisible()
    })

    test('should frame selected nodes via context menu', async ({
      comfyPage
    }) => {
      const initialGroupCount = await comfyPage.page.evaluate(
        () => window.app!.graph.groups.length
      )

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Frame Nodes')

      await expect
        .poll(() =>
          comfyPage.page.evaluate(() => window.app!.graph.groups.length)
        )
        .toBe(initialGroupCount + 1)
    })

    test('should convert selected nodes to subgraph via context menu', async ({
      comfyPage
    }) => {
      const initialCount = await comfyPage.nodeOps.getGraphNodesCount()

      await openMultiNodeContextMenu(comfyPage, nodeTitles)
      await clickExactMenuItem(comfyPage, 'Convert to Subgraph')

      const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
      await expect(subgraphNode).toBeVisible()

      await expect
        .poll(() => comfyPage.nodeOps.getGraphNodesCount())
        .toBe(initialCount - nodeTitles.length + 1)
    })
  })
})
