import { describe, it, expect, vi } from 'vitest'
import { renderHook } from '@testing-library/react'
import { useCommandRegistry, buildTypeCommands, extractVaultTypes, pluralizeType, groupSortKey } from './useCommandRegistry'
import type { CommandAction } from './useCommandRegistry'
import { NEW_AI_CHAT_EVENT, OPEN_AI_CHAT_EVENT } from '../utils/aiPromptBridge'
import { formatShortcutDisplay } from './appCommandCatalog'

function makeConfig(overrides: Record<string, unknown> = {}) {
  return {
    activeTabPath: '/vault/test.md',
    entries: [],
    modifiedCount: 0,
    onQuickOpen: vi.fn(),
    onCreateNote: vi.fn(),
    onCreateNoteOfType: vi.fn(),
    onSave: vi.fn(),
    onPastePlainText: vi.fn(),
    onOpenSettings: vi.fn(),
    onDeleteNote: vi.fn(),
    onArchiveNote: vi.fn(),
    onUnarchiveNote: vi.fn(),
    onToggleOrganized: vi.fn(),
    onCommitPush: vi.fn(),
    onResolveConflicts: vi.fn(),
    onSetViewMode: vi.fn(),
    onToggleInspector: vi.fn(),
    onToggleDiff: vi.fn(),
    onToggleRawEditor: vi.fn(),
    noteWidth: 'normal',
    defaultNoteWidth: 'normal',
    onSetNoteWidth: vi.fn(),
    onSetDefaultNoteWidth: vi.fn(),
    onToggleAIChat: vi.fn(),
    onOpenVault: vi.fn(),
    activeNoteModified: false,
    onZoomIn: vi.fn(),
    onZoomOut: vi.fn(),
    onZoomReset: vi.fn(),
    zoomLevel: 100,
    onSelect: vi.fn(),
    onCloseTab: vi.fn(),
    onGoBack: vi.fn(),
    onGoForward: vi.fn(),
    canGoBack: false,
    canGoForward: false,
    onCheckForUpdates: vi.fn(),
    onCreateType: vi.fn(),
    ...overrides,
  }
}

function findCommand(commands: CommandAction[], id: string): CommandAction | undefined {
  return commands.find(c => c.id === id)
}

function expectFolderCommandStates(overrides: Record<string, unknown>, expected: {
  copy: boolean
  delete: boolean
  rename: boolean
  reveal: boolean
}) {
  const { result } = renderHook(() => useCommandRegistry(makeConfig(overrides)))

  expect(findCommand(result.current, 'reveal-selected-folder')?.enabled).toBe(expected.reveal)
  expect(findCommand(result.current, 'copy-selected-folder-path')?.enabled).toBe(expected.copy)
  expect(findCommand(result.current, 'rename-folder')?.enabled).toBe(expected.rename)
  expect(findCommand(result.current, 'delete-folder')?.enabled).toBe(expected.delete)
}

