import { describe, it, expect, vi, afterEach } from 'vitest'
import { renderHook } from '@testing-library/react'
import { useAppKeyboard } from './useAppKeyboard'
import {
  APP_COMMAND_IDS,
  executeAppCommand,
  resetAppCommandDispatchStateForTests,
} from './appCommandDispatcher'

function fireKey(
  key: string,
  mods: { altKey?: boolean; metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; code?: string } = {},
) {
  return fireKeyOnTarget(window, key, mods)
}

function fireKeyOnTarget(
  target: EventTarget,
  key: string,
  mods: { altKey?: boolean; metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; code?: string } = {},
) {
  const event = new KeyboardEvent('keydown', {
    key,
    code: mods.code,
    altKey: mods.altKey ?? false,
    metaKey: mods.metaKey ?? false,
    ctrlKey: mods.ctrlKey ?? false,
    shiftKey: mods.shiftKey ?? false,
    bubbles: true,
    cancelable: true,
  })
  target.dispatchEvent(event)
  return event
}

function makeActions() {
  return {
    onQuickOpen: vi.fn(),
    onCommandPalette: vi.fn(),
    onSearch: vi.fn(),
    onCreateNote: vi.fn(),
    onSave: vi.fn(),
    onUndo: vi.fn(),
    onRedo: vi.fn(),
    onOpenSettings: vi.fn(),
    onDeleteNote: vi.fn(),
    onArchiveNote: vi.fn(),
    onToggleOrganized: vi.fn(),
    onSetViewMode: vi.fn(),
    onZoomIn: vi.fn(),
    onZoomOut: vi.fn(),
    onZoomReset: vi.fn(),
    onPastePlainText: vi.fn(),
    onGoBack: vi.fn(),
    onGoForward: vi.fn(),
    activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject<string | null>,
    multiSelectionCommandRef: { current: null },
  }
}

const originalUserAgent = navigator.userAgent

function setUserAgent(userAgent: string) {
  Object.defineProperty(window.navigator, 'userAgent', {
    configurable: true,
    value: userAgent,
  })
}

