/**
 * Tests for createEditorKeyHandler — the pure keyboard dispatch logic
 * extracted from useEditorKeyboardShortcuts.
 */

import { beforeEach, describe, expect, it, vi } from 'vitest';

// Mock @codemirror/search — handler calls openSearchPanel when view exists
vi.mock('@codemirror/search', () => ({
  openSearchPanel: vi.fn(),
}));

import { openSearchPanel } from '@codemirror/search';
import { createEditorKeyHandler } from '@renderer/hooks/useEditorKeyboardShortcuts';

import type { EditorKeyHandlerDeps } from '@renderer/hooks/useEditorKeyboardShortcuts';
import type { EditorFileTab } from '@shared/types/editor';

// =============================================================================
// Helpers
// =============================================================================

function createMockDeps(overrides: Partial<EditorKeyHandlerDeps> = {}): EditorKeyHandlerDeps {
  return {
    activeTabId: '/project/file1.ts',
    openTabs: [
      {
        id: '/project/file1.ts',
        filePath: '/project/file1.ts',
        fileName: 'file1.ts',
        language: 'typescript',
      },
      {
        id: '/project/file2.ts',
        filePath: '/project/file2.ts',
        fileName: 'file2.ts',
        language: 'typescript',
      },
      {
        id: '/project/file3.ts',
        filePath: '/project/file3.ts',
        fileName: 'file3.ts',
        language: 'typescript',
      },
    ] as EditorFileTab[],
    setActiveEditorTab: vi.fn(),
    saveFile: vi.fn().mockResolvedValue(undefined),
    saveAllFiles: vi.fn().mockResolvedValue(undefined),
    hasUnsavedChanges: vi.fn().mockReturnValue(false),
    onToggleQuickOpen: vi.fn(),
    onToggleSearchPanel: vi.fn(),
    onToggleGoToLine: vi.fn(),
    onToggleSidebar: vi.fn(),
    onToggleLineWrap: vi.fn(),
    getEditorView: vi.fn().mockReturnValue(null),
    ...overrides,
  };
}

/** Map a shortcut key to its physical KeyboardEvent.code value. */
function keyToCode(key: string): string {
  if (key.length === 1 && /[a-z]/i.test(key)) return `Key${key.toUpperCase()}`;
  if (key.length === 1 && /[0-9]/.test(key)) return `Digit${key}`;
  if (key === '[') return 'BracketLeft';
  if (key === ']') return 'BracketRight';
  if (key === '\\') return 'Backslash';
  if (key === ',') return 'Comma';
  if (key === '.') return 'Period';
  if (key === '/') return 'Slash';
  return key; // Tab, Enter, Escape, Arrow*, etc.
}

function createKeyEvent(key: string, opts: Partial<KeyboardEvent> = {}): KeyboardEvent {
  return new KeyboardEvent('keydown', {
    key,
    code: keyToCode(key),
    metaKey: opts.metaKey ?? true,
    ctrlKey: opts.ctrlKey ?? false,
    shiftKey: opts.shiftKey ?? false,
    altKey: opts.altKey ?? false,
    bubbles: true,
    cancelable: true,
  });
}

// =============================================================================
// Tests
// =============================================================================