describe('useCommandRegistry', () => {
  it('includes resolve-conflicts command in Git group', () => {
    const config = makeConfig()
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'resolve-conflicts')
    expect(cmd).toBeDefined()
    expect(cmd!.group).toBe('Git')
    expect(cmd!.label).toBe('Resolve Conflicts')
  })

  it('resolve-conflicts is always enabled', () => {
    const config = makeConfig()
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'resolve-conflicts')
    expect(cmd!.enabled).toBe(true)
  })

  it('resolve-conflicts executes onResolveConflicts callback', () => {
    const onResolveConflicts = vi.fn()
    const config = makeConfig({ onResolveConflicts })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'resolve-conflicts')
    cmd!.execute()
    expect(onResolveConflicts).toHaveBeenCalled()
  })

  it('resolve-conflicts has searchable keywords', () => {
    const config = makeConfig()
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'resolve-conflicts')
    expect(cmd!.keywords).toContain('conflict')
    expect(cmd!.keywords).toContain('merge')
  })

  it('commit-push is enabled when modifiedCount > 0', () => {
    const config = makeConfig({ modifiedCount: 5 })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'commit-push')
    expect(cmd!.enabled).toBe(true)
  })

  it('commit-push is disabled when modifiedCount is 0', () => {
    const config = makeConfig({ modifiedCount: 0 })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'commit-push')
    expect(cmd!.enabled).toBe(false)
  })

  it('includes initialize-git command for non-git vaults', () => {
    const onInitializeGit = vi.fn()
    const config = makeConfig({ isGitVault: false, onInitializeGit })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'initialize-git')

    expect(cmd).toBeDefined()
    expect(cmd!.group).toBe('Git')
    expect(cmd!.label).toBe('Initialize Git for Current Vault')
    expect(cmd!.enabled).toBe(true)

    cmd!.execute()
    expect(onInitializeGit).toHaveBeenCalledOnce()
  })

  it('hides remote git commands for non-git vaults', () => {
    const config = makeConfig({ isGitVault: false, modifiedCount: 5 })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'commit-push')).toBeUndefined()
    expect(findCommand(result.current, 'git-pull')).toBeUndefined()
    expect(findCommand(result.current, 'add-remote')).toBeUndefined()
    expect(findCommand(result.current, 'view-changes')).toBeUndefined()
  })

  it('hides all Git commands when Git features are disabled globally', () => {
    const config = makeConfig({ gitFeaturesEnabled: false, isGitVault: false, modifiedCount: 5 })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'initialize-git')).toBeUndefined()
    expect(findCommand(result.current, 'commit-push')).toBeUndefined()
    expect(findCommand(result.current, 'git-pull')).toBeUndefined()
    expect(findCommand(result.current, 'add-remote')).toBeUndefined()
    expect(findCommand(result.current, 'view-changes')).toBeUndefined()
  })

  it('exposes a single pull command for all active repositories', () => {
    const onPull = vi.fn()
    const onPullRepository = vi.fn()
    const config = makeConfig({
      gitRepositories: [
        { path: '/vault/main', label: 'Main Vault', defaultForNewNotes: true },
        { path: '/vault/brian', label: 'Brian', defaultForNewNotes: false },
      ],
      onPull,
      onPullRepository,
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'git-pull-0')).toBeUndefined()
    expect(findCommand(result.current, 'git-pull-1')).toBeUndefined()

    const pull = findCommand(result.current, 'git-pull')
    expect(pull?.label).toBe('Pull from Remote')
    pull?.execute()
    expect(onPull).toHaveBeenCalled()
    expect(onPullRepository).not.toHaveBeenCalled()
  })

  it('resolve-conflicts stays enabled across rerenders', () => {
    const config = makeConfig()
    const { result, rerender } = renderHook(
      (props) => useCommandRegistry(props),
      { initialProps: config },
    )
    expect(findCommand(result.current, 'resolve-conflicts')!.enabled).toBe(true)

    rerender(makeConfig())
    expect(findCommand(result.current, 'resolve-conflicts')!.enabled).toBe(true)
  })

  it('includes set-note-icon command in Note group', () => {
    const config = makeConfig({ onSetNoteIcon: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'set-note-icon')
    expect(cmd).toBeDefined()
    expect(cmd!.group).toBe('Note')
    expect(cmd!.label).toBe('Set Note Icon')
  })

  it('set-note-icon is enabled when active note and callback exist', () => {
    const config = makeConfig({ onSetNoteIcon: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'set-note-icon')
    expect(cmd!.enabled).toBe(true)
  })

  it('set-note-icon is disabled when no active note', () => {
    const config = makeConfig({ activeTabPath: null, onSetNoteIcon: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'set-note-icon')
    expect(cmd!.enabled).toBe(false)
  })

  it('remove-note-icon is enabled when active note has icon', () => {
    const config = makeConfig({ onRemoveNoteIcon: vi.fn(), activeNoteHasIcon: true })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'remove-note-icon')
    expect(cmd!.enabled).toBe(true)
  })

  it('remove-note-icon is disabled when active note has no icon', () => {
    const config = makeConfig({ onRemoveNoteIcon: vi.fn(), activeNoteHasIcon: false })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'remove-note-icon')
    expect(cmd!.enabled).toBe(false)
  })

  it('set-note-icon executes callback', () => {
    const onSetNoteIcon = vi.fn()
    const config = makeConfig({ onSetNoteIcon })
    const { result } = renderHook(() => useCommandRegistry(config))
    findCommand(result.current, 'set-note-icon')!.execute()
    expect(onSetNoteIcon).toHaveBeenCalled()
  })

  it('includes Change Note Type when the active note can be retargeted', () => {
    const onChangeNoteType = vi.fn()
    const config = makeConfig({ onChangeNoteType })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'change-note-type')

    expect(cmd).toBeDefined()
    expect(cmd!.enabled).toBe(true)

    cmd!.execute()
    expect(onChangeNoteType).toHaveBeenCalledOnce()
  })

  it('exposes command palette actions for changing the focused editor block type', () => {
    const onTurnCurrentBlockInto = vi.fn()
    const config = makeConfig({ onTurnCurrentBlockInto })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'turn-current-block-into-heading-2')

    expect(cmd).toMatchObject({
      enabled: true,
      group: 'Note',
      label: 'Turn Current Block into Heading 2',
    })
    expect(cmd?.keywords).toEqual(expect.arrayContaining([
      'block',
      'convert',
      'heading',
      'turn into',
    ]))

    cmd?.execute()

    expect(onTurnCurrentBlockInto).toHaveBeenCalledWith(expect.objectContaining({
      key: 'heading-2',
      props: { level: 2 },
      type: 'heading',
    }))
  })

  it('disables focused block type commands outside markdown notes', () => {
    const config = makeConfig({
      activeTabPath: '/vault/Attachments/photo.png',
      entries: [{ path: '/vault/Attachments/photo.png', title: 'photo.png', fileKind: 'binary' }],
      onTurnCurrentBlockInto: vi.fn(),
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'turn-current-block-into-heading-2')?.enabled).toBe(false)
  })

  it('enables Move Note to Folder only when another folder destination exists', () => {
    const onMoveNoteToFolder = vi.fn()
    const { result, rerender } = renderHook(
      (props) => useCommandRegistry(props),
      {
        initialProps: makeConfig({
          onMoveNoteToFolder,
          canMoveNoteToFolder: true,
        }),
      },
    )

    expect(findCommand(result.current, 'move-note-to-folder')?.enabled).toBe(true)
    findCommand(result.current, 'move-note-to-folder')!.execute()
    expect(onMoveNoteToFolder).toHaveBeenCalledOnce()

    rerender(makeConfig({
      onMoveNoteToFolder,
      canMoveNoteToFolder: false,
    }))
    expect(findCommand(result.current, 'move-note-to-folder')?.enabled).toBe(false)
  })

  it('includes restore deleted note command when provided', () => {
    const config = makeConfig({ onRestoreDeletedNote: vi.fn(), canRestoreDeletedNote: true })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'restore-deleted-note')
    expect(cmd).toBeDefined()
    expect(cmd!.enabled).toBe(true)
  })

  it('disables restore deleted note when there is no deleted preview', () => {
    const config = makeConfig({ onRestoreDeletedNote: vi.fn(), canRestoreDeletedNote: false })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'restore-deleted-note')
    expect(cmd!.enabled).toBe(false)
  })

  it('includes Customize Inbox columns when the Inbox action is available', () => {
    const onCustomizeNoteListColumns = vi.fn()
    const config = makeConfig({
      selection: { kind: 'filter', filter: 'inbox' },
      onCustomizeNoteListColumns,
      canCustomizeNoteListColumns: true,
    })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'customize-note-list-columns')
    expect(cmd).toBeDefined()
    expect(cmd!.enabled).toBe(true)
    expect(cmd!.label).toBe('Customize Inbox columns')

    cmd!.execute()
    expect(onCustomizeNoteListColumns).toHaveBeenCalled()
  })

  it('includes Customize All Notes columns in the all-notes view', () => {
    const config = makeConfig({
      selection: { kind: 'filter', filter: 'all' },
      onCustomizeNoteListColumns: vi.fn(),
      canCustomizeNoteListColumns: true,
    })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'customize-note-list-columns')
    expect(cmd).toBeDefined()
    expect(cmd!.enabled).toBe(true)
    expect(cmd!.label).toBe('Customize All Notes columns')
  })

  it('disables note-list column customization outside supported views', () => {
    const config = makeConfig({
      selection: { kind: 'sectionGroup', type: 'Book' },
      onCustomizeNoteListColumns: vi.fn(),
      canCustomizeNoteListColumns: false,
    })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'customize-note-list-columns')
    expect(cmd!.enabled).toBe(false)
  })

  it('shows Cmd+E on toggle organized and removes it from archive note', () => {
    const config = makeConfig()
    const { result } = renderHook(() => useCommandRegistry(config))
    expect(findCommand(result.current, 'toggle-organized')?.shortcut).toBe(
      formatShortcutDisplay({ display: '⌘E' }),
    )
    expect(findCommand(result.current, 'archive-note')?.shortcut).toBeUndefined()
  })

  it('exposes undo and redo commands only when action history has entries', () => {
    const onUndo = vi.fn()
    const onRedo = vi.fn()
    const { result, rerender } = renderHook(
      (props) => useCommandRegistry(props),
      {
        initialProps: makeConfig({
          onUndo,
          onRedo,
          canUndo: true,
          canRedo: false,
          undoLabel: 'Archive Note',
          redoLabel: null,
        }),
      },
    )

    expect(findCommand(result.current, 'undo-action')).toMatchObject({
      enabled: true,
      label: 'Undo Archive Note',
      shortcut: formatShortcutDisplay({ display: '⌘Z' }),
    })
    expect(findCommand(result.current, 'redo-action')).toMatchObject({
      enabled: false,
      label: 'Redo',
      shortcut: formatShortcutDisplay({ display: '⌘⇧Z' }),
    })

    findCommand(result.current, 'undo-action')?.execute()
    expect(onUndo).toHaveBeenCalledOnce()

    rerender(makeConfig({
      onUndo,
      onRedo,
      canUndo: false,
      canRedo: true,
      undoLabel: null,
      redoLabel: 'Archive Note',
    }))

    expect(findCommand(result.current, 'undo-action')?.enabled).toBe(false)
    expect(findCommand(result.current, 'redo-action')).toMatchObject({
      enabled: true,
      label: 'Redo Archive Note',
    })
  })

  it('removes AI commands when AI features are disabled', () => {
    const config = makeConfig({
      aiFeaturesEnabled: false,
      onToggleAIChat: vi.fn(),
      onOpenAiAgents: vi.fn(),
      aiAgentsStatus: {
        claude_code: { status: 'installed', version: '1.0.0' },
        codex: { status: 'missing', version: null },
        copilot: { status: 'missing', version: null },
        opencode: { status: 'missing', version: null },
        pi: { status: 'missing', version: null },
        antigravity: { status: 'missing', version: null },
        kiro: { status: 'missing', version: null },
        hermes: { status: 'missing', version: null },
      },
      selectedAiAgent: 'claude_code',
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'toggle-ai-panel')).toBeUndefined()
    expect(findCommand(result.current, 'new-ai-chat')).toBeUndefined()
    expect(findCommand(result.current, 'open-ai-agents')).toBeUndefined()
  })

  it('exposes active file actions when a note is selected', () => {
    const onRevealActiveFile = vi.fn()
    const onCopyActiveFilePath = vi.fn()
    const onCopyActiveDeepLink = vi.fn()
    const onExportNoteAsPdf = vi.fn()
    const config = makeConfig({
      activeTabPath: '/vault/current.md',
      entries: [{ path: '/vault/current.md', title: 'Current', fileKind: 'markdown' }],
      onRevealActiveFile,
      onCopyActiveFilePath,
      onCopyActiveDeepLink,
      onExportNoteAsPdf,
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'reveal-active-file')).toMatchObject({
      enabled: true,
      group: 'Note',
      label: 'Reveal in Finder',
    })
    expect(findCommand(result.current, 'copy-active-file-path')).toMatchObject({
      enabled: true,
      group: 'Note',
      label: 'Copy File Path',
    })
    expect(findCommand(result.current, 'copy-active-deep-link')).toMatchObject({
      enabled: true,
      group: 'Note',
      label: 'Copy deep link to current item',
    })
    expect(findCommand(result.current, 'export-note-pdf')).toMatchObject({
      enabled: true,
      group: 'Note',
      label: 'Export note as PDF',
    })

    findCommand(result.current, 'reveal-active-file')!.execute()
    findCommand(result.current, 'copy-active-file-path')!.execute()
    findCommand(result.current, 'copy-active-deep-link')!.execute()
    findCommand(result.current, 'export-note-pdf')!.execute()

    expect(onRevealActiveFile).toHaveBeenCalledWith('/vault/current.md')
    expect(onCopyActiveFilePath).toHaveBeenCalledWith('/vault/current.md')
    expect(onCopyActiveDeepLink).toHaveBeenCalledWith('/vault/current.md')
    expect(onExportNoteAsPdf).toHaveBeenCalledOnce()
  })

  it('only enables file-kind specific actions for supported active files', () => {
    const onOpenActiveFileExternal = vi.fn()
    const onExportNoteAsPdf = vi.fn()
    const { result, rerender } = renderHook(
      (props) => useCommandRegistry(props),
      {
        initialProps: makeConfig({
          activeTabPath: '/vault/current.md',
          entries: [{ path: '/vault/current.md', title: 'Current', fileKind: 'markdown' }],
          onOpenActiveFileExternal,
          onExportNoteAsPdf,
        }),
      },
    )

    expect(findCommand(result.current, 'open-active-file-external')?.enabled).toBe(false)
    expect(findCommand(result.current, 'export-note-pdf')?.enabled).toBe(true)

    rerender(makeConfig({
      activeTabPath: '/vault/Attachments/photo.png',
      entries: [{ path: '/vault/Attachments/photo.png', title: 'photo.png', fileKind: 'binary' }],
      onOpenActiveFileExternal,
      onExportNoteAsPdf,
    }))

    const command = findCommand(result.current, 'open-active-file-external')!
    expect(command.enabled).toBe(true)
    expect(findCommand(result.current, 'export-note-pdf')?.enabled).toBe(false)
    command.execute()
    expect(onOpenActiveFileExternal).toHaveBeenCalledWith('/vault/Attachments/photo.png')
  })

  it('disables Toggle Raw Editor when the active file cannot switch to rich mode', () => {
    const config = makeConfig({ onToggleRawEditor: undefined })
    const { result } = renderHook(() => useCommandRegistry(config))
    expect(findCommand(result.current, 'toggle-raw-editor')?.enabled).toBe(false)
  })

  it('exposes command palette actions for note width modes', () => {
    const onSetNoteWidth = vi.fn()
    const config = makeConfig({ noteWidth: 'normal', onSetNoteWidth })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'set-note-width-wide')

    expect(cmd).toBeDefined()
    expect(cmd!.group).toBe('View')
    expect(cmd!.label).toBe('Use Wide Note Width')
    expect(cmd!.keywords).toContain('wide')

    cmd!.execute()

    expect(onSetNoteWidth).toHaveBeenCalledWith('wide')
  })

  it('exposes command palette actions for moving the selected saved view', () => {
    const onMoveSelectedViewUp = vi.fn()
    const onMoveSelectedViewDown = vi.fn()
    const config = makeConfig({
      selectedViewName: 'Active Projects',
      onMoveSelectedViewUp,
      onMoveSelectedViewDown,
      canMoveSelectedViewUp: true,
      canMoveSelectedViewDown: true,
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    const moveUp = findCommand(result.current, 'move-view-up')
    const moveDown = findCommand(result.current, 'move-view-down')

    expect(moveUp).toMatchObject({
      label: 'Move Active Projects Up',
      group: 'View',
      enabled: true,
    })
    expect(moveDown).toMatchObject({
      label: 'Move Active Projects Down',
      group: 'View',
      enabled: true,
    })

    moveUp!.execute()
    moveDown!.execute()

    expect(onMoveSelectedViewUp).toHaveBeenCalledOnce()
    expect(onMoveSelectedViewDown).toHaveBeenCalledOnce()
  })

  it('disables saved view move commands at list boundaries', () => {
    const config = makeConfig({
      selectedViewName: 'Top View',
      onMoveSelectedViewUp: vi.fn(),
      onMoveSelectedViewDown: vi.fn(),
      canMoveSelectedViewUp: false,
      canMoveSelectedViewDown: true,
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'move-view-up')?.enabled).toBe(false)
    expect(findCommand(result.current, 'move-view-down')?.enabled).toBe(true)
  })

  it('disables the command for the active note width mode', () => {
    const config = makeConfig({ noteWidth: 'wide' })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'set-note-width-wide')?.enabled).toBe(false)
    expect(findCommand(result.current, 'set-note-width-normal')?.enabled).toBe(true)
  })

  it('exposes command palette actions for the default note width', () => {
    const onSetDefaultNoteWidth = vi.fn()
    const config = makeConfig({ defaultNoteWidth: 'normal', onSetDefaultNoteWidth })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'set-default-note-width-wide')

    expect(cmd).toMatchObject({
      label: 'Use Wide Note Width by Default',
      group: 'View',
      enabled: true,
    })

    cmd!.execute()
    expect(onSetDefaultNoteWidth).toHaveBeenCalledWith('wide')
  })

  it('exposes command palette actions for light and dark mode', () => {
    const onSetThemeMode = vi.fn()
    const config = makeConfig({ onSetThemeMode })
    const { result } = renderHook(() => useCommandRegistry(config))
    const lightMode = findCommand(result.current, 'use-light-mode')
    const darkMode = findCommand(result.current, 'use-dark-mode')
    const systemMode = findCommand(result.current, 'use-system-theme-mode')

    expect(lightMode).toMatchObject({
      label: 'Use Light Mode',
      enabled: true,
      group: 'Settings',
    })
    expect(darkMode).toMatchObject({
      label: 'Use Dark Mode',
      enabled: true,
      group: 'Settings',
    })
    expect(systemMode).toMatchObject({
      label: 'Use System Theme',
      enabled: true,
      group: 'Settings',
    })

    lightMode?.execute()
    darkMode?.execute()
    systemMode?.execute()

    expect(onSetThemeMode).toHaveBeenNthCalledWith(1, 'light')
    expect(onSetThemeMode).toHaveBeenNthCalledWith(2, 'dark')
    expect(onSetThemeMode).toHaveBeenNthCalledWith(3, 'system')
  })

  it('includes a New AI chat command that opens and resets the panel session', () => {
    vi.useFakeTimers()
    const config = makeConfig()
    const dispatchSpy = vi.spyOn(window, 'dispatchEvent')
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'new-ai-chat')

    expect(cmd).toBeDefined()
    expect(cmd!.group).toBe('View')
    expect(cmd!.label).toBe('New AI chat')
    expect(cmd!.enabled).toBe(true)

    cmd!.execute()

    expect(dispatchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: OPEN_AI_CHAT_EVENT }))
    vi.runOnlyPendingTimers()
    expect(dispatchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: NEW_AI_CHAT_EVENT }))
    dispatchSpy.mockRestore()
    vi.useRealTimers()
  })

  it('omits Inbox navigation when the explicit workflow is disabled', () => {
    const config = makeConfig({ showInbox: false })
    const { result } = renderHook(() => useCommandRegistry(config))
    expect(findCommand(result.current, 'go-inbox')).toBeUndefined()
  })

  it('enables folder commands when a folder is selected', () => {
    expectFolderCommandStates({
      selection: { kind: 'folder', path: 'projects' },
      onRenameFolder: vi.fn(),
      onDeleteFolder: vi.fn(),
      onRevealSelectedFolder: vi.fn(),
      onCopySelectedFolderPath: vi.fn(),
    }, { copy: true, delete: true, rename: true, reveal: true })
  })

  it('disables folder commands outside folder selection', () => {
    expectFolderCommandStates({
      selection: { kind: 'filter', filter: 'all' },
      onRenameFolder: vi.fn(),
      onDeleteFolder: vi.fn(),
      onRevealSelectedFolder: vi.fn(),
      onCopySelectedFolderPath: vi.fn(),
    }, { copy: false, delete: false, rename: false, reveal: false })
  })

  it('keeps root folder reveal and copy commands enabled without destructive actions', () => {
    expectFolderCommandStates({
      selection: { kind: 'folder', path: '', rootPath: '/Users/luca/Laputa' },
      onRenameFolder: vi.fn(),
      onDeleteFolder: vi.fn(),
      onRevealSelectedFolder: vi.fn(),
      onCopySelectedFolderPath: vi.fn(),
    }, { copy: true, delete: false, rename: false, reveal: true })
  })

  it('executes folder command callbacks', () => {
    const onRenameFolder = vi.fn()
    const onDeleteFolder = vi.fn()
    const onRevealSelectedFolder = vi.fn()
    const onCopySelectedFolderPath = vi.fn()
    const config = makeConfig({
      selection: { kind: 'folder', path: 'projects' },
      onRenameFolder,
      onDeleteFolder,
      onRevealSelectedFolder,
      onCopySelectedFolderPath,
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    findCommand(result.current, 'reveal-selected-folder')!.execute()
    findCommand(result.current, 'copy-selected-folder-path')!.execute()
    findCommand(result.current, 'rename-folder')!.execute()
    findCommand(result.current, 'delete-folder')!.execute()

    expect(onRevealSelectedFolder).toHaveBeenCalledTimes(1)
    expect(onCopySelectedFolderPath).toHaveBeenCalledTimes(1)
    expect(onRenameFolder).toHaveBeenCalledTimes(1)
    expect(onDeleteFolder).toHaveBeenCalledTimes(1)
  })

  it('omits the removed daily-note command', () => {
    const config = makeConfig()
    const { result } = renderHook(() => useCommandRegistry(config))
    expect(findCommand(result.current, 'open-daily-note')).toBeUndefined()
  })

  it('includes Contribute in the Settings group when available', () => {
    const onOpenFeedback = vi.fn()
    const config = makeConfig({ onOpenFeedback })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'open-contribute')
    expect(cmd).toBeDefined()
    expect(cmd!.label).toBe('Contribute')
    expect(cmd!.group).toBe('Settings')
    expect(cmd!.enabled).toBe(true)

    cmd!.execute()
    expect(onOpenFeedback).toHaveBeenCalledOnce()
  })

  it('keeps a single canonical New Note command when generic note types are present', () => {
    const config = makeConfig({
      entries: [
        { path: '/type-note.md', title: 'Note', isA: 'Type' },
        { path: '/lowercase-note.md', title: 'lowercase-note', isA: 'note' },
      ],
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    const newNoteCommands = result.current.filter(command => command.label.toLowerCase() === 'new note')

    expect(newNoteCommands).toHaveLength(1)
    expect(newNoteCommands[0]).toMatchObject({
      id: 'create-note',
      shortcut: formatShortcutDisplay({ display: '⌘N' }),
    })
  })

  it('exposes a current-folder create command only when a folder is selected', () => {
    const onCreateNote = vi.fn()
    const { result } = renderHook(() => useCommandRegistry(makeConfig({
      onCreateNote,
      selection: { kind: 'folder', path: 'Projects/2026 Planning', rootPath: '/vault' },
    })))

    const command = findCommand(result.current, 'create-note-current-folder')
    expect(command).toMatchObject({
      label: 'Create New Note in Current Folder',
      group: 'Note',
      enabled: true,
    })

    command!.execute()
    expect(onCreateNote).toHaveBeenCalledWith(undefined, {
      creationPath: 'folder_command_palette',
      folderPath: 'Projects/2026 Planning',
      vaultPath: '/vault',
    })
  })

  it('exposes a sheet create command using note display metadata', () => {
    const onCreateNote = vi.fn()
    const { result } = renderHook(() => useCommandRegistry(makeConfig({ onCreateNote })))

    const command = findCommand(result.current, 'create-sheet')
    expect(command).toMatchObject({
      label: 'New Sheet',
      group: 'Note',
      enabled: true,
    })

    command!.execute()
    expect(onCreateNote).toHaveBeenCalledWith(undefined, {
      creationPath: 'cmd_sheet',
      format: 'sheet',
    })
  })

  it('disables the current-folder create command outside folder selections', () => {
    const { result } = renderHook(() => useCommandRegistry(makeConfig({
      selection: { kind: 'filter', filter: 'all' },
    })))

    expect(findCommand(result.current, 'create-note-current-folder')?.enabled).toBe(false)
  })

  it('exposes paste without formatting in the command palette', () => {
    const onPastePlainText = vi.fn()
    const { result } = renderHook(() => useCommandRegistry(makeConfig({ onPastePlainText })))
    const command = findCommand(result.current, 'paste-plain-text')

    expect(command).toMatchObject({
      label: 'Paste without formatting',
      group: 'Note',
      shortcut: formatShortcutDisplay({ display: '⌘⇧V' }),
      enabled: true,
    })

    command!.execute()
    expect(onPastePlainText).toHaveBeenCalledOnce()
  })

  it('keeps a single canonical New Type command when the Type definition exists', () => {
    const onCreateType = vi.fn()
    const onCreateNoteOfType = vi.fn()
    const config = makeConfig({
      onCreateType,
      onCreateNoteOfType,
      entries: [
        { path: '/type-definition.md', title: 'Type', isA: 'Type' },
        { path: '/recipe-definition.md', title: 'Recipe', isA: 'Type' },
      ],
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    const newTypeCommands = result.current.filter(command => command.label === 'New Type')

    expect(newTypeCommands).toHaveLength(1)
    expect(newTypeCommands[0]).toMatchObject({
      id: 'create-type',
      group: 'Note',
    })
    expect(findCommand(result.current, 'list-type')).toMatchObject({
      label: 'List Types',
      group: 'Navigation',
    })

    newTypeCommands[0].execute()
    expect(onCreateType).toHaveBeenCalledOnce()
    expect(onCreateNoteOfType).not.toHaveBeenCalled()
  })
})

describe('pluralizeType', () => {
  it('pluralizes regular types', () => {
    expect(pluralizeType('Project')).toBe('Projects')
    expect(pluralizeType('Note')).toBe('Notes')
  })

  it('uses overrides for irregular plurals', () => {
    expect(pluralizeType('Person')).toBe('People')
    expect(pluralizeType('Responsibility')).toBe('Responsibilities')
  })

  it('handles sibilant endings', () => {
    expect(pluralizeType('Address')).toBe('Addresses')
  })
})

describe('extractVaultTypes', () => {
  it('returns default types when no entries', () => {
    expect(extractVaultTypes([])).toEqual(['Event', 'Person', 'Project', 'Note'])
  })

  it('extracts unique types from entries', () => {
    const entries = [
      { path: '/a', title: 'A', isA: 'Project' },
      { path: '/b', title: 'B', isA: 'Project' },
      { path: '/c', title: 'C', isA: 'Event' },
    ] as never[]
    const types = extractVaultTypes(entries)
    expect(types).toContain('Project')
    expect(types).toContain('Event')
    expect(types).toHaveLength(2)
  })

  it('includes types from Type definition entries', () => {
    const entries = [
      { path: '/book.md', title: 'Book', isA: 'Type' },
    ] as never[]
    const types = extractVaultTypes(entries)
    expect(types).toContain('Book')
  })

  it('includes types from both definitions and instances', () => {
    const entries = [
      { path: '/book.md', title: 'Book', isA: 'Type' },
      { path: '/hp.md', title: 'Harry Potter', isA: 'Book' },
      { path: '/person.md', title: 'Person', isA: 'Type' },
    ] as never[]
    const types = extractVaultTypes(entries)
    expect(types).toContain('Book')
    expect(types).toContain('Person')
    expect(types).toHaveLength(2)
  })

  it('deduplicates default types case-insensitively and keeps canonical casing', () => {
    const entries = [
      { path: '/note-type.md', title: 'note', isA: 'Type' },
      { path: '/note-instance.md', title: 'Example', isA: 'Note' },
      { path: '/project-instance.md', title: 'Project Plan', isA: 'project' },
    ] as never[]

    expect(extractVaultTypes(entries)).toEqual(['Note', 'Project'])
  })

  it('omits the legacy Journal type when no Type document defines it', () => {
    const entries = [
      { path: '/2026-03-11.md', title: 'March 11', isA: 'Journal' },
      { path: '/note.md', title: 'General Note', isA: 'Note' },
    ] as never[]

    expect(extractVaultTypes(entries)).toEqual(['Note'])
  })

  it('includes Journal when a real Type document defines it', () => {
    const entries = [
      { path: '/journal.md', title: 'Journal', isA: 'Type' },
      { path: '/2026-03-11.md', title: 'March 11', isA: 'Journal' },
      { path: '/note.md', title: 'General Note', isA: 'Note' },
    ] as never[]

    expect(extractVaultTypes(entries)).toEqual(['Journal', 'Note'])
  })

  it('omits hidden types from extracted command-palette types', () => {
    const entries = [
      { path: '/recipe.md', title: 'Recipe', isA: 'Type', visible: false },
      { path: '/dinner.md', title: 'Dinner', isA: 'Recipe' },
      { path: '/project.md', title: 'Project', isA: 'Type' },
    ] as never[]

    expect(extractVaultTypes(entries)).toEqual(['Project'])
  })
})

describe('groupSortKey', () => {
  it('returns correct order for groups', () => {
    expect(groupSortKey('Navigation')).toBeLessThan(groupSortKey('Note'))
    expect(groupSortKey('Note')).toBeLessThan(groupSortKey('Git'))
    expect(groupSortKey('Git')).toBeLessThan(groupSortKey('View'))
  })
})

describe('install-mcp command', () => {
  it('is enabled when mcpStatus is not_installed and handler provided', () => {
    const config = makeConfig({ mcpStatus: 'not_installed', onInstallMcp: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'install-mcp')
    expect(cmd).toBeDefined()
    expect(cmd!.enabled).toBe(true)
    expect(cmd!.label).toBe('Set Up External AI Tools…')
  })

  it('is enabled when mcpStatus is installed and handler provided (manage use case)', () => {
    const config = makeConfig({ mcpStatus: 'installed', onInstallMcp: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'install-mcp')
    expect(cmd!.enabled).toBe(true)
    expect(cmd!.label).toBe('Manage External AI Tools…')
  })

  it('is enabled even when mcpStatus is checking', () => {
    const config = makeConfig({ mcpStatus: 'checking', onInstallMcp: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'install-mcp')
    expect(cmd!.enabled).toBe(true)
  })

  it('is enabled even when no handler provided', () => {
    const config = makeConfig({ mcpStatus: 'not_installed' })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'install-mcp')
    expect(cmd!.enabled).toBe(true)
  })

  it('has setup keywords for discoverability', () => {
    const config = makeConfig({ mcpStatus: 'installed', onInstallMcp: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'install-mcp')
    expect(cmd!.keywords).toContain('setup')
    expect(cmd!.keywords).toContain('external')
    expect(cmd!.keywords).toContain('mcp')
    expect(cmd!.keywords).toContain('cursor')
  })

  it('executes onInstallMcp callback', () => {
    const onInstallMcp = vi.fn()
    const config = makeConfig({ mcpStatus: 'installed', onInstallMcp })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'install-mcp')
    cmd!.execute()
    expect(onInstallMcp).toHaveBeenCalled()
  })

  it('is in Settings group', () => {
    const config = makeConfig({ mcpStatus: 'installed', onInstallMcp: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'install-mcp')
    expect(cmd!.group).toBe('Settings')
  })
})

describe('reload-vault command', () => {
  it('is present in Settings group', () => {
    const config = makeConfig({ onReloadVault: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'reload-vault')
    expect(cmd).toBeDefined()
    expect(cmd!.group).toBe('Settings')
    expect(cmd!.label).toBe('Reload Vault')
  })

  it('is enabled when onReloadVault is provided', () => {
    const config = makeConfig({ onReloadVault: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'reload-vault')
    expect(cmd!.enabled).toBe(true)
  })

  it('is disabled when onReloadVault is not provided', () => {
    const config = makeConfig()
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'reload-vault')
    expect(cmd!.enabled).toBe(false)
  })

  it('executes onReloadVault callback', () => {
    const onReloadVault = vi.fn()
    const config = makeConfig({ onReloadVault })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'reload-vault')
    cmd!.execute()
    expect(onReloadVault).toHaveBeenCalled()
  })

  it('has searchable keywords', () => {
    const config = makeConfig({ onReloadVault: vi.fn() })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'reload-vault')
    expect(cmd!.keywords).toContain('reload')
    expect(cmd!.keywords).toContain('refresh')
    expect(cmd!.keywords).toContain('rescan')
  })

  it('builds explicit AI agent switch commands for installed alternatives', () => {
    const onSetDefaultAiAgent = vi.fn()
    const config = makeConfig({
      aiAgentsStatus: {
        claude_code: { status: 'installed', version: '1.0.20' },
        codex: { status: 'installed', version: '0.37.0' },
        copilot: { status: 'installed', version: '1.0.58' },
        opencode: { status: 'installed', version: '0.3.1' },
        pi: { status: 'installed', version: '0.70.2' },
        antigravity: { status: 'installed', version: '0.5.1' },
        kiro: { status: 'missing', version: null },
        hermes: { status: 'missing', version: null },
      },
      selectedAiAgent: 'claude_code',
      onSetDefaultAiAgent,
    })
    const { result } = renderHook(() => useCommandRegistry(config))
    const cmd = findCommand(result.current, 'switch-ai-agent-codex')

    expect(cmd).toBeDefined()
    expect(cmd!.label).toBe('Switch AI Agent to Codex')
    expect(findCommand(result.current, 'switch-ai-agent-copilot')).toBeDefined()
    expect(findCommand(result.current, 'switch-ai-agent-opencode')).toBeDefined()
    expect(findCommand(result.current, 'switch-ai-agent-pi')).toBeDefined()
    expect(findCommand(result.current, 'switch-ai-agent-antigravity')).toBeDefined()

    cmd!.execute()
    expect(onSetDefaultAiAgent).toHaveBeenCalledWith('codex')
    expect(findCommand(result.current, 'switch-default-ai-agent')).toBeUndefined()
  })

  it('omits explicit AI switch commands when no alternate installed agent exists', () => {
    const config = makeConfig({
      aiAgentsStatus: {
        claude_code: { status: 'installed', version: '1.0.20' },
        codex: { status: 'missing', version: null },
        copilot: { status: 'missing', version: null },
        opencode: { status: 'missing', version: null },
        pi: { status: 'missing', version: null },
        antigravity: { status: 'missing', version: null },
        kiro: { status: 'missing', version: null },
        hermes: { status: 'missing', version: null },
      },
      selectedAiAgent: 'claude_code',
      onSetDefaultAiAgent: vi.fn(),
    })
    const { result } = renderHook(() => useCommandRegistry(config))

    expect(findCommand(result.current, 'switch-ai-agent-codex')).toBeUndefined()
    expect(findCommand(result.current, 'switch-ai-agent-copilot')).toBeUndefined()
    expect(findCommand(result.current, 'switch-ai-agent-opencode')).toBeUndefined()
    expect(findCommand(result.current, 'switch-ai-agent-pi')).toBeUndefined()
    expect(findCommand(result.current, 'switch-ai-agent-antigravity')).toBeUndefined()
    expect(findCommand(result.current, 'switch-default-ai-agent')).toBeUndefined()
  })
})

describe('buildTypeCommands', () => {
  it('creates new and list commands for each type', () => {
    const onCreateNoteOfType = vi.fn()
    const onSelect = vi.fn()
    const commands = buildTypeCommands(['Project', 'Event'], onCreateNoteOfType, onSelect)
    expect(commands).toHaveLength(4)
    expect(commands[0].id).toBe('new-project')
    expect(commands[1].id).toBe('list-project')
    expect(commands[2].id).toBe('new-event')
    expect(commands[3].id).toBe('list-event')
  })

  it('omits the generic Note create command while keeping navigation for notes', () => {
    const onCreateNoteOfType = vi.fn()
    const onSelect = vi.fn()
    const commands = buildTypeCommands(['Note', 'Project'], onCreateNoteOfType, onSelect)

    expect(commands.map(command => command.id)).toEqual([
      'list-note',
      'new-project',
      'list-project',
    ])
  })
})