describe('useAppKeyboard', () => {
  afterEach(() => {
    setUserAgent(originalUserAgent)
    delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__
    resetAppCommandDispatchStateForTests()
    vi.restoreAllMocks()
  })

  it('Cmd+1 sets view mode to editor-only', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('1', { metaKey: true })
    expect(actions.onSetViewMode).toHaveBeenCalledWith('editor-only')
  })

  it('Cmd+2 sets view mode to editor-list', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('2', { metaKey: true })
    expect(actions.onSetViewMode).toHaveBeenCalledWith('editor-list')
  })

  it('Cmd+3 sets view mode to all', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('3', { metaKey: true })
    expect(actions.onSetViewMode).toHaveBeenCalledWith('all')
  })

  it('does not fire view mode when Cmd+Alt pressed', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('1', { metaKey: true, altKey: true })
    expect(actions.onSetViewMode).not.toHaveBeenCalled()
  })

  it('Cmd+P triggers quick open', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('p', { metaKey: true })
    expect(actions.onQuickOpen).toHaveBeenCalled()
  })

  it('Cmd+O triggers quick open', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('o', { metaKey: true, code: 'KeyO' })
    expect(actions.onQuickOpen).toHaveBeenCalled()
  })

  it('Cmd+N triggers create note', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('n', { metaKey: true })
    expect(actions.onCreateNote).toHaveBeenCalled()
  })

  it('Cmd+N still works in Tauri mode', () => {
    const actions = makeActions()
    ;(window as typeof window & { __TAURI__?: object }).__TAURI__ = {}
    renderHook(() => useAppKeyboard(actions))
    fireKey('n', { metaKey: true })
    expect(actions.onCreateNote).toHaveBeenCalled()
  })

  it('Cmd+\\ still works in Tauri mode', () => {
    const actions = makeActions()
    actions.onToggleRawEditor = vi.fn()
    ;(window as typeof window & { __TAURI__?: object }).__TAURI__ = {}
    renderHook(() => useAppKeyboard(actions))
    fireKey('\\', { metaKey: true })
    expect(actions.onToggleRawEditor).toHaveBeenCalled()
  })

  it('Cmd+Shift+I still works in Tauri mode', () => {
    const actions = makeActions()
    const onToggleInspector = vi.fn()
    ;(window as typeof window & { __TAURI__?: object }).__TAURI__ = {}
    renderHook(() => useAppKeyboard({ ...actions, onToggleInspector }))
    fireKey('i', { metaKey: true, shiftKey: true })
    expect(onToggleInspector).toHaveBeenCalled()
  })

  it('Cmd+Shift+L still works in Tauri mode', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    ;(window as typeof window & { __TAURI__?: object }).__TAURI__ = {}
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
    fireKey('l', { metaKey: true, shiftKey: true })
    expect(onToggleAIChat).toHaveBeenCalled()
  })

  it('Cmd+D triggers toggle favorite on active note', () => {
    const actions = makeActions()
    actions.onToggleFavorite = vi.fn()
    renderHook(() => useAppKeyboard(actions))
    fireKey('d', { metaKey: true })
    expect(actions.onToggleFavorite).toHaveBeenCalledWith('/vault/test.md')
  })

  it('Cmd+D still works in Tauri mode', () => {
    const actions = makeActions()
    actions.onToggleFavorite = vi.fn()
    ;(window as typeof window & { __TAURI__?: object }).__TAURI__ = {}
    renderHook(() => useAppKeyboard(actions))
    fireKey('d', { metaKey: true })
    expect(actions.onToggleFavorite).toHaveBeenCalledWith('/vault/test.md')
  })

  it('Cmd+E triggers toggle organized on active note, not archive', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('e', { metaKey: true })
    expect(actions.onToggleOrganized).toHaveBeenCalledWith('/vault/test.md')
    expect(actions.onArchiveNote).not.toHaveBeenCalled()
  })

  it('Cmd+Z triggers app action undo outside text editing', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('z', { metaKey: true, code: 'KeyZ' })
    expect(actions.onUndo).toHaveBeenCalledTimes(1)
  })

  it('Cmd+Shift+Z triggers app action redo outside text editing', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('z', { metaKey: true, shiftKey: true, code: 'KeyZ' })
    expect(actions.onRedo).toHaveBeenCalledTimes(1)
  })

  it('Ctrl+Y triggers app action redo on non-mac platforms', () => {
    setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('y', { ctrlKey: true, code: 'KeyY' })
    expect(actions.onRedo).toHaveBeenCalledTimes(1)
  })

  it('leaves text undo and redo to focused editors', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))

    withFocusedContentEditable((editable) => {
      expect(fireKeyOnTarget(editable, 'z', { metaKey: true, code: 'KeyZ' }).defaultPrevented).toBe(false)
      expect(fireKeyOnTarget(editable, 'z', { metaKey: true, shiftKey: true, code: 'KeyZ' }).defaultPrevented).toBe(false)
      expect(actions.onUndo).not.toHaveBeenCalled()
      expect(actions.onRedo).not.toHaveBeenCalled()
    })
  })

  it('Cmd+E uses the current multi-selection instead of the active note', () => {
    const actions = makeActions()
    const organizeSelected = vi.fn()
    actions.multiSelectionCommandRef.current = {
      selectedPaths: ['/vault/a.md', '/vault/b.md'],
      organizeSelected,
    }

    renderHook(() => useAppKeyboard(actions))
    fireKey('e', { metaKey: true })

    expect(organizeSelected).toHaveBeenCalledTimes(1)
    expect(actions.onToggleOrganized).not.toHaveBeenCalled()
  })

  it('lets macOS Ctrl text-editing bindings pass through focused text inputs', () => {
    setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')
    const actions = makeActions()
    actions.onToggleFavorite = vi.fn()
    renderHook(() => useAppKeyboard(actions))

    withFocusedInput(() => {
      const organize = fireKey('e', { ctrlKey: true, code: 'KeyE' })
      const nextLine = fireKey('n', { ctrlKey: true, code: 'KeyN' })
      const previousLine = fireKey('p', { ctrlKey: true, code: 'KeyP' })
      const deleteForward = fireKey('d', { ctrlKey: true, code: 'KeyD' })

      expect(organize.defaultPrevented).toBe(false)
      expect(nextLine.defaultPrevented).toBe(false)
      expect(previousLine.defaultPrevented).toBe(false)
      expect(deleteForward.defaultPrevented).toBe(false)
      expect(actions.onToggleOrganized).not.toHaveBeenCalled()
      expect(actions.onCreateNote).not.toHaveBeenCalled()
      expect(actions.onQuickOpen).not.toHaveBeenCalled()
      expect(actions.onToggleFavorite).not.toHaveBeenCalled()
    })
  })

  it('lets macOS Ctrl text-editing bindings pass through focused rich editors', () => {
    setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')
    const actions = makeActions()
    actions.onToggleFavorite = vi.fn()
    renderHook(() => useAppKeyboard(actions))

    withFocusedContentEditable((editable) => {
      expect(fireKeyOnTarget(editable, 'e', { ctrlKey: true, code: 'KeyE' }).defaultPrevented).toBe(false)
      expect(fireKeyOnTarget(editable, 'd', { ctrlKey: true, code: 'KeyD' }).defaultPrevented).toBe(false)
      expect(actions.onToggleOrganized).not.toHaveBeenCalled()
      expect(actions.onToggleFavorite).not.toHaveBeenCalled()
    })
  })

  it('still runs Command shortcuts on macOS outside focused text inputs', () => {
    setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')
    const actions = makeActions()
    actions.onToggleFavorite = vi.fn()
    renderHook(() => useAppKeyboard(actions))

    fireKey('e', { metaKey: true, code: 'KeyE' })
    fireKey('d', { metaKey: true, code: 'KeyD' })

    expect(actions.onToggleOrganized).toHaveBeenCalledWith('/vault/test.md')
    expect(actions.onToggleFavorite).toHaveBeenCalledWith('/vault/test.md')
  })

  it('uses active-layout letter shortcuts before physical key codes', () => {
    const actions = makeActions()
    actions.onToggleFavorite = vi.fn()
    renderHook(() => useAppKeyboard(actions))

    fireKey('e', { metaKey: true, code: 'KeyD' })

    expect(actions.onToggleOrganized).toHaveBeenCalledWith('/vault/test.md')
    expect(actions.onToggleFavorite).not.toHaveBeenCalled()
  })

  it('Cmd+E still works when editor focus stops propagation', () => {
    const actions = makeActions()
    const onToggleOrganized = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleOrganized }))
    withFocusedContentEditable((editable) => {
      editable.addEventListener('keydown', (event) => event.stopPropagation())
      fireKeyOnTarget(editable, 'e', { metaKey: true })
      expect(onToggleOrganized).toHaveBeenCalledWith('/vault/test.md')
      expect(actions.onArchiveNote).not.toHaveBeenCalled()
    })
  })

  it('Cmd+J no longer triggers any app command', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('j', { metaKey: true })
    expect(actions.onCreateNote).not.toHaveBeenCalled()
    expect(actions.onQuickOpen).not.toHaveBeenCalled()
  })

  it('Alt+4 does not trigger any view mode', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('4', { altKey: true })
    expect(actions.onSetViewMode).not.toHaveBeenCalled()
  })

  it('Cmd+K triggers command palette', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('k', { metaKey: true })
    expect(actions.onCommandPalette).toHaveBeenCalled()
  })

  it('Cmd+Shift+F triggers search', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('f', { metaKey: true, shiftKey: true })
    expect(actions.onSearch).toHaveBeenCalled()
  })

  it('Cmd+Shift+F does not trigger other shortcuts', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('f', { metaKey: true, shiftKey: true })
    expect(actions.onQuickOpen).not.toHaveBeenCalled()
    expect(actions.onCreateNote).not.toHaveBeenCalled()
  })

  it('Cmd+Shift+V triggers plain-text paste in focused text editors', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    withFocusedContentEditable((editable) => {
      const event = fireKeyOnTarget(editable, 'v', { metaKey: true, shiftKey: true, code: 'KeyV' })

      expect(event.defaultPrevented).toBe(true)
      expect(actions.onPastePlainText).toHaveBeenCalledOnce()
    })
  })

  it('Cmd+Z and Cmd+Shift+Z run app history when text is not focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))

    fireKey('z', { metaKey: true, code: 'KeyZ' })
    fireKey('z', { metaKey: true, shiftKey: true, code: 'KeyZ' })

    expect(actions.onUndo).toHaveBeenCalledOnce()
    expect(actions.onRedo).toHaveBeenCalledOnce()
  })

  it('Ctrl+Y runs redo on Windows-style platforms', () => {
    setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))

    fireKey('y', { ctrlKey: true, code: 'KeyY' })

    expect(actions.onRedo).toHaveBeenCalledOnce()
  })

  it('lets focused text inputs own undo and redo shortcuts', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))

    withFocusedInput((input) => {
      const undo = fireKeyOnTarget(input, 'z', { metaKey: true, code: 'KeyZ' })
      const redo = fireKeyOnTarget(input, 'z', { metaKey: true, shiftKey: true, code: 'KeyZ' })

      expect(undo.defaultPrevented).toBe(false)
      expect(redo.defaultPrevented).toBe(false)
      expect(actions.onUndo).not.toHaveBeenCalled()
      expect(actions.onRedo).not.toHaveBeenCalled()
    })
  })

  function withFocusedInput(fn: (input: HTMLInputElement) => void) {
    const input = document.createElement('input')
    document.body.appendChild(input)
    input.focus()
    try { fn(input) } finally { document.body.removeChild(input) }
  }

  function withFocusedContentEditable(fn: (editable: HTMLDivElement) => void) {
    const editable = document.createElement('div')
    editable.setAttribute('contenteditable', 'true')
    document.body.appendChild(editable)
    editable.focus()
    try { fn(editable) } finally { document.body.removeChild(editable) }
  }

  it('Cmd+Backspace does not delete note when text input is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    withFocusedInput(() => {
      fireKey('Backspace', { metaKey: true })
      expect(actions.onDeleteNote).not.toHaveBeenCalled()
    })
  })

  it('Cmd+Backspace does not delete note when contenteditable is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    withFocusedContentEditable((editable) => {
      fireKeyOnTarget(editable, 'Backspace', { metaKey: true })
      expect(actions.onDeleteNote).not.toHaveBeenCalled()
    })
  })

  it('Cmd+Backspace in a focused contenteditable suppresses the native Delete Note echo', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    withFocusedContentEditable((editable) => {
      fireKeyOnTarget(editable, 'Backspace', { metaKey: true })
      executeAppCommand(APP_COMMAND_IDS.noteDelete, actions, 'native-menu')
      expect(actions.onDeleteNote).not.toHaveBeenCalled()
    })
  })

  it('Cmd+Backspace deletes note when no text input is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('Backspace', { metaKey: true })
    expect(actions.onDeleteNote).toHaveBeenCalledWith('/vault/test.md')
  })

  it('Cmd+Backspace deletes the current multi-selection instead of the active note', () => {
    const actions = makeActions()
    const deleteSelected = vi.fn()
    actions.multiSelectionCommandRef.current = {
      selectedPaths: ['/vault/a.md', '/vault/b.md'],
      deleteSelected,
    }

    renderHook(() => useAppKeyboard(actions))
    fireKey('Backspace', { metaKey: true })

    expect(deleteSelected).toHaveBeenCalledTimes(1)
    expect(actions.onDeleteNote).not.toHaveBeenCalled()
  })

  it('Cmd+Left does not go back when contenteditable is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    withFocusedContentEditable((editable) => {
      fireKeyOnTarget(editable, 'ArrowLeft', { metaKey: true, code: 'ArrowLeft' })
      expect(actions.onGoBack).not.toHaveBeenCalled()
    })
  })

  it('Cmd+Right does not go forward when contenteditable is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    withFocusedContentEditable((editable) => {
      fireKeyOnTarget(editable, 'ArrowRight', { metaKey: true, code: 'ArrowRight' })
      expect(actions.onGoForward).not.toHaveBeenCalled()
    })
  })

  it('Cmd+Left still goes back when no text input is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('ArrowLeft', { metaKey: true, code: 'ArrowLeft' })
    expect(actions.onGoBack).toHaveBeenCalled()
  })

  it('Cmd+Right still goes forward when no text input is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('ArrowRight', { metaKey: true, code: 'ArrowRight' })
    expect(actions.onGoForward).toHaveBeenCalled()
  })

  it('Cmd+K still works when text input is focused', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    withFocusedInput(() => {
      fireKey('k', { metaKey: true })
      expect(actions.onCommandPalette).toHaveBeenCalled()
    })
  })

  it('Cmd+= triggers zoom in', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('=', { metaKey: true })
    expect(actions.onZoomIn).toHaveBeenCalled()
  })

  it('Cmd++ triggers zoom in', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('+', { metaKey: true })
    expect(actions.onZoomIn).toHaveBeenCalled()
  })

  it('Cmd+- triggers zoom out', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('-', { metaKey: true })
    expect(actions.onZoomOut).toHaveBeenCalled()
  })

  it('Cmd+0 triggers zoom reset', () => {
    const actions = makeActions()
    renderHook(() => useAppKeyboard(actions))
    fireKey('0', { metaKey: true })
    expect(actions.onZoomReset).toHaveBeenCalled()
  })

  it('Cmd+Shift+L triggers toggle AI chat', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
    fireKey('l', { metaKey: true, shiftKey: true })
    expect(onToggleAIChat).toHaveBeenCalled()
  })

  it('Cmd+Shift+L works when text input is focused', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
    withFocusedInput(() => {
      fireKey('l', { metaKey: true, shiftKey: true })
      expect(onToggleAIChat).toHaveBeenCalled()
    })
  })

  it('Cmd+Shift+L works when editor stops propagation', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
    withFocusedContentEditable((editable) => {
      editable.addEventListener('keydown', (event) => event.stopPropagation())
      fireKeyOnTarget(editable, 'l', { metaKey: true, shiftKey: true })
      expect(onToggleAIChat).toHaveBeenCalled()
    })
  })

  it('Cmd+Shift+L matches by physical key code when the localized key differs', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
    fireKey('¬', { code: 'KeyL', metaKey: true, shiftKey: true })
    expect(onToggleAIChat).toHaveBeenCalled()
  })

  it('Ctrl+Shift+L triggers toggle AI chat', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
    fireKey('l', { ctrlKey: true, shiftKey: true })
    expect(onToggleAIChat).toHaveBeenCalled()
  })

  it('Cmd+I does not trigger AI chat (reserved for italic)', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
    fireKey('i', { metaKey: true })
    expect(onToggleAIChat).not.toHaveBeenCalled()
  })

  it('Cmd+Shift+O triggers open in new window', () => {
    const actions = makeActions()
    const onOpenInNewWindow = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onOpenInNewWindow }))
    fireKey('o', { metaKey: true, shiftKey: true })
    expect(onOpenInNewWindow).toHaveBeenCalled()
  })

  it('Cmd+Shift+I triggers toggle inspector', () => {
    const actions = makeActions()
    const onToggleInspector = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleInspector }))
    fireKey('i', { metaKey: true, shiftKey: true })
    expect(onToggleInspector).toHaveBeenCalled()
  })

  it('Cmd+Shift+I does not trigger AI chat toggle', () => {
    const actions = makeActions()
    const onToggleAIChat = vi.fn()
    const onToggleInspector = vi.fn()
    renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat, onToggleInspector }))
    fireKey('i', { metaKey: true, shiftKey: true })
    expect(onToggleInspector).toHaveBeenCalled()
    expect(onToggleAIChat).not.toHaveBeenCalled()
  })
})
