import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'

import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'

const MULTI_BINDING_COMMAND = 'Comfy.Canvas.DeleteSelectedItems'
const SINGLE_BINDING_COMMAND = 'Comfy.SaveWorkflow'
const NO_BINDING_COMMAND = 'TestCommand.KeybindingPanelE2E.NoBinding'

async function searchKeybindings(page: Page, query: string) {
  await getKeybindingSearchInput(page).fill(query)
}

async function clearSearch(page: Page) {
  await getKeybindingSearchInput(page).clear()
}

function getKeybindingSearchInput(page: Page): Locator {
  return page.getByPlaceholder('Search Keybindings...')
}

function getCommandRow(page: Page, commandId: string): Locator {
  return page
    .locator('.keybinding-panel tr')
    .filter({ has: page.locator(`[title="${commandId}"]`) })
}

function getExpansionContent(page: Page, commandId: string): Locator {
  // PrimeVue renders the expansion row as the next sibling <tr> of the
  // expanded row. Scoping by sibling avoids matching unrelated expanded rows.
  return getCommandRow(page, commandId)
    .locator('xpath=following-sibling::tr[1]')
    .getByTestId('keybinding-expansion-content')
}

async function openContextMenu(page: Page, commandId: string) {
  const row = getCommandRow(page, commandId)
  await row.locator(`[title="${commandId}"]`).click({ button: 'right' })
  await expect(
    page.getByRole('menuitem', { name: /Change keybinding/i })
  ).toBeVisible()
}

function getKeybindingInput(page: Page): Locator {
  return getEditKeybindingDialog(page).locator('input[autofocus]')
}

function getEditKeybindingDialog(page: Page): Locator {
  return page.getByRole('dialog', { name: /Modify keybinding/i })
}

function getRemoveAllKeybindingsDialog(page: Page): Locator {
  return page.getByRole('dialog', { name: /Remove all keybindings/i })
}

function getResetAllKeybindingsDialog(page: Page): Locator {
  return page.getByRole('dialog', { name: /Reset all keybindings/i })
}

async function pressComboOnInput(page: Page, combo: string) {
  const input = getKeybindingInput(page)
  await expect(input).toBeFocused()
  await input.press(combo)
}

async function saveAndCloseKeybindingDialog(page: Page) {
  const dialog = getEditKeybindingDialog(page)
  await dialog.getByRole('button', { name: /Save/i }).click()
  await expect(dialog).toBeHidden()
}

async function cancelAndCloseDialog(page: Page) {
  const dialog = getEditKeybindingDialog(page)
  await dialog.getByRole('button', { name: /Cancel/i }).click()
  await expect(dialog).toBeHidden()
}

async function addKeybindingToRow(page: Page, row: Locator, combo: string) {
  await row.getByRole('button', { name: /Add new keybinding/i }).click()
  await pressComboOnInput(page, combo)
  await saveAndCloseKeybindingDialog(page)
}

test.beforeEach(async ({ comfyPage }) => {
  await registerNoBindingCommand(comfyPage)
  await comfyPage.settingDialog.open()
  await comfyPage.settingDialog.category('Keybinding').click()
})

test.afterEach(async ({ comfyPage }) => {
  await comfyPage.settings.setSetting('Comfy.Keybinding.NewBindings', [])
  await comfyPage.settings.setSetting('Comfy.Keybinding.UnsetBindings', [])
})

async function registerNoBindingCommand(comfyPage: ComfyPage) {
  await comfyPage.page.evaluate((commandId) => {
    const app = window.app!
    app.registerExtension({
      name: 'TestExtension.KeybindingPanelE2E',
      commands: [{ id: commandId, function: () => {} }]
    })
  }, NO_BINDING_COMMAND)
}

