import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { act, render, screen, fireEvent, waitFor } from '@testing-library/react'
import type { VaultEntry } from '../types'
import { queueAiPrompt, requestOpenAiChat } from '../utils/aiPromptBridge'
import { readSelectionRange } from './inlineWikilinkDom'
import { CommandPalette } from './CommandPalette'
import type { CommandAction } from '../hooks/useCommandRegistry'

type NativeDropPayload = {
  type: string
  paths: string[]
  position: { x: number; y: number }
}
type NativeDropHandler = (event: { payload: NativeDropPayload }) => void
const nativeDropState = vi.hoisted(() => ({
  tauriMode: false,
  handlers: {} as Record<string, NativeDropHandler[] | undefined>,
}))

// jsdom doesn't implement scrollIntoView
Element.prototype.scrollIntoView = vi.fn()

vi.mock('../mock-tauri', () => ({
  isTauri: () => nativeDropState.tauriMode,
}))

vi.mock('@tauri-apps/api/window', () => ({
  getCurrentWindow: () => ({
    onDragDropEvent: vi.fn((handler: NativeDropHandler) => {
      nativeDropState.handlers['native-drag-drop'] = [
        ...(nativeDropState.handlers['native-drag-drop'] ?? []),
        handler,
      ]
      return Promise.resolve(() => {
        const handlers = nativeDropState.handlers['native-drag-drop']?.filter((candidate) => candidate !== handler) ?? []
        if (handlers.length > 0) nativeDropState.handlers['native-drag-drop'] = handlers
        else delete nativeDropState.handlers['native-drag-drop']
      })
    }),
  }),
}))

vi.mock('../utils/aiPromptBridge', () => ({
  queueAiPrompt: vi.fn(),
  requestOpenAiChat: vi.fn(),
}))

const makeCommand = (overrides: Partial<CommandAction> = {}): CommandAction => ({
  id: 'test-cmd',
  label: 'Test Command',
  group: 'Navigation',
  keywords: [],
  enabled: true,
  shortcut: undefined,
  execute: vi.fn(),
  ...overrides,
})

const commands: CommandAction[] = [
  makeCommand({ id: 'search-notes', label: 'Search Notes', group: 'Navigation', shortcut: '⌘P', keywords: ['find'] }),
  makeCommand({ id: 'create-note', label: 'New Note', group: 'Note', shortcut: '⌘N' }),
  makeCommand({ id: 'commit-push', label: 'Commit & Push', group: 'Git', keywords: ['git', 'sync'] }),
  makeCommand({ id: 'open-settings', label: 'Open Settings', group: 'Settings', shortcut: '⌘,' }),
  makeCommand({ id: 'disabled-cmd', label: 'Disabled Command', group: 'Note', enabled: false }),
]

const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
  path: '/vault/note/test.md',
  filename: 'test.md',
  title: 'Test Note',
  isA: 'Note',
  aliases: [],
  belongsTo: [],
  relatedTo: [],
  status: null,
  owner: null,
  cadence: null,
  archived: false,
  modifiedAt: 1700000000,
  createdAt: 1700000000,
  fileSize: 100,
  snippet: '',
  wordCount: 0,
  relationships: {},
  icon: null,
  color: null,
  order: null,
  outgoingLinks: [],
  ...overrides,
})

const entries: VaultEntry[] = [
  makeEntry({ path: '/vault/alpha.md', filename: 'alpha.md', title: 'Alpha', isA: 'Project' }),
]

function setSelection(editor: HTMLElement, offset: number) {
  const selection = window.getSelection()
  if (!selection) return

  const targetNode = editor.firstChild ?? editor
  const safeOffset = targetNode.nodeType === Node.TEXT_NODE
    ? Math.min(offset, targetNode.textContent?.length ?? 0)
    : Math.min(offset, targetNode.childNodes.length)

  const range = document.createRange()
  range.setStart(targetNode, safeOffset)
  range.collapse(true)
  selection.removeAllRanges()
  selection.addRange(range)
}

function updateAiInput(text: string) {
  const editor = screen.getByTestId('command-palette-ai-input')
  editor.textContent = text
  setSelection(editor, text.length)
  fireEvent.input(editor)
  return screen.queryByTestId('command-palette-ai-input') ?? editor
}