describe('createEditorKeyHandler', () => {
  let deps: EditorKeyHandlerDeps;

  beforeEach(() => {
    vi.resetAllMocks();
    deps = createMockDeps();
  });

  it('ignores events without modifier key', () => {
    const handler = createEditorKeyHandler(deps);
    const event = new KeyboardEvent('keydown', { key: 'p', bubbles: true, cancelable: true });
    handler(event);
    expect(deps.onToggleQuickOpen).not.toHaveBeenCalled();
  });

  describe('Cmd+P — Quick Open', () => {
    it('calls onToggleQuickOpen', () => {
      const handler = createEditorKeyHandler(deps);
      const event = createKeyEvent('p');
      handler(event);
      expect(deps.onToggleQuickOpen).toHaveBeenCalledOnce();
      expect(event.defaultPrevented).toBe(true);
    });

    it('does not trigger with Shift', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('p', { shiftKey: true }));
      expect(deps.onToggleQuickOpen).not.toHaveBeenCalled();
    });
  });

  describe('Cmd+Shift+F — Search in Files', () => {
    it('calls onToggleSearchPanel', () => {
      const handler = createEditorKeyHandler(deps);
      const event = createKeyEvent('f', { shiftKey: true });
      handler(event);
      expect(deps.onToggleSearchPanel).toHaveBeenCalledOnce();
    });
  });

  describe('Cmd+F — Find in File (CM6)', () => {
    it('calls openSearchPanel when editor view exists', () => {
      const mockView = { dispatch: vi.fn() };
      deps = createMockDeps({ getEditorView: vi.fn().mockReturnValue(mockView) });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('f'));
      expect(openSearchPanel).toHaveBeenCalledWith(mockView);
    });

    it('does nothing when no editor view', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('f'));
      expect(openSearchPanel).not.toHaveBeenCalled();
    });
  });

  describe('Cmd+G — Go to Line', () => {
    it('calls onToggleGoToLine', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('g'));
      expect(deps.onToggleGoToLine).toHaveBeenCalled();
    });
  });

  describe('Cmd+S — Save', () => {
    it('calls saveFile with active tab id', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('s'));
      expect(deps.saveFile).toHaveBeenCalledWith('/project/file1.ts');
    });

    it('does nothing when no active tab', () => {
      deps = createMockDeps({ activeTabId: null });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('s'));
      expect(deps.saveFile).not.toHaveBeenCalled();
    });
  });

  describe('Cmd+Shift+S — Save All', () => {
    it('calls saveAllFiles when unsaved changes exist', () => {
      deps = createMockDeps({ hasUnsavedChanges: vi.fn().mockReturnValue(true) });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('s', { shiftKey: true }));
      expect(deps.saveAllFiles).toHaveBeenCalledOnce();
    });

    it('does nothing when no unsaved changes', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('s', { shiftKey: true }));
      expect(deps.saveAllFiles).not.toHaveBeenCalled();
    });
  });

  describe('Cmd+Shift+W — Toggle Line Wrap', () => {
    it('calls onToggleLineWrap', () => {
      const handler = createEditorKeyHandler(deps);
      const event = createKeyEvent('w', { shiftKey: true });
      handler(event);
      expect(deps.onToggleLineWrap).toHaveBeenCalledOnce();
      expect(event.defaultPrevented).toBe(true);
    });
  });

  describe('Cmd+W — Close Tab', () => {
    it('dispatches editor-close-tab CustomEvent with active tab id', () => {
      const handler = createEditorKeyHandler(deps);
      const eventSpy = vi.fn();
      window.addEventListener('editor-close-tab', eventSpy);

      handler(createKeyEvent('w'));

      expect(eventSpy).toHaveBeenCalledOnce();
      const detail = (eventSpy.mock.calls[0][0] as CustomEvent).detail;
      expect(detail).toBe('/project/file1.ts');

      window.removeEventListener('editor-close-tab', eventSpy);
    });

    it('does nothing with Alt modifier', () => {
      const handler = createEditorKeyHandler(deps);
      const eventSpy = vi.fn();
      window.addEventListener('editor-close-tab', eventSpy);

      handler(createKeyEvent('w', { altKey: true }));
      expect(eventSpy).not.toHaveBeenCalled();

      window.removeEventListener('editor-close-tab', eventSpy);
    });
  });

  describe('Cmd+B — Toggle Sidebar', () => {
    it('calls onToggleSidebar', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('b'));
      expect(deps.onToggleSidebar).toHaveBeenCalledOnce();
    });
  });

  describe('Cmd+Shift+] / [ — Tab Navigation', () => {
    it('moves to next tab with Cmd+Shift+]', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent(']', { shiftKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file2.ts');
    });

    it('wraps to first tab when on last', () => {
      deps = createMockDeps({ activeTabId: '/project/file3.ts' });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent(']', { shiftKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file1.ts');
    });

    it('moves to previous tab with Cmd+Shift+[', () => {
      deps = createMockDeps({ activeTabId: '/project/file2.ts' });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('[', { shiftKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file1.ts');
    });

    it('wraps to last tab when on first with Cmd+Shift+[', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('[', { shiftKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file3.ts');
    });
  });

  describe('Ctrl+Tab — Tab Cycling', () => {
    it('moves to next tab', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('Tab', { metaKey: false, ctrlKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file2.ts');
    });

    it('moves to previous tab with Shift', () => {
      deps = createMockDeps({ activeTabId: '/project/file2.ts' });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('Tab', { metaKey: false, ctrlKey: true, shiftKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file1.ts');
    });

    it('wraps forward on last tab', () => {
      deps = createMockDeps({ activeTabId: '/project/file3.ts' });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('Tab', { metaKey: false, ctrlKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file1.ts');
    });

    it('wraps backward on first tab', () => {
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent('Tab', { metaKey: false, ctrlKey: true, shiftKey: true }));
      expect(deps.setActiveEditorTab).toHaveBeenCalledWith('/project/file3.ts');
    });
  });

  describe('edge cases', () => {
    it('does nothing when openTabs is empty', () => {
      deps = createMockDeps({ openTabs: [], activeTabId: null });
      const handler = createEditorKeyHandler(deps);
      handler(createKeyEvent(']', { shiftKey: true }));
      expect(deps.setActiveEditorTab).not.toHaveBeenCalled();
    });

    it('stopPropagation is called on handled shortcuts', () => {
      const handler = createEditorKeyHandler(deps);
      const event = createKeyEvent('p');
      const spy = vi.spyOn(event, 'stopPropagation');
      handler(event);
      expect(spy).toHaveBeenCalledOnce();
    });
  });
});