test.describe('Keybinding Panel', { tag: '@keyboard' }, () => {
  test.describe('Row Expansion', () => {
    test('Click on row with 2+ keybindings toggles expansion', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, MULTI_BINDING_COMMAND)
      const row = getCommandRow(page, MULTI_BINDING_COMMAND)
      await expect(row).toBeVisible()

      await row.locator(`[title="${MULTI_BINDING_COMMAND}"]`).click()

      const expansionContent = getExpansionContent(page, MULTI_BINDING_COMMAND)
      await expect(expansionContent).toBeVisible()

      await row.locator(`[title="${MULTI_BINDING_COMMAND}"]`).click()
      await expect(expansionContent).toBeHidden()
    })

    test('Click on row with 1 keybinding does not expand', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)
      await expect(row).toBeVisible()

      await row.locator(`[title="${SINGLE_BINDING_COMMAND}"]`).click()

      const expansionContent = getExpansionContent(page, SINGLE_BINDING_COMMAND)
      await expect(expansionContent).toBeHidden()
    })
  })

  test.describe('Double-Click', () => {
    test('Double-click row with 0 keybindings opens Add dialog', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, NO_BINDING_COMMAND)
      const row = getCommandRow(page, NO_BINDING_COMMAND)
      await expect(row).toBeVisible()

      await row.locator(`[title="${NO_BINDING_COMMAND}"]`).dblclick()

      const input = getKeybindingInput(page)
      await expect(input).toBeVisible()

      await cancelAndCloseDialog(page)
    })

    test('Double-click row with 1 keybinding opens Edit dialog', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)
      await expect(row).toBeVisible()

      await row.locator(`[title="${SINGLE_BINDING_COMMAND}"]`).dblclick()

      const input = getKeybindingInput(page)
      await expect(input).toBeVisible()

      await cancelAndCloseDialog(page)
    })
  })

  test.describe('Context Menu', () => {
    test('Right-click row shows context menu with correct items', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      await openContextMenu(page, SINGLE_BINDING_COMMAND)

      const changeItem = page.getByRole('menuitem', {
        name: /Change keybinding/i
      })
      const addItem = page.getByRole('menuitem', {
        name: /Add new keybinding/i
      })
      const resetItem = page.getByRole('menuitem', {
        name: /Reset to default/i
      })
      const removeItem = page.getByRole('menuitem', {
        name: /Remove keybinding/i
      })

      await expect(changeItem).toBeVisible()
      await expect(addItem).toBeVisible()
      await expect(resetItem).toBeVisible()
      await expect(removeItem).toBeVisible()

      await page.keyboard.press('Escape')
    })

    test("Context menu 'Add new keybinding' opens add dialog", async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      await openContextMenu(page, SINGLE_BINDING_COMMAND)

      await page.getByRole('menuitem', { name: /Add new keybinding/i }).click()

      const input = getKeybindingInput(page)
      await expect(input).toBeVisible()

      await cancelAndCloseDialog(page)
    })

    test("Context menu 'Change keybinding' on single-binding command opens edit dialog", async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      await openContextMenu(page, SINGLE_BINDING_COMMAND)

      await page.getByRole('menuitem', { name: /Change keybinding/i }).click()

      const input = getKeybindingInput(page)
      await expect(input).toBeVisible()

      await cancelAndCloseDialog(page)
    })

    test("Context menu 'Change keybinding' on multi-binding command expands row", async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, MULTI_BINDING_COMMAND)

      const expansionContent = getExpansionContent(page, MULTI_BINDING_COMMAND)
      await expect(expansionContent).toBeHidden()

      await openContextMenu(page, MULTI_BINDING_COMMAND)

      await page.getByRole('menuitem', { name: /Change keybinding/i }).click()

      await expect(expansionContent).toBeVisible()
    })

    test("Context menu 'Remove keybinding' after adding second binding shows confirm dialog", async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)

      await addKeybindingToRow(page, row, 'Control+Shift+F9')

      await openContextMenu(page, SINGLE_BINDING_COMMAND)
      await page.getByRole('menuitem', { name: /Remove keybinding/i }).click()

      const confirmDialog = getRemoveAllKeybindingsDialog(page)
      await expect(confirmDialog).toBeVisible()
      await confirmDialog.getByRole('button', { name: /Remove all/i }).click()

      await expect(row.locator('td').nth(1)).toContainText('-')
    })

    test("Context menu 'Reset to default' resets modified command", async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)

      await addKeybindingToRow(page, row, 'Control+Shift+F10')

      await openContextMenu(page, SINGLE_BINDING_COMMAND)
      await page.getByRole('menuitem', { name: /Reset to default/i }).click()

      await expect(row.getByRole('button', { name: /Reset/i })).toBeDisabled()
    })

    test('Context menu items disabled when no keybindings', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, NO_BINDING_COMMAND)
      await openContextMenu(page, NO_BINDING_COMMAND)

      const changeItem = page.getByRole('menuitem', {
        name: /Change keybinding/i
      })
      const removeItem = page.getByRole('menuitem', {
        name: /Remove keybinding/i
      })

      await expect(changeItem).toHaveAttribute('data-disabled', '')
      await expect(removeItem).toHaveAttribute('data-disabled', '')

      await page.keyboard.press('Escape')
    })
  })

  test.describe('Action Buttons', () => {
    test('Edit button opens edit dialog for single-binding command', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)

      const editButton = row.getByRole('button', { name: /^Edit$/i })
      await expect(editButton).toBeVisible()
      await editButton.click()

      const input = getKeybindingInput(page)
      await expect(input).toBeVisible()

      await cancelAndCloseDialog(page)
    })

    test('Add button opens add dialog', async ({ comfyPage }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)

      await row.getByRole('button', { name: /Add new keybinding/i }).click()

      const input = getKeybindingInput(page)
      await expect(input).toBeVisible()

      await cancelAndCloseDialog(page)
    })

    test('Reset button is disabled for unmodified commands', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)

      const resetButton = row.getByRole('button', { name: /Reset/i })
      await expect(resetButton).toBeDisabled()
    })

    test('Reset button resets modified keybinding', async ({ comfyPage }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)

      await addKeybindingToRow(page, row, 'Control+Shift+F11')

      const resetButton = row.getByRole('button', { name: /Reset/i })
      await expect(resetButton).toBeEnabled()

      await resetButton.click()

      await expect(resetButton).toBeDisabled()
    })

    test('Delete button is disabled for commands with 0 keybindings', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, NO_BINDING_COMMAND)
      const row = getCommandRow(page, NO_BINDING_COMMAND)

      const deleteButton = row.getByRole('button', { name: /Delete/i })
      await expect(deleteButton).toBeDisabled()
    })

    test('Delete button removes single keybinding directly', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, NO_BINDING_COMMAND)
      const row = getCommandRow(page, NO_BINDING_COMMAND)

      await addKeybindingToRow(page, row, 'Control+Shift+F12')

      const deleteButton = row.getByRole('button', { name: /Delete/i })
      await expect(deleteButton).toBeEnabled()
      await deleteButton.click()

      await expect(row.locator('td').nth(1)).toContainText('-')
    })

    test('Delete button on command with 2+ keybindings shows confirm dialog', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, MULTI_BINDING_COMMAND)
      const row = getCommandRow(page, MULTI_BINDING_COMMAND)

      const deleteButton = row.getByRole('button', { name: /Delete/i })
      await deleteButton.click()

      const confirmDialog = getRemoveAllKeybindingsDialog(page)
      await expect(confirmDialog).toBeVisible()

      await confirmDialog.getByRole('button', { name: /Cancel/i }).click()
      await expect(confirmDialog).toBeHidden()
      await expect(row.locator('td').nth(1)).not.toContainText('-')
    })
  })

  test.describe('Expanded Row Actions', () => {
    test('Edit button in expanded row opens edit dialog for that binding', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, MULTI_BINDING_COMMAND)

      await page.locator(`[title="${MULTI_BINDING_COMMAND}"]`).click()
      const expansionContent = getExpansionContent(page, MULTI_BINDING_COMMAND)
      await expect(expansionContent).toBeVisible()

      const firstBindingRow = expansionContent
        .getByTestId('keybinding-expansion-binding')
        .first()
      await firstBindingRow.getByRole('button', { name: /^Edit$/i }).click()

      const input = getKeybindingInput(page)
      await expect(input).toBeVisible()

      await cancelAndCloseDialog(page)
    })

    test('Delete button in expanded row removes that binding and collapses', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, MULTI_BINDING_COMMAND)

      await page.locator(`[title="${MULTI_BINDING_COMMAND}"]`).click()
      const expansionContent = getExpansionContent(page, MULTI_BINDING_COMMAND)
      await expect(expansionContent).toBeVisible()

      const bindingRows = expansionContent.getByTestId(
        'keybinding-expansion-binding'
      )
      await expect
        .poll(() => bindingRows.count(), {
          message: 'Expected at least 2 bindings'
        })
        .toBeGreaterThanOrEqual(2)
      const initialBindingCount = await bindingRows.count()

      await bindingRows
        .first()
        .getByRole('button', { name: /Remove keybinding/i })
        .click()

      if (initialBindingCount === 2) {
        // Expansion auto-collapses when bindings drop below 2
        await expect(expansionContent).toBeHidden()
      } else {
        await expect(bindingRows).toHaveCount(initialBindingCount - 1)
      }
    })
  })

  test.describe('Reset All', () => {
    test('Reset All button shows confirmation and resets on confirm', async ({
      comfyPage
    }) => {
      const { page } = comfyPage

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const row = getCommandRow(page, SINGLE_BINDING_COMMAND)
      await addKeybindingToRow(page, row, 'Control+Shift+F8')

      await expect(row.getByRole('button', { name: /Reset/i })).toBeEnabled()

      await clearSearch(page)

      const resetAllButton = page
        .locator('.keybinding-panel')
        .getByRole('button', { name: /Reset All/i })
      await resetAllButton.click()

      const confirmDialog = getResetAllKeybindingsDialog(page)
      await expect(confirmDialog).toBeVisible()
      await expect(confirmDialog).toContainText(/Reset all keybindings/i)

      await confirmDialog.getByRole('button', { name: /Reset All/i }).click()

      await expect(comfyPage.toast.visibleToasts).toHaveCount(1)

      await searchKeybindings(page, SINGLE_BINDING_COMMAND)
      const rowAfterReset = getCommandRow(page, SINGLE_BINDING_COMMAND)
      await expect(
        rowAfterReset.getByRole('button', { name: /Reset/i })
      ).toBeDisabled()
    })

    test('Reset All confirmation can be cancelled', async ({ comfyPage }) => {
      const { page } = comfyPage

      const resetAllButton = page
        .locator('.keybinding-panel')
        .getByRole('button', { name: /Reset All/i })
      await resetAllButton.click()

      const confirmDialog = getResetAllKeybindingsDialog(page)
      await expect(confirmDialog).toBeVisible()
      await confirmDialog.getByRole('button', { name: /Cancel/i }).click()

      await expect(confirmDialog).toBeHidden()
    })
  })

  test.describe('Search Filter', () => {
    test('Typing in search clears expanded rows', async ({ comfyPage }) => {
      const { page } = comfyPage

      await searchKeybindings(page, MULTI_BINDING_COMMAND)

      await page.locator(`[title="${MULTI_BINDING_COMMAND}"]`).click()
      const expansionContent = getExpansionContent(page, MULTI_BINDING_COMMAND)
      await expect(expansionContent).toBeVisible()

      // Changing the filter triggers watch(filters, ...) which clears expansion
      await searchKeybindings(page, MULTI_BINDING_COMMAND + ' ')
      await expect(expansionContent).toBeHidden()
    })
  })
})