function resetNativeDropState() {
  nativeDropState.tauriMode = false
  for (const eventName of Object.keys(nativeDropState.handlers)) {
    delete nativeDropState.handlers[eventName]
  }
}

function mockElementRect(element: HTMLElement) {
  Object.defineProperty(element, 'getBoundingClientRect', {
    configurable: true,
    value: () => ({
      x: 0,
      y: 0,
      left: 0,
      top: 0,
      right: 400,
      bottom: 48,
      width: 400,
      height: 48,
      toJSON: () => ({}),
    }),
  })
}

function emitNativePathDrop(paths: string[]) {
  const handlers = nativeDropState.handlers['native-drag-drop']
  if (!handlers || handlers.length === 0) throw new Error('No native drop handler registered')
  for (const handler of handlers) {
    handler({
      payload: {
        type: 'drop',
        paths,
        position: { x: 20, y: 20 },
      },
    })
  }
}

async function waitForNativePathDropListener() {
  await waitFor(() => {
    expect(nativeDropState.handlers['native-drag-drop']?.length).toBeGreaterThan(0)
  })
}

describe('CommandPalette', () => {
  const onClose = vi.fn()

  beforeEach(() => {
    vi.clearAllMocks()
    resetNativeDropState()
  })

  afterEach(resetNativeDropState)

  it('renders nothing when closed', () => {
    const { container } = render(
      <CommandPalette open={false} commands={commands} onClose={onClose} />,
    )
    expect(container.firstChild).toBeNull()
  })

  it('shows search input when open', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    expect(screen.getByPlaceholderText('Type a command...')).toBeInTheDocument()
  })

  it('opts the command input out of spellcheck without disabling IME autocorrection', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    const input = screen.getByPlaceholderText('Type a command...')

    expect(input).toHaveAttribute('spellcheck', 'false')
    expect(input).toHaveAttribute('autocomplete', 'off')
    expect(input).not.toHaveAttribute('autocorrect')
    expect(input).not.toHaveAttribute('autocapitalize')
  })

  it('shows all enabled commands grouped by category', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    expect(screen.getByText('Search Notes')).toBeInTheDocument()
    expect(screen.getByText('New Note')).toBeInTheDocument()
    expect(screen.getByText('Commit & Push')).toBeInTheDocument()
    expect(screen.getByText('Open Settings')).toBeInTheDocument()
    // Disabled command should not appear
    expect(screen.queryByText('Disabled Command')).not.toBeInTheDocument()
  })

  it('shows group labels', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    expect(screen.getByText('Navigation')).toBeInTheDocument()
    expect(screen.getByText('Note')).toBeInTheDocument()
    expect(screen.getByText('Git')).toBeInTheDocument()
    expect(screen.getByText('Settings')).toBeInTheDocument()
  })

  it('shows keyboard shortcuts', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    expect(screen.getByText('⌘P')).toBeInTheDocument()
    expect(screen.getByText('⌘N')).toBeInTheDocument()
    expect(screen.getByText('⌘,')).toBeInTheDocument()
  })

  it('filters commands by fuzzy search', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    const input = screen.getByPlaceholderText('Type a command...')
    fireEvent.change(input, { target: { value: 'commit' } })

    expect(screen.getByText('Commit & Push')).toBeInTheDocument()
    expect(screen.queryByText('Search Notes')).not.toBeInTheDocument()
  })

  it('matches by keyword', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    const input = screen.getByPlaceholderText('Type a command...')
    fireEvent.change(input, { target: { value: 'find' } })

    expect(screen.getByText('Search Notes')).toBeInTheDocument()
  })

  it('shows "No matching commands" when no results', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    const input = screen.getByPlaceholderText('Type a command...')
    fireEvent.change(input, { target: { value: 'zzzzzzz' } })

    expect(screen.getByText('No matching commands')).toBeInTheDocument()
  })

  it('localizes command palette chrome', () => {
    render(<CommandPalette open={true} commands={commands} locale="zh-CN" onClose={onClose} />)
    const input = screen.getByPlaceholderText('输入命令...')
    fireEvent.change(input, { target: { value: 'zzzzzzz' } })

    expect(screen.getByText('没有匹配的命令')).toBeInTheDocument()
    expect(screen.getByText('↑↓ 导航')).toBeInTheDocument()
  })

  it('calls onClose when pressing Escape', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    fireEvent.keyDown(window, { key: 'Escape' })
    expect(onClose).toHaveBeenCalled()
  })

  it('executes command and closes on Enter', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    fireEvent.keyDown(window, { key: 'Enter' })

    // First enabled command (Search Notes) should execute
    expect(commands[0].execute).toHaveBeenCalled()
    expect(onClose).toHaveBeenCalled()
  })

  it('navigates with arrow keys and selects with Enter', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    fireEvent.keyDown(window, { key: 'ArrowDown' })
    fireEvent.keyDown(window, { key: 'Enter' })

    // Second enabled command (New Note) should execute
    expect(commands[1].execute).toHaveBeenCalled()
    expect(onClose).toHaveBeenCalled()
  })

  it('keeps keyboard selection stable when the mouse is already over a row', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    fireEvent.mouseMove(screen.getByText('Commit & Push'), {
      clientX: 10,
      clientY: 10,
      screenX: 10,
      screenY: 10,
    })
    fireEvent.keyDown(window, { key: 'ArrowDown' })
    fireEvent.keyDown(window, { key: 'Enter' })

    expect(commands[1].execute).toHaveBeenCalledOnce()
    expect(commands[2].execute).not.toHaveBeenCalled()
    expect(onClose).toHaveBeenCalledOnce()
  })

  it('lets real mouse movement take over command selection', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    fireEvent.mouseMove(screen.getByText('Commit & Push'), {
      clientX: 10,
      clientY: 10,
      screenX: 10,
      screenY: 10,
    })
    fireEvent.mouseMove(screen.getByText('Commit & Push'), {
      clientX: 10,
      clientY: 11,
      screenX: 10,
      screenY: 11,
    })
    fireEvent.keyDown(window, { key: 'Enter' })

    expect(commands[2].execute).toHaveBeenCalledOnce()
    expect(onClose).toHaveBeenCalledOnce()
  })

  it('ignores stationary mouse hover again after keyboard navigation changes selection', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    fireEvent.mouseMove(screen.getByText('Commit & Push'), {
      clientX: 10,
      clientY: 10,
      screenX: 10,
      screenY: 10,
    })
    fireEvent.mouseMove(screen.getByText('Commit & Push'), {
      clientX: 10,
      clientY: 11,
      screenX: 10,
      screenY: 11,
    })
    fireEvent.keyDown(window, { key: 'ArrowUp' })
    fireEvent.mouseMove(screen.getByText('Commit & Push'), {
      clientX: 10,
      clientY: 11,
      screenX: 10,
      screenY: 11,
    })
    fireEvent.keyDown(window, { key: 'Enter' })

    expect(commands[1].execute).toHaveBeenCalledOnce()
    expect(commands[2].execute).not.toHaveBeenCalled()
    expect(onClose).toHaveBeenCalledOnce()
  })

  it('keeps a short query keyboard-selectable after ArrowDown and Enter', () => {
    const changeNoteType = makeCommand({
      id: 'change-note-type',
      label: 'Change Note Type…',
      group: 'Note',
    })

    render(
      <CommandPalette
        open={true}
        commands={[
          changeNoteType,
          makeCommand({ id: 'open-settings', label: 'Open Settings', group: 'Settings' }),
        ]}
        onClose={onClose}
      />,
    )

    fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: 'ch' } })
    fireEvent.keyDown(window, { key: 'ArrowDown' })

    const selectedRow = screen.getByText('Change Note Type…').closest('[data-selected]')
    expect(selectedRow).toHaveAttribute('data-selected', 'true')

    fireEvent.keyDown(window, { key: 'Enter' })
    expect(changeNoteType.execute).toHaveBeenCalledOnce()
    expect(onClose).toHaveBeenCalledOnce()
  })

  it('does not go below the last item', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    for (let i = 0; i < 20; i++) {
      fireEvent.keyDown(window, { key: 'ArrowDown' })
    }
    fireEvent.keyDown(window, { key: 'Enter' })

    // Should select last enabled command (Open Settings)
    expect(commands[3].execute).toHaveBeenCalled()
  })

  it('does not go above first item', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    fireEvent.keyDown(window, { key: 'ArrowUp' })
    fireEvent.keyDown(window, { key: 'Enter' })

    // Should still select first command
    expect(commands[0].execute).toHaveBeenCalled()
  })

  it('calls onClose when clicking backdrop', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    const backdrop = screen.getByPlaceholderText('Type a command...').closest('.fixed')!
    fireEvent.click(backdrop)

    expect(onClose).toHaveBeenCalled()
  })

  it('executes command when clicking an item', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    fireEvent.click(screen.getByText('Commit & Push'))

    expect(commands[2].execute).toHaveBeenCalled()
    expect(onClose).toHaveBeenCalled()
  })

  it('shows footer hints', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    expect(screen.getByText('↑↓ navigate')).toBeInTheDocument()
    expect(screen.getByText('↵ select')).toBeInTheDocument()
    expect(screen.getByText('esc close')).toBeInTheDocument()
  })

  it('switches into AI mode when the query starts with a leading space', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)
    fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: ' ' } })

    expect(screen.getByTestId('command-palette-ai-input')).toBeInTheDocument()
    expect(screen.getAllByText('Ask Claude Code').length).toBeGreaterThan(0)
    expect(screen.queryByText('Search Notes')).not.toBeInTheDocument()
  })

  it('keeps leading-space input in command search when AI mode is disabled', () => {
    render(
      <CommandPalette
        open={true}
        commands={commands}
        aiModeEnabled={false}
        onClose={onClose}
      />,
    )

    const input = screen.getByPlaceholderText('Type a command...')
    fireEvent.change(input, { target: { value: ' search' } })

    expect(screen.queryByTestId('command-palette-ai-input')).not.toBeInTheDocument()
    expect(input).toHaveValue(' search')
    expect(queueAiPrompt).not.toHaveBeenCalled()
    expect(requestOpenAiChat).not.toHaveBeenCalled()
  })

  it('inserts Tauri native folder drops into the command query input', async () => {
    nativeDropState.tauriMode = true
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    const input = screen.getByPlaceholderText('Type a command...') as HTMLInputElement
    mockElementRect(input)
    input.focus()
    await waitForNativePathDropListener()

    act(() => {
      emitNativePathDrop(['/Users/test/Projects'])
    })

    await waitFor(() => {
      expect(input).toHaveValue('/Users/test/Projects')
    })
  })

  it('focuses the AI editor immediately when the leading space triggers AI mode', () => {
    render(<CommandPalette open={true} commands={commands} entries={entries} onClose={onClose} />)

    const input = screen.getByPlaceholderText('Type a command...')
    input.focus()
    fireEvent.change(input, { target: { value: ' ' } })

    expect(screen.getByTestId('command-palette-ai-input')).toHaveFocus()
  })

  it('places the AI editor caret after the trigger space on mode entry', () => {
    render(<CommandPalette open={true} commands={commands} entries={entries} onClose={onClose} />)

    const input = screen.getByPlaceholderText('Type a command...')
    input.focus()
    fireEvent.change(input, { target: { value: ' ' } })

    const editor = screen.getByTestId('command-palette-ai-input') as HTMLDivElement
    expect(editor).toHaveFocus()
    expect(readSelectionRange(editor)).toEqual({ start: 1, end: 1 })
  })

  it('returns to command mode when the leading space is deleted', () => {
    render(
      <CommandPalette open={true} commands={commands} entries={entries} onClose={onClose} />,
    )

    fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: ' ' } })
    updateAiInput('new')

    const input = screen.getByPlaceholderText('Type a command...') as HTMLInputElement
    expect(screen.queryByTestId('command-palette-ai-input')).toBeNull()
    expect(input.value).toBe('new')
    expect(screen.getByText('New Note')).toBeInTheDocument()
  })

  it('queues a stripped AI prompt and closes on Enter in AI mode', () => {
    render(
      <CommandPalette open={true} commands={commands} entries={entries} onClose={onClose} />,
    )

    fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: ' ' } })
    const editor = updateAiInput(' hello world')
    fireEvent.keyDown(editor, { key: 'Enter' })

    expect(queueAiPrompt).toHaveBeenCalledWith('hello world', [])
    expect(requestOpenAiChat).toHaveBeenCalledOnce()
    expect(onClose).toHaveBeenCalledOnce()
  })

  it('submits the latest AI editor DOM text when Enter arrives before state catches up', () => {
    render(
      <CommandPalette open={true} commands={commands} entries={entries} onClose={onClose} />,
    )

    fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: ' ' } })
    const editor = screen.getByTestId('command-palette-ai-input')
    editor.textContent = ' fast prompt'
    setSelection(editor, ' fast prompt'.length)
    fireEvent.keyDown(editor, { key: 'Enter' })

    expect(queueAiPrompt).toHaveBeenCalledWith('fast prompt', [])
    expect(requestOpenAiChat).toHaveBeenCalledOnce()
    expect(onClose).toHaveBeenCalledOnce()
  })

  it('queues AI prompts with the selected workspace target id', () => {
    render(
      <CommandPalette
        open={true}
        commands={commands}
        entries={entries}
        aiAgentLabel="Codex"
        aiPromptTargetId="agent:codex"
        onClose={onClose}
      />,
    )

    fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: ' ' } })
    const editor = updateAiInput(' ask the selected agent')
    fireEvent.keyDown(editor, { key: 'Enter' })

    expect(screen.getAllByText('Ask Codex').length).toBeGreaterThan(0)
    expect(queueAiPrompt).toHaveBeenCalledWith('ask the selected agent', [], 'agent:codex')
    expect(requestOpenAiChat).toHaveBeenCalledOnce()
  })

  it('closes without queueing when AI mode only contains the trigger space', () => {
    render(<CommandPalette open={true} commands={commands} onClose={onClose} />)

    fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: ' ' } })
    fireEvent.keyDown(screen.getByTestId('command-palette-ai-input'), { key: 'Enter' })

    expect(queueAiPrompt).not.toHaveBeenCalled()
    expect(requestOpenAiChat).not.toHaveBeenCalled()
    expect(onClose).toHaveBeenCalledOnce()
  })

  describe('relevance ranking', () => {
    const relevanceCommands: CommandAction[] = [
      makeCommand({ id: 'create-note', label: 'New Note', group: 'Note' }),
      makeCommand({ id: 'toggle-raw', label: 'Toggle Raw Editor', group: 'View' }),
      makeCommand({ id: 'search-notes', label: 'Search Notes', group: 'Navigation' }),
    ]

    function getVisibleLabels() {
      return screen.getAllByText(
        (_content, el) =>
          el?.tagName === 'SPAN' &&
          el.classList.contains('text-foreground') &&
          !!el.textContent,
      ).map(el => el.textContent)
    }

    it('shows only the relevant raw command for query "raw"', () => {
      render(<CommandPalette open={true} commands={relevanceCommands} onClose={onClose} />)
      fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: 'raw' } })

      const labels = getVisibleLabels()
      expect(labels).toEqual(['Toggle Raw Editor'])
    })

    it('ranks "New Note" first for query "new note"', () => {
      render(<CommandPalette open={true} commands={relevanceCommands} onClose={onClose} />)
      fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: 'new note' } })

      const labels = getVisibleLabels()
      expect(labels[0]).toBe('New Note')
    })

    it('preserves default section order with empty query', () => {
      render(<CommandPalette open={true} commands={relevanceCommands} onClose={onClose} />)

      const groupHeaders = screen.getAllByText(
        (_content, el) =>
          el?.tagName === 'DIV' &&
          el.classList.contains('text-[11px]') &&
          el.classList.contains('font-medium') &&
          !!el.textContent,
      ).map(el => el.textContent)

      // Default order: Navigation < Note < View
      expect(groupHeaders).toEqual(['Navigation', 'Note', 'View'])
    })
  })
})
