/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
  afterEach,
  type Mock,
} from 'vitest';
import type React from 'react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { useVim, type VimMode } from './vim.js';
import type { Key } from './useKeypress.js';
import {
  textBufferReducer,
  type TextBuffer,
  type TextBufferState,
  type TextBufferAction,
} from '../components/shared/text-buffer.js';

// Mock the VimModeContext
const mockVimContext = {
  vimEnabled: true,
  vimMode: 'INSERT' as VimMode,
  toggleVimEnabled: vi.fn(),
  setVimMode: vi.fn(),
};

vi.mock('../contexts/VimModeContext.js', () => ({
  useVimMode: () => mockVimContext,
  VimModeProvider: ({ children }: { children: React.ReactNode }) => children,
}));

// Helper to create a full Key object from partial data
const createKey = (partial: Partial<Key>): Key => ({
  name: partial.name || '',
  sequence: partial.sequence || '',
  shift: partial.shift || false,
  alt: partial.alt || false,
  ctrl: partial.ctrl || false,
  cmd: partial.cmd || false,
  insertable: partial.insertable || false,
  ...partial,
});

const createMockTextBufferState = (
  partial: Partial<TextBufferState>,
): TextBufferState => {
  const lines = partial.lines || [''];
  return {
    lines,
    cursorRow: 0,
    cursorCol: 0,
    preferredCol: null,
    undoStack: [],
    redoStack: [],
    clipboard: null,
    selectionAnchor: null,
    viewportWidth: 80,
    viewportHeight: 24,
    transformationsByLine: lines.map(() => []),
    visualLayout: {
      visualLines: lines,
      logicalToVisualMap: lines.map((_, i) => [[i, 0]]),
      visualToLogicalMap: lines.map((_, i) => [i, 0]),
      transformedToLogicalMaps: lines.map(() => []),
      visualToTransformedMap: [],
    },
    pastedContent: {},
    expandedPaste: null,
    yankRegister: null,
    ...partial,
  };
};
// Test constants
const TEST_SEQUENCES = {
  ESCAPE: createKey({ sequence: '\u001b', name: 'escape' }),
  LEFT: createKey({ sequence: 'h' }),
  RIGHT: createKey({ sequence: 'l' }),
  UP: createKey({ sequence: 'k' }),
  DOWN: createKey({ sequence: 'j' }),
  INSERT: createKey({ sequence: 'i' }),
  APPEND: createKey({ sequence: 'a' }),
  DELETE_CHAR: createKey({ sequence: 'x' }),
  DELETE: createKey({ sequence: 'd' }),
  CHANGE: createKey({ sequence: 'c' }),
  WORD_FORWARD: createKey({ sequence: 'w' }),
  WORD_BACKWARD: createKey({ sequence: 'b' }),
  WORD_END: createKey({ sequence: 'e' }),
  LINE_START: createKey({ sequence: '0' }),
  LINE_END: createKey({ sequence: '$' }),
  REPEAT: createKey({ sequence: '.' }),
  CTRL_C: createKey({ sequence: '\x03', name: 'c', ctrl: true }),
  CTRL_X: createKey({ sequence: '\x18', name: 'x', ctrl: true }),
  F12: createKey({ sequence: '\u001b[24~', name: 'f12' }),
} as const;

describe('useVim hook', async () => {
  let mockBuffer: Partial<TextBuffer>;
  let mockHandleFinalSubmit: Mock;

  const createMockBuffer = (
    text = 'hello world',
    cursor: [number, number] = [0, 5],
  ) => {
    const cursorState = { pos: cursor };
    const lines = text.split('\n');

    return {
      lines,
      get cursor() {
        return cursorState.pos;
      },
      set cursor(newPos: [number, number]) {
        cursorState.pos = newPos;
      },
      text,
      move: vi.fn().mockImplementation((direction: string) => {
        let [row, col] = cursorState.pos;
        const line = lines[row] || '';
        if (direction === 'left') {
          col = Math.max(0, col - 1);
        } else if (direction === 'right') {
          col = Math.min(line.length, col + 1);
        } else if (direction === 'home') {
          col = 0;
        } else if (direction === 'end') {
          col = line.length;
        }
        cursorState.pos = [row, col];
      }),
      del: vi.fn(),
      moveToOffset: vi.fn(),
      insert: vi.fn(),
      newline: vi.fn(),
      replaceRangeByOffset: vi.fn(),
      handleInput: vi.fn(),
      setText: vi.fn(),
      openInExternalEditor: vi.fn(),
      // Vim-specific methods
      vimDeleteWordForward: vi.fn(),
      vimDeleteWordBackward: vi.fn(),
      vimDeleteWordEnd: vi.fn(),
      vimChangeWordForward: vi.fn(),
      vimChangeWordBackward: vi.fn(),
      vimChangeWordEnd: vi.fn(),
      vimDeleteLine: vi.fn(),
      vimChangeLine: vi.fn(),
      vimDeleteToEndOfLine: vi.fn(),
      vimChangeToEndOfLine: vi.fn(),
      vimChangeMovement: vi.fn(),
      vimMoveLeft: vi.fn(),
      vimMoveRight: vi.fn(),
      vimMoveUp: vi.fn(),
      vimMoveDown: vi.fn(),
      vimMoveWordForward: vi.fn(),
      vimMoveWordBackward: vi.fn(),
      vimMoveWordEnd: vi.fn(),
      vimMoveBigWordForward: vi.fn(),
      vimMoveBigWordBackward: vi.fn(),
      vimMoveBigWordEnd: vi.fn(),
      vimDeleteBigWordForward: vi.fn(),
      vimDeleteBigWordBackward: vi.fn(),
      vimDeleteBigWordEnd: vi.fn(),
      vimChangeBigWordForward: vi.fn(),
      vimChangeBigWordBackward: vi.fn(),
      vimChangeBigWordEnd: vi.fn(),
      vimDeleteChar: vi.fn(),
      vimDeleteCharBefore: vi.fn(),
      vimToggleCase: vi.fn(),
      vimReplaceChar: vi.fn(),
      vimFindCharForward: vi.fn(),
      vimFindCharBackward: vi.fn(),
      vimDeleteToCharForward: vi.fn(),
      vimDeleteToCharBackward: vi.fn(),
      vimInsertAtCursor: vi.fn(),
      vimAppendAtCursor: vi.fn().mockImplementation(() => {
        // Append moves cursor right (vim 'a' behavior - position after current char)
        const [row, col] = cursorState.pos;
        // In vim, 'a' moves cursor to position after current character
        // This allows inserting at the end of the line
        cursorState.pos = [row, col + 1];
      }),
      vimOpenLineBelow: vi.fn(),
      vimOpenLineAbove: vi.fn(),
      vimAppendAtLineEnd: vi.fn(),
      vimInsertAtLineStart: vi.fn(),
      vimMoveToLineStart: vi.fn(),
      vimMoveToLineEnd: vi.fn(),
      vimMoveToFirstNonWhitespace: vi.fn(),
      vimMoveToFirstLine: vi.fn(),
      vimMoveToLastLine: vi.fn(),
      vimMoveToLine: vi.fn(),
      vimEscapeInsertMode: vi.fn().mockImplementation(() => {
        // Escape moves cursor left unless at beginning of line
        const [row, col] = cursorState.pos;
        if (col > 0) {
          cursorState.pos = [row, col - 1];
        }
      }),
      vimYankLine: vi.fn(),
      vimYankWordForward: vi.fn(),
      vimYankBigWordForward: vi.fn(),
      vimYankWordEnd: vi.fn(),
      vimYankBigWordEnd: vi.fn(),
      vimYankToEndOfLine: vi.fn(),
      vimPasteAfter: vi.fn(),
      vimPasteBefore: vi.fn(),
      // Additional properties for transformations
      transformedToLogicalMaps: lines.map(() => []),
      visualToTransformedMap: [],
      transformationsByLine: lines.map(() => []),
    };
  };

  const renderVimHook = async (buffer?: Partial<TextBuffer>) =>
    renderHook(() =>
      useVim((buffer || mockBuffer) as TextBuffer, mockHandleFinalSubmit),
    );

  const exitInsertMode = (result: {
    current: {
      handleInput: (key: Key) => boolean;
    };
  }) => {
    act(() => {
      result.current.handleInput(TEST_SEQUENCES.ESCAPE);
    });
  };

  beforeEach(() => {
    vi.clearAllMocks();
    mockHandleFinalSubmit = vi.fn();
    mockBuffer = createMockBuffer();
    // Reset mock context to default state
    mockVimContext.vimEnabled = true;
    mockVimContext.vimMode = 'INSERT';
    mockVimContext.toggleVimEnabled.mockClear();
    mockVimContext.setVimMode.mockClear();
  });

  describe('Mode switching', async () => {
    it('should start in INSERT mode', async () => {
      const { result } = await renderVimHook();
      expect(result.current.mode).toBe('INSERT');
    });

    it('should switch to INSERT mode with i command', async () => {
      const { result } = await renderVimHook();

      exitInsertMode(result);
      expect(result.current.mode).toBe('NORMAL');

      act(() => {
        result.current.handleInput(TEST_SEQUENCES.INSERT);
      });

      expect(result.current.mode).toBe('INSERT');
      expect(mockVimContext.setVimMode).toHaveBeenCalledWith('INSERT');
    });

    it('should switch back to NORMAL mode with Escape', async () => {
      const { result } = await renderVimHook();

      act(() => {
        result.current.handleInput(TEST_SEQUENCES.INSERT);
      });
      expect(result.current.mode).toBe('INSERT');

      exitInsertMode(result);
      expect(result.current.mode).toBe('NORMAL');
    });

    it('should properly handle escape followed immediately by a command', async () => {
      const testBuffer = createMockBuffer('hello world test', [0, 6]);
      const { result } = await renderVimHook(testBuffer);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'i' }));
      });
      expect(result.current.mode).toBe('INSERT');

      vi.clearAllMocks();

      exitInsertMode(result);
      expect(result.current.mode).toBe('NORMAL');

      act(() => {
        result.current.handleInput(createKey({ sequence: 'b' }));
      });

      expect(testBuffer.vimMoveWordBackward).toHaveBeenCalledWith(1);
    });
  });

  describe('Navigation commands', async () => {
    it('should handle h (left movement)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'h' }));
      });

      expect(mockBuffer.vimMoveLeft).toHaveBeenCalledWith(1);
    });

    it('should handle l (right movement)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'l' }));
      });

      expect(mockBuffer.vimMoveRight).toHaveBeenCalledWith(1);
    });

    it('should handle j (down movement)', async () => {
      const testBuffer = createMockBuffer('first line\nsecond line');
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'j' }));
      });

      expect(testBuffer.vimMoveDown).toHaveBeenCalledWith(1);
    });

    it('should handle k (up movement)', async () => {
      const testBuffer = createMockBuffer('first line\nsecond line');
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'k' }));
      });

      expect(testBuffer.vimMoveUp).toHaveBeenCalledWith(1);
    });

    it('should handle 0 (move to start of line)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: '0' }));
      });

      expect(mockBuffer.vimMoveToLineStart).toHaveBeenCalled();
    });

    it('should handle $ (move to end of line)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: '$' }));
      });

      expect(mockBuffer.vimMoveToLineEnd).toHaveBeenCalled();
    });
  });

  describe('Mode switching commands', async () => {
    it('should handle a (append after cursor)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'a' }));
      });

      expect(mockBuffer.vimAppendAtCursor).toHaveBeenCalled();
      expect(result.current.mode).toBe('INSERT');
    });

    it('should handle A (append at end of line)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'A' }));
      });

      expect(mockBuffer.vimAppendAtLineEnd).toHaveBeenCalled();
      expect(result.current.mode).toBe('INSERT');
    });

    it('should handle o (open line below)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'o' }));
      });

      expect(mockBuffer.vimOpenLineBelow).toHaveBeenCalled();
      expect(result.current.mode).toBe('INSERT');
    });

    it('should handle O (open line above)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'O' }));
      });

      expect(mockBuffer.vimOpenLineAbove).toHaveBeenCalled();
      expect(result.current.mode).toBe('INSERT');
    });
  });

  describe('Edit commands', async () => {
    it('should handle x (delete character)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      vi.clearAllMocks();

      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });

      expect(mockBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
    });

    it('should move cursor left when deleting last character on line (vim behavior)', async () => {
      const testBuffer = createMockBuffer('hello', [0, 4]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });

      expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
    });

    it('should handle first d key (sets pending state)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });

      expect(mockBuffer.replaceRangeByOffset).not.toHaveBeenCalled();
    });
  });

  describe('Count handling', async () => {
    it('should handle count input and return to count 0 after command', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        const handled = result.current.handleInput(
          createKey({ sequence: '3' }),
        );
        expect(handled).toBe(true);
      });

      act(() => {
        const handled = result.current.handleInput(
          createKey({ sequence: 'h' }),
        );
        expect(handled).toBe(true);
      });

      expect(mockBuffer.vimMoveLeft).toHaveBeenCalledWith(3);
    });

    it('should only delete 1 character with x command when no count is specified', async () => {
      const testBuffer = createMockBuffer();
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });

      expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
    });
  });

  describe('Word movement', async () => {
    it('should properly initialize vim hook with word movement support', async () => {
      const testBuffer = createMockBuffer('cat elephant mouse', [0, 0]);
      const { result } = await renderVimHook(testBuffer);

      expect(result.current.vimModeEnabled).toBe(true);
      expect(result.current.mode).toBe('INSERT');
      expect(result.current.handleInput).toBeDefined();
    });

    it('should support vim mode and basic operations across multiple lines', async () => {
      const testBuffer = createMockBuffer(
        'first line word\nsecond line word',
        [0, 11],
      );
      const { result } = await renderVimHook(testBuffer);

      expect(result.current.vimModeEnabled).toBe(true);
      expect(result.current.mode).toBe('INSERT');
      expect(result.current.handleInput).toBeDefined();
      expect(testBuffer.replaceRangeByOffset).toBeDefined();
      expect(testBuffer.moveToOffset).toBeDefined();
    });

    it('should handle w (next word)', async () => {
      const testBuffer = createMockBuffer('hello world test');
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'w' }));
      });

      expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(1);
    });

    it('should handle b (previous word)', async () => {
      const testBuffer = createMockBuffer('hello world test', [0, 6]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'b' }));
      });

      expect(testBuffer.vimMoveWordBackward).toHaveBeenCalledWith(1);
    });

    it('should handle e (end of word)', async () => {
      const testBuffer = createMockBuffer('hello world test');
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'e' }));
      });

      expect(testBuffer.vimMoveWordEnd).toHaveBeenCalledWith(1);
    });

    it('should handle w when cursor is on the last word', async () => {
      const testBuffer = createMockBuffer('hello world', [0, 8]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'w' }));
      });

      expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(1);
    });

    it('should handle first c key (sets pending change state)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'c' }));
      });

      expect(result.current.mode).toBe('NORMAL');
      expect(mockBuffer.del).not.toHaveBeenCalled();
    });

    it('should clear pending state on invalid command sequence (df)', async () => {
      const { result } = await renderVimHook();

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
        result.current.handleInput(createKey({ sequence: 'f' }));
      });

      expect(mockBuffer.replaceRangeByOffset).not.toHaveBeenCalled();
      expect(mockBuffer.del).not.toHaveBeenCalled();
    });

    it('should clear pending state with Escape in NORMAL mode', async () => {
      const { result } = await renderVimHook();

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });

      exitInsertMode(result);

      expect(mockBuffer.replaceRangeByOffset).not.toHaveBeenCalled();
    });
  });

  describe('Big Word movement', async () => {
    it('should handle W (next big word)', async () => {
      const testBuffer = createMockBuffer('hello world test');
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'W' }));
      });

      expect(testBuffer.vimMoveBigWordForward).toHaveBeenCalledWith(1);
    });

    it('should handle B (previous big word)', async () => {
      const testBuffer = createMockBuffer('hello world test', [0, 6]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'B' }));
      });

      expect(testBuffer.vimMoveBigWordBackward).toHaveBeenCalledWith(1);
    });

    it('should handle E (end of big word)', async () => {
      const testBuffer = createMockBuffer('hello world test');
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'E' }));
      });

      expect(testBuffer.vimMoveBigWordEnd).toHaveBeenCalledWith(1);
    });

    it('should handle dW (delete big word forward)', async () => {
      const testBuffer = createMockBuffer('hello.world test', [0, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'W' }));
      });

      expect(testBuffer.vimDeleteBigWordForward).toHaveBeenCalledWith(1);
    });

    it('should handle cW (change big word forward)', async () => {
      const testBuffer = createMockBuffer('hello.world test', [0, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'c' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'W' }));
      });

      expect(testBuffer.vimChangeBigWordForward).toHaveBeenCalledWith(1);
      expect(result.current.mode).toBe('INSERT');
    });

    it('should handle dB (delete big word backward)', async () => {
      const testBuffer = createMockBuffer('hello.world test', [0, 11]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'B' }));
      });

      expect(testBuffer.vimDeleteBigWordBackward).toHaveBeenCalledWith(1);
    });

    it('should handle dE (delete big word end)', async () => {
      const testBuffer = createMockBuffer('hello.world test', [0, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'E' }));
      });

      expect(testBuffer.vimDeleteBigWordEnd).toHaveBeenCalledWith(1);
    });
  });

  describe('Disabled vim mode', async () => {
    it('should not respond to vim commands when disabled', async () => {
      mockVimContext.vimEnabled = false;
      const { result } = await renderVimHook(mockBuffer);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'h' }));
      });

      expect(mockBuffer.move).not.toHaveBeenCalled();
    });
  });

  // These tests are no longer applicable at the hook level

  describe('Command repeat system', async () => {
    it('should repeat x command from current cursor position', async () => {
      const testBuffer = createMockBuffer('abcd\nefgh\nijkl', [0, 1]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });
      expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);

      testBuffer.cursor = [1, 2];

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });
      expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
    });

    it('should repeat dd command from current position', async () => {
      const testBuffer = createMockBuffer('line1\nline2\nline3', [1, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });
      expect(testBuffer.vimDeleteLine).toHaveBeenCalledTimes(1);

      testBuffer.cursor = [0, 0];

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });

      expect(testBuffer.vimDeleteLine).toHaveBeenCalledTimes(2);
    });

    it('should repeat ce command from current position', async () => {
      const testBuffer = createMockBuffer('word', [0, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'c' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'e' }));
      });
      expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledTimes(1);

      // Exit INSERT mode to complete the command
      exitInsertMode(result);

      testBuffer.cursor = [0, 2];

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });

      expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledTimes(2);
    });

    it('should repeat cc command from current position', async () => {
      const testBuffer = createMockBuffer('line1\nline2\nline3', [1, 2]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'c' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'c' }));
      });
      expect(testBuffer.vimChangeLine).toHaveBeenCalledTimes(1);

      // Exit INSERT mode to complete the command
      exitInsertMode(result);

      testBuffer.cursor = [0, 1];

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });

      expect(testBuffer.vimChangeLine).toHaveBeenCalledTimes(2);
    });

    it('should repeat cw command from current position', async () => {
      const testBuffer = createMockBuffer('hello world test', [0, 6]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'c' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'w' }));
      });
      expect(testBuffer.vimChangeWordForward).toHaveBeenCalledTimes(1);

      // Exit INSERT mode to complete the command
      exitInsertMode(result);

      testBuffer.cursor = [0, 0];

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });

      expect(testBuffer.vimChangeWordForward).toHaveBeenCalledTimes(2);
    });

    it('should repeat D command from current position', async () => {
      const testBuffer = createMockBuffer('hello world test', [0, 6]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'D' }));
      });
      expect(testBuffer.vimDeleteToEndOfLine).toHaveBeenCalledTimes(1);

      testBuffer.cursor = [0, 2];
      vi.clearAllMocks(); // Clear all mocks instead of just one method

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });

      expect(testBuffer.vimDeleteToEndOfLine).toHaveBeenCalledTimes(1);
    });

    it('should repeat C command from current position', async () => {
      const testBuffer = createMockBuffer('hello world test', [0, 6]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'C' }));
      });
      expect(testBuffer.vimChangeToEndOfLine).toHaveBeenCalledTimes(1);

      // Exit INSERT mode to complete the command
      exitInsertMode(result);

      testBuffer.cursor = [0, 2];

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });

      expect(testBuffer.vimChangeToEndOfLine).toHaveBeenCalledTimes(2);
    });

    it('should repeat command after cursor movement', async () => {
      const testBuffer = createMockBuffer('test text', [0, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });
      expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);

      testBuffer.cursor = [0, 2];

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });
      expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
    });

    it('should move cursor to the correct position after exiting INSERT mode with "a"', async () => {
      const testBuffer = createMockBuffer('hello world', [0, 11]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);
      expect(testBuffer.cursor).toEqual([0, 10]);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'a' }));
      });
      expect(result.current.mode).toBe('INSERT');
      expect(testBuffer.cursor).toEqual([0, 11]);

      exitInsertMode(result);
      expect(result.current.mode).toBe('NORMAL');
      expect(testBuffer.cursor).toEqual([0, 10]);
    });
  });

  describe('Special characters and edge cases', async () => {
    it('should handle ^ (move to first non-whitespace character)', async () => {
      const testBuffer = createMockBuffer('   hello world', [0, 5]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: '^' }));
      });

      expect(testBuffer.vimMoveToFirstNonWhitespace).toHaveBeenCalled();
    });

    it('should handle G without count (go to last line)', async () => {
      const testBuffer = createMockBuffer('line1\nline2\nline3', [0, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'G' }));
      });

      expect(testBuffer.vimMoveToLastLine).toHaveBeenCalled();
    });

    it('should handle gg (go to first line)', async () => {
      const testBuffer = createMockBuffer('line1\nline2\nline3', [2, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      // First 'g' sets pending state
      act(() => {
        result.current.handleInput(createKey({ sequence: 'g' }));
      });

      // Second 'g' executes the command
      act(() => {
        result.current.handleInput(createKey({ sequence: 'g' }));
      });

      expect(testBuffer.vimMoveToFirstLine).toHaveBeenCalled();
    });

    it('should handle count with movement commands', async () => {
      const testBuffer = createMockBuffer('hello world test', [0, 0]);
      const { result } = await renderVimHook(testBuffer);
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: '3' }));
      });

      act(() => {
        result.current.handleInput(TEST_SEQUENCES.WORD_FORWARD);
      });

      expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(3);
    });
  });

  describe('Vim word operations', async () => {
    describe('dw (delete word forward)', async () => {
      it('should delete from cursor to start of next word', async () => {
        const testBuffer = createMockBuffer('hello world test', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        expect(testBuffer.vimDeleteWordForward).toHaveBeenCalledWith(1);
      });

      it('should actually delete the complete word including trailing space', async () => {
        // This test uses the real text-buffer reducer instead of mocks
        const initialState = createMockTextBufferState({
          lines: ['hello world test'],
          cursorRow: 0,
          cursorCol: 0,
          preferredCol: null,
          undoStack: [],
          redoStack: [],
          clipboard: null,
          selectionAnchor: null,
        });

        const result = textBufferReducer(initialState, {
          type: 'vim_delete_word_forward',
          payload: { count: 1 },
        });

        // Should delete "hello " (word + space), leaving "world test"
        expect(result.lines).toEqual(['world test']);
        expect(result.cursorRow).toBe(0);
        expect(result.cursorCol).toBe(0);
      });

      it('should delete word from middle of word correctly', async () => {
        const initialState = createMockTextBufferState({
          lines: ['hello world test'],
          cursorRow: 0,
          cursorCol: 2, // cursor on 'l' in "hello"
          preferredCol: null,
          undoStack: [],
          redoStack: [],
          clipboard: null,
          selectionAnchor: null,
        });

        const result = textBufferReducer(initialState, {
          type: 'vim_delete_word_forward',
          payload: { count: 1 },
        });

        // Should delete "llo " (rest of word + space), leaving "he world test"
        expect(result.lines).toEqual(['heworld test']);
        expect(result.cursorRow).toBe(0);
        expect(result.cursorCol).toBe(2);
      });

      it('should handle dw at end of line', async () => {
        const initialState = createMockTextBufferState({
          lines: ['hello world'],
          cursorRow: 0,
          cursorCol: 6, // cursor on 'w' in "world"
          preferredCol: null,
          undoStack: [],
          redoStack: [],
          clipboard: null,
          selectionAnchor: null,
        });

        const result = textBufferReducer(initialState, {
          type: 'vim_delete_word_forward',
          payload: { count: 1 },
        });

        // Should delete "world" (no trailing space at end), leaving "hello "
        // Cursor clamps to last valid index in NORMAL mode (col 5 = trailing space)
        expect(result.lines).toEqual(['hello ']);
        expect(result.cursorRow).toBe(0);
        expect(result.cursorCol).toBe(5);
      });

      it('should delete multiple words with count', async () => {
        const testBuffer = createMockBuffer('one two three four', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: '2' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        expect(testBuffer.vimDeleteWordForward).toHaveBeenCalledWith(2);
      });

      it('should record command for repeat with dot', async () => {
        const testBuffer = createMockBuffer('hello world test', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        // Execute dw
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        vi.clearAllMocks();

        // Execute dot repeat
        act(() => {
          result.current.handleInput(createKey({ sequence: '.' }));
        });

        expect(testBuffer.vimDeleteWordForward).toHaveBeenCalledWith(1);
      });
    });

    describe('de (delete word end)', async () => {
      it('should delete from cursor to end of current word', async () => {
        const testBuffer = createMockBuffer('hello world test', [0, 1]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'e' }));
        });

        expect(testBuffer.vimDeleteWordEnd).toHaveBeenCalledWith(1);
      });

      it('should handle count with de', async () => {
        const testBuffer = createMockBuffer('one two three four', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: '3' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'e' }));
        });

        expect(testBuffer.vimDeleteWordEnd).toHaveBeenCalledWith(3);
      });
    });

    describe('cw (change word forward)', async () => {
      it('should change from cursor to start of next word and enter INSERT mode', async () => {
        const testBuffer = createMockBuffer('hello world test', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        expect(testBuffer.vimChangeWordForward).toHaveBeenCalledWith(1);
        expect(result.current.mode).toBe('INSERT');
        expect(mockVimContext.setVimMode).toHaveBeenCalledWith('INSERT');
      });

      it('should handle count with cw', async () => {
        const testBuffer = createMockBuffer('one two three four', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: '2' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        expect(testBuffer.vimChangeWordForward).toHaveBeenCalledWith(2);
        expect(result.current.mode).toBe('INSERT');
      });

      it('should be repeatable with dot', async () => {
        const testBuffer = createMockBuffer('hello world test more', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        // Execute cw
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        // Exit INSERT mode
        exitInsertMode(result);

        vi.clearAllMocks();
        mockVimContext.setVimMode.mockClear();

        // Execute dot repeat
        act(() => {
          result.current.handleInput(createKey({ sequence: '.' }));
        });

        expect(testBuffer.vimChangeWordForward).toHaveBeenCalledWith(1);
        expect(result.current.mode).toBe('INSERT');
      });
    });

    describe('ce (change word end)', async () => {
      it('should change from cursor to end of word and enter INSERT mode', async () => {
        const testBuffer = createMockBuffer('hello world test', [0, 1]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'e' }));
        });

        expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledWith(1);
        expect(result.current.mode).toBe('INSERT');
      });

      it('should handle count with ce', async () => {
        const testBuffer = createMockBuffer('one two three four', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: '2' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'e' }));
        });

        expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledWith(2);
        expect(result.current.mode).toBe('INSERT');
      });
    });

    describe('cc (change line)', async () => {
      it('should change entire line and enter INSERT mode', async () => {
        const testBuffer = createMockBuffer('hello world\nsecond line', [0, 5]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });

        expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(1);
        expect(result.current.mode).toBe('INSERT');
      });

      it('should change multiple lines with count', async () => {
        const testBuffer = createMockBuffer(
          'line1\nline2\nline3\nline4',
          [1, 0],
        );
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: '3' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });

        expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(3);
        expect(result.current.mode).toBe('INSERT');
      });

      it('should be repeatable with dot', async () => {
        const testBuffer = createMockBuffer('line1\nline2\nline3', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        // Execute cc
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });

        // Exit INSERT mode
        exitInsertMode(result);

        vi.clearAllMocks();
        mockVimContext.setVimMode.mockClear();

        // Execute dot repeat
        act(() => {
          result.current.handleInput(createKey({ sequence: '.' }));
        });

        expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(1);
        expect(result.current.mode).toBe('INSERT');
      });
    });

    describe('db (delete word backward)', async () => {
      it('should delete from cursor to start of previous word', async () => {
        const testBuffer = createMockBuffer('hello world test', [0, 11]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'b' }));
        });

        expect(testBuffer.vimDeleteWordBackward).toHaveBeenCalledWith(1);
      });

      it('should handle count with db', async () => {
        const testBuffer = createMockBuffer('one two three four', [0, 18]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: '2' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'b' }));
        });

        expect(testBuffer.vimDeleteWordBackward).toHaveBeenCalledWith(2);
      });
    });

    describe('cb (change word backward)', async () => {
      it('should change from cursor to start of previous word and enter INSERT mode', async () => {
        const testBuffer = createMockBuffer('hello world test', [0, 11]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'b' }));
        });

        expect(testBuffer.vimChangeWordBackward).toHaveBeenCalledWith(1);
        expect(result.current.mode).toBe('INSERT');
      });

      it('should handle count with cb', async () => {
        const testBuffer = createMockBuffer('one two three four', [0, 18]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        act(() => {
          result.current.handleInput(createKey({ sequence: '3' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'b' }));
        });

        expect(testBuffer.vimChangeWordBackward).toHaveBeenCalledWith(3);
        expect(result.current.mode).toBe('INSERT');
      });
    });

    describe('Pending state handling', async () => {
      it('should clear pending delete state after dw', async () => {
        const testBuffer = createMockBuffer('hello world', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        // Press 'd' to enter pending delete state
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });

        // Complete with 'w'
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        // Next 'd' should start a new pending state, not continue the previous one
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });

        // This should trigger dd (delete line), not an error
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });

        expect(testBuffer.vimDeleteLine).toHaveBeenCalledWith(1);
      });

      it('should clear pending change state after cw', async () => {
        const testBuffer = createMockBuffer('hello world', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        // Execute cw
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        // Exit INSERT mode
        exitInsertMode(result);

        // Next 'c' should start a new pending state
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'c' }));
        });

        expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(1);
      });

      it('should clear pending state with escape', async () => {
        const testBuffer = createMockBuffer('hello world', [0, 0]);
        const { result } = await renderVimHook(testBuffer);
        exitInsertMode(result);

        // Enter pending delete state
        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });

        // Press escape to clear pending state
        act(() => {
          result.current.handleInput(createKey({ name: 'escape' }));
        });

        // Now 'w' should just move cursor, not delete
        act(() => {
          result.current.handleInput(createKey({ sequence: 'w' }));
        });

        expect(testBuffer.vimDeleteWordForward).not.toHaveBeenCalled();
        // w should move to next word after clearing pending state
        expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(1);
      });
    });

    describe('NORMAL mode escape behavior', async () => {
      it('should pass escape through when no pending operator is active', async () => {
        mockVimContext.vimMode = 'NORMAL';
        const { result } = await renderVimHook();

        const handled = result.current.handleInput(
          createKey({ name: 'escape' }),
        );

        expect(handled).toBe(false);
      });

      it('should handle escape and clear pending operator', async () => {
        mockVimContext.vimMode = 'NORMAL';
        const { result } = await renderVimHook();

        act(() => {
          result.current.handleInput(createKey({ sequence: 'd' }));
        });

        let handled: boolean | undefined;
        act(() => {
          handled = result.current.handleInput(createKey({ name: 'escape' }));
        });

        expect(handled).toBe(true);
      });
    });
  });

  describe('Shell command pass-through', async () => {
    it('should pass through ctrl+r in INSERT mode', async () => {
      mockVimContext.vimMode = 'INSERT';
      const { result } = await renderVimHook();

      await waitFor(() => {
        expect(result.current.mode).toBe('INSERT');
      });

      const handled = result.current.handleInput(
        createKey({ name: 'r', ctrl: true }),
      );

      expect(handled).toBe(false);
    });

    it('should pass through ! in INSERT mode when buffer is empty', async () => {
      mockVimContext.vimMode = 'INSERT';
      const emptyBuffer = createMockBuffer('');
      const { result } = await renderVimHook(emptyBuffer);

      await waitFor(() => {
        expect(result.current.mode).toBe('INSERT');
      });

      const handled = result.current.handleInput(createKey({ sequence: '!' }));

      expect(handled).toBe(false);
    });

    it('should handle ! as input in INSERT mode when buffer is not empty', async () => {
      mockVimContext.vimMode = 'INSERT';
      const nonEmptyBuffer = createMockBuffer('not empty');
      const { result } = await renderVimHook(nonEmptyBuffer);

      await waitFor(() => {
        expect(result.current.mode).toBe('INSERT');
      });

      const key = createKey({ sequence: '!', name: '!' });

      act(() => {
        result.current.handleInput(key);
      });

      expect(nonEmptyBuffer.handleInput).toHaveBeenCalledWith(
        expect.objectContaining(key),
      );
    });
  });

  // Line operations (dd, cc) are tested in text-buffer.test.ts

  describe('Reducer-based integration tests', async () => {
    type VimActionType =
      | 'vim_delete_word_end'
      | 'vim_delete_word_backward'
      | 'vim_change_word_forward'
      | 'vim_change_word_end'
      | 'vim_change_word_backward'
      | 'vim_change_line'
      | 'vim_delete_line'
      | 'vim_delete_to_end_of_line'
      | 'vim_change_to_end_of_line';

    type VimReducerTestCase = {
      command: string;
      desc: string;
      lines: string[];
      cursorRow: number;
      cursorCol: number;
      actionType: VimActionType;
      count?: number;
      expectedLines: string[];
      expectedCursorRow: number;
      expectedCursorCol: number;
    };

    const testCases: VimReducerTestCase[] = [
      {
        command: 'de',
        desc: 'delete from cursor to end of current word',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 1,
        actionType: 'vim_delete_word_end' as const,
        count: 1,
        expectedLines: ['h world test'],
        expectedCursorRow: 0,
        expectedCursorCol: 1,
      },
      {
        command: 'de',
        desc: 'delete multiple word ends with count',
        lines: ['hello world test more'],
        cursorRow: 0,
        cursorCol: 1,
        actionType: 'vim_delete_word_end' as const,
        count: 2,
        expectedLines: ['h test more'],
        expectedCursorRow: 0,
        expectedCursorCol: 1,
      },
      {
        command: 'db',
        desc: 'delete from cursor to start of previous word',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 11,
        actionType: 'vim_delete_word_backward' as const,
        count: 1,
        expectedLines: ['hello  test'],
        expectedCursorRow: 0,
        expectedCursorCol: 6,
      },
      {
        command: 'db',
        desc: 'delete multiple words backward with count',
        lines: ['hello world test more'],
        cursorRow: 0,
        cursorCol: 17,
        actionType: 'vim_delete_word_backward' as const,
        count: 2,
        expectedLines: ['hello more'],
        expectedCursorRow: 0,
        expectedCursorCol: 6,
      },
      {
        command: 'cw',
        desc: 'delete from cursor to start of next word',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 0,
        actionType: 'vim_change_word_forward' as const,
        count: 1,
        expectedLines: ['world test'],
        expectedCursorRow: 0,
        expectedCursorCol: 0,
      },
      {
        command: 'cw',
        desc: 'change multiple words with count',
        lines: ['hello world test more'],
        cursorRow: 0,
        cursorCol: 0,
        actionType: 'vim_change_word_forward' as const,
        count: 2,
        expectedLines: ['test more'],
        expectedCursorRow: 0,
        expectedCursorCol: 0,
      },
      {
        command: 'ce',
        desc: 'change from cursor to end of current word',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 1,
        actionType: 'vim_change_word_end' as const,
        count: 1,
        expectedLines: ['h world test'],
        expectedCursorRow: 0,
        expectedCursorCol: 1,
      },
      {
        command: 'ce',
        desc: 'change multiple word ends with count',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 1,
        actionType: 'vim_change_word_end' as const,
        count: 2,
        expectedLines: ['h test'],
        expectedCursorRow: 0,
        expectedCursorCol: 1,
      },
      {
        command: 'cb',
        desc: 'change from cursor to start of previous word',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 11,
        actionType: 'vim_change_word_backward' as const,
        count: 1,
        expectedLines: ['hello  test'],
        expectedCursorRow: 0,
        expectedCursorCol: 6,
      },
      {
        command: 'cc',
        desc: 'clear the line and place cursor at the start',
        lines: ['  hello world'],
        cursorRow: 0,
        cursorCol: 5,
        actionType: 'vim_change_line' as const,
        count: 1,
        expectedLines: [''],
        expectedCursorRow: 0,
        expectedCursorCol: 0,
      },
      {
        command: 'dd',
        desc: 'delete the current line',
        lines: ['line1', 'line2', 'line3'],
        cursorRow: 1,
        cursorCol: 2,
        actionType: 'vim_delete_line' as const,
        count: 1,
        expectedLines: ['line1', 'line3'],
        expectedCursorRow: 1,
        expectedCursorCol: 0,
      },
      {
        command: 'dd',
        desc: 'delete multiple lines with count',
        lines: ['line1', 'line2', 'line3', 'line4'],
        cursorRow: 1,
        cursorCol: 2,
        actionType: 'vim_delete_line' as const,
        count: 2,
        expectedLines: ['line1', 'line4'],
        expectedCursorRow: 1,
        expectedCursorCol: 0,
      },
      {
        command: 'dd',
        desc: 'handle deleting last line',
        lines: ['only line'],
        cursorRow: 0,
        cursorCol: 3,
        actionType: 'vim_delete_line' as const,
        count: 1,
        expectedLines: [''],
        expectedCursorRow: 0,
        expectedCursorCol: 0,
      },
      {
        command: 'D',
        desc: 'delete from cursor to end of line',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 6,
        actionType: 'vim_delete_to_end_of_line' as const,
        count: 1,
        expectedLines: ['hello '],
        expectedCursorRow: 0,
        // Cursor clamps to last valid index in NORMAL mode (col 5 = trailing space)
        expectedCursorCol: 5,
      },
      {
        command: 'D',
        desc: 'handle D at end of line',
        lines: ['hello world'],
        cursorRow: 0,
        cursorCol: 11,
        actionType: 'vim_delete_to_end_of_line' as const,
        count: 1,
        expectedLines: ['hello world'],
        expectedCursorRow: 0,
        expectedCursorCol: 11,
      },
      {
        command: 'C',
        desc: 'change from cursor to end of line',
        lines: ['hello world test'],
        cursorRow: 0,
        cursorCol: 6,
        actionType: 'vim_change_to_end_of_line' as const,
        count: 1,
        expectedLines: ['hello '],
        expectedCursorRow: 0,
        expectedCursorCol: 6,
      },
      {
        command: 'C',
        desc: 'handle C at beginning of line',
        lines: ['hello world'],
        cursorRow: 0,
        cursorCol: 0,
        actionType: 'vim_change_to_end_of_line' as const,
        count: 1,
        expectedLines: [''],
        expectedCursorRow: 0,
        expectedCursorCol: 0,
      },
    ];

    it.each(testCases)(
      '$command: should $desc',
      ({
        lines,
        cursorRow,
        cursorCol,
        actionType,
        count,
        expectedLines,
        expectedCursorRow,
        expectedCursorCol,
      }: VimReducerTestCase) => {
        const initialState = createMockTextBufferState({
          lines,
          cursorRow,
          cursorCol,
          preferredCol: null,
          undoStack: [],
          redoStack: [],
          clipboard: null,
          selectionAnchor: null,
        });

        const action = (
          count
            ? { type: actionType, payload: { count } }
            : { type: actionType }
        ) as TextBufferAction;

        const result = textBufferReducer(initialState, action);

        expect(result.lines).toEqual(expectedLines);
        expect(result.cursorRow).toBe(expectedCursorRow);
        expect(result.cursorCol).toBe(expectedCursorCol);
      },
    );
  });

  describe('double-escape to clear buffer', async () => {
    beforeEach(() => {
      mockBuffer = createMockBuffer('hello world');
      mockVimContext.vimEnabled = true;
      mockVimContext.vimMode = 'INSERT';
      mockHandleFinalSubmit = vi.fn();
      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
    });

    it('should clear buffer on double-escape in NORMAL mode', async () => {
      const { result } = await renderHook(() =>
        useVim(mockBuffer as TextBuffer, mockHandleFinalSubmit),
      );
      exitInsertMode(result);
      // Wait to clear escape history
      await act(async () => {
        vi.advanceTimersByTime(600);
      });

      // First escape - should pass through (return false)
      let handled: boolean;
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });
      expect(handled!).toBe(false);

      // Second escape within timeout - should clear buffer (return true)
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });
      expect(handled!).toBe(true);
      expect(mockBuffer.setText).toHaveBeenCalledWith('');
    });

    it('should clear buffer on double-escape in INSERT mode', async () => {
      const { result } = await renderHook(() =>
        useVim(mockBuffer as TextBuffer, mockHandleFinalSubmit),
      );

      // First escape - switches to NORMAL mode
      let handled: boolean;
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });
      expect(handled!).toBe(true);
      expect(mockBuffer.vimEscapeInsertMode).toHaveBeenCalled();

      // Second escape within timeout - should clear buffer
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });
      expect(handled!).toBe(true);
      expect(mockBuffer.setText).toHaveBeenCalledWith('');
    });

    it('should NOT clear buffer if escapes are too slow', async () => {
      const { result } = await renderHook(() =>
        useVim(mockBuffer as TextBuffer, mockHandleFinalSubmit),
      );
      exitInsertMode(result);
      // Wait to clear escape history
      await act(async () => {
        vi.advanceTimersByTime(600);
      });

      // First escape
      await act(async () => {
        result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });

      // Wait longer than timeout (500ms)
      await act(async () => {
        vi.advanceTimersByTime(600);
      });

      // Second escape - should NOT clear buffer because timeout expired
      let handled: boolean;
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });
      // First escape of new sequence, passes through
      expect(handled!).toBe(false);
      expect(mockBuffer.setText).not.toHaveBeenCalled();
    });

    it('should clear escape history when clearing pending operator', async () => {
      const { result } = await renderHook(() =>
        useVim(mockBuffer as TextBuffer, mockHandleFinalSubmit),
      );
      exitInsertMode(result);
      // Wait to clear escape history
      await act(async () => {
        vi.advanceTimersByTime(600);
      });

      // First escape
      await act(async () => {
        result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });

      // Type 'd' to set pending operator
      await act(async () => {
        result.current.handleInput(TEST_SEQUENCES.DELETE);
      });

      // Escape to clear pending operator
      await act(async () => {
        result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });

      // Another escape - should NOT clear buffer (history was reset)
      let handled: boolean;
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.ESCAPE);
      });
      expect(handled!).toBe(false);
      expect(mockBuffer.setText).not.toHaveBeenCalled();
    });

    it('should pass Ctrl+C through to InputPrompt in NORMAL mode', async () => {
      const { result } = await renderHook(() =>
        useVim(mockBuffer as TextBuffer, mockHandleFinalSubmit),
      );
      exitInsertMode(result);

      let handled: boolean;
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.CTRL_C);
      });
      // Should return false to let InputPrompt handle it
      expect(handled!).toBe(false);
    });

    it('should pass Ctrl+C through to InputPrompt in INSERT mode', async () => {
      const { result } = await renderHook(() =>
        useVim(mockBuffer as TextBuffer, mockHandleFinalSubmit),
      );

      let handled: boolean;
      await act(async () => {
        handled = result.current.handleInput(TEST_SEQUENCES.CTRL_C);
      });
      // Should return false to let InputPrompt handle it
      expect(handled!).toBe(false);
    });
  });

  describe('Character deletion and case toggle (X, ~)', async () => {
    it('X: should call vimDeleteCharBefore', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      let handled: boolean;
      act(() => {
        handled = result.current.handleInput(createKey({ sequence: 'X' }));
      });

      expect(handled!).toBe(true);
      expect(mockBuffer.vimDeleteCharBefore).toHaveBeenCalledWith(1);
    });

    it('~: should call vimToggleCase', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      let handled: boolean;
      act(() => {
        handled = result.current.handleInput(createKey({ sequence: '~' }));
      });

      expect(handled!).toBe(true);
      expect(mockBuffer.vimToggleCase).toHaveBeenCalledWith(1);
    });

    it('X can be repeated with dot (.)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'X' }));
      });
      expect(mockBuffer.vimDeleteCharBefore).toHaveBeenCalledTimes(1);

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });
      expect(mockBuffer.vimDeleteCharBefore).toHaveBeenCalledTimes(2);
    });

    it('~ can be repeated with dot (.)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: '~' }));
      });
      expect(mockBuffer.vimToggleCase).toHaveBeenCalledTimes(1);

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });
      expect(mockBuffer.vimToggleCase).toHaveBeenCalledTimes(2);
    });

    it('3X calls vimDeleteCharBefore with count=3', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: '3' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'X' }));
      });
      expect(mockBuffer.vimDeleteCharBefore).toHaveBeenCalledWith(3);
    });

    it('2~ calls vimToggleCase with count=2', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: '2' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: '~' }));
      });
      expect(mockBuffer.vimToggleCase).toHaveBeenCalledWith(2);
    });
  });

  describe('Replace character (r)', async () => {
    it('r{char}: should call vimReplaceChar with the next key', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'r' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });

      expect(mockBuffer.vimReplaceChar).toHaveBeenCalledWith('x', 1);
    });

    it('r: should consume the pending char without passing through', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      let rHandled: boolean;
      let charHandled: boolean;
      act(() => {
        rHandled = result.current.handleInput(createKey({ sequence: 'r' }));
      });
      act(() => {
        charHandled = result.current.handleInput(createKey({ sequence: 'a' }));
      });

      expect(rHandled!).toBe(true);
      expect(charHandled!).toBe(true);
      expect(mockBuffer.vimReplaceChar).toHaveBeenCalledWith('a', 1);
    });

    it('Escape cancels pending r (pendingFindOp cleared on Esc)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'r' }));
      });
      act(() => {
        result.current.handleInput(
          createKey({ sequence: '\u001b', name: 'escape' }),
        );
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'a' }));
      });

      expect(mockBuffer.vimReplaceChar).not.toHaveBeenCalled();
    });

    it('2rx calls vimReplaceChar with count=2', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: '2' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'r' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });
      expect(mockBuffer.vimReplaceChar).toHaveBeenCalledWith('x', 2);
    });

    it('r{char} is dot-repeatable', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'r' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'z' }));
      });
      expect(mockBuffer.vimReplaceChar).toHaveBeenCalledWith('z', 1);

      act(() => {
        result.current.handleInput(createKey({ sequence: '.' }));
      });
      expect(mockBuffer.vimReplaceChar).toHaveBeenCalledTimes(2);
      expect(mockBuffer.vimReplaceChar).toHaveBeenLastCalledWith('z', 1);
    });
  });

  describe('Character find motions (f, F, t, T, ;, ,)', async () => {
    type FindCase = {
      key: string;
      char: string;
      mockFn: 'vimFindCharForward' | 'vimFindCharBackward';
      till: boolean;
    };
    it.each<FindCase>([
      { key: 'f', char: 'o', mockFn: 'vimFindCharForward', till: false },
      { key: 'F', char: 'o', mockFn: 'vimFindCharBackward', till: false },
      { key: 't', char: 'w', mockFn: 'vimFindCharForward', till: true },
      { key: 'T', char: 'w', mockFn: 'vimFindCharBackward', till: true },
    ])(
      '$key{char}: calls $mockFn (till=$till)',
      async ({ key, char, mockFn, till }) => {
        const { result } = await renderVimHook();
        exitInsertMode(result);
        act(() => {
          result.current.handleInput(createKey({ sequence: key }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: char }));
        });
        expect(mockBuffer[mockFn]).toHaveBeenCalledWith(char, 1, till);
      },
    );

    it(';: should repeat last f forward find', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      // f o
      act(() => {
        result.current.handleInput(createKey({ sequence: 'f' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'o' }));
      });
      // ;
      act(() => {
        result.current.handleInput(createKey({ sequence: ';' }));
      });

      expect(mockBuffer.vimFindCharForward).toHaveBeenCalledTimes(2);
      expect(mockBuffer.vimFindCharForward).toHaveBeenLastCalledWith(
        'o',
        1,
        false,
      );
    });

    it(',: should repeat last f find in reverse direction', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      // f o
      act(() => {
        result.current.handleInput(createKey({ sequence: 'f' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'o' }));
      });
      // ,
      act(() => {
        result.current.handleInput(createKey({ sequence: ',' }));
      });

      expect(mockBuffer.vimFindCharBackward).toHaveBeenCalledWith(
        'o',
        1,
        false,
      );
    });

    it('; and , should do nothing if no prior find', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: ';' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: ',' }));
      });

      expect(mockBuffer.vimFindCharForward).not.toHaveBeenCalled();
      expect(mockBuffer.vimFindCharBackward).not.toHaveBeenCalled();
    });

    it('Escape cancels pending f (pendingFindOp cleared on Esc)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'f' }));
      });
      act(() => {
        result.current.handleInput(
          createKey({ sequence: '\u001b', name: 'escape' }),
        );
      });
      // o should NOT be consumed as find target
      act(() => {
        result.current.handleInput(createKey({ sequence: 'o' }));
      });

      expect(mockBuffer.vimFindCharForward).not.toHaveBeenCalled();
    });

    it('2fo calls vimFindCharForward with count=2', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: '2' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'f' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'o' }));
      });
      expect(mockBuffer.vimFindCharForward).toHaveBeenCalledWith('o', 2, false);
    });
  });

  describe('should handle unmapped keys in Normal mode', () => {
    type UnmappedKeyCase = {
      char: string;
      insertable: boolean;
    };
    it.each<UnmappedKeyCase>([
      { char: 'm', insertable: true },
      { char: 'n', insertable: true },
      { char: 'p', insertable: true },
      { char: 'q', insertable: true },
      { char: 's', insertable: true },
      { char: 'v', insertable: true },
      { char: 'y', insertable: true },
      { char: 'z', insertable: true },
      { char: 'H', insertable: true },
      { char: 'J', insertable: true },
      { char: 'K', insertable: true },
      { char: 'L', insertable: true },
      { char: 'M', insertable: true },
      { char: 'N', insertable: true },
      { char: 'P', insertable: true },
      { char: 'Q', insertable: true },
      { char: 'R', insertable: true },
      { char: 'S', insertable: true },
      { char: 'U', insertable: true },
      { char: 'V', insertable: true },
      { char: 'Y', insertable: true },
      { char: 'Z', insertable: true },
      { char: '/', insertable: true },
      { char: '#', insertable: true },
      { char: '%', insertable: true },
      { char: '&', insertable: true },
      { char: "'", insertable: true },
      { char: '(', insertable: true },
      { char: ')', insertable: true },
      { char: '*', insertable: true },
      { char: '+', insertable: true },
      { char: '-', insertable: true },
      { char: '/', insertable: true },
      { char: ':', insertable: true },
      { char: '<', insertable: true },
      { char: '=', insertable: true },
      { char: '>', insertable: true },
      { char: '@', insertable: true },
      { char: '[', insertable: true },
      { char: '\\', insertable: true },
      { char: ']', insertable: true },
      { char: '_', insertable: true },
      { char: '`', insertable: true },
      { char: '{', insertable: true },
      { char: '|', insertable: true },
      { char: '}', insertable: true },
    ])(
      '$char: should be swallowed and do nothing in Normal mode',
      async ({ char, insertable }) => {
        const { result } = await renderVimHook();
        exitInsertMode(result);

        let handled = false;
        act(() => {
          handled = result.current.handleInput(
            createKey({ sequence: char, name: char, insertable }),
          );
        });

        expect(handled).toBe(true);
        expect(mockVimContext.setVimMode).not.toHaveBeenCalledWith('INSERT');

        expect(mockBuffer.vimFindCharForward).not.toHaveBeenCalled();
        expect(mockBuffer.vimFindCharBackward).not.toHaveBeenCalled();
      },
    );
  });

  describe('Operator + find motions (df, dt, dF, dT, cf, ct, cF, cT)', async () => {
    it('df{char}: executes delete-to-char, not a dangling operator', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);

      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'f' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'x' }));
      });

      expect(mockBuffer.vimDeleteToCharForward).toHaveBeenCalledWith(
        'x',
        1,
        false,
      );
      expect(mockBuffer.vimFindCharForward).not.toHaveBeenCalled();

      // Next key is a fresh normal-mode command — no dangling state
      act(() => {
        result.current.handleInput(createKey({ sequence: 'l' }));
      });
      expect(mockBuffer.vimMoveRight).toHaveBeenCalledWith(1);
    });

    // operator + find/till motions (df, dt, dF, dT, cf, ct, ...)
    type OperatorFindCase = {
      operator: string;
      findKey: string;
      mockFn: 'vimDeleteToCharForward' | 'vimDeleteToCharBackward';
      till: boolean;
      entersInsert: boolean;
    };
    it.each<OperatorFindCase>([
      {
        operator: 'd',
        findKey: 'f',
        mockFn: 'vimDeleteToCharForward',
        till: false,
        entersInsert: false,
      },
      {
        operator: 'd',
        findKey: 't',
        mockFn: 'vimDeleteToCharForward',
        till: true,
        entersInsert: false,
      },
      {
        operator: 'd',
        findKey: 'F',
        mockFn: 'vimDeleteToCharBackward',
        till: false,
        entersInsert: false,
      },
      {
        operator: 'd',
        findKey: 'T',
        mockFn: 'vimDeleteToCharBackward',
        till: true,
        entersInsert: false,
      },
      {
        operator: 'c',
        findKey: 'f',
        mockFn: 'vimDeleteToCharForward',
        till: false,
        entersInsert: true,
      },
      {
        operator: 'c',
        findKey: 't',
        mockFn: 'vimDeleteToCharForward',
        till: true,
        entersInsert: true,
      },
      {
        operator: 'c',
        findKey: 'F',
        mockFn: 'vimDeleteToCharBackward',
        till: false,
        entersInsert: true,
      },
      {
        operator: 'c',
        findKey: 'T',
        mockFn: 'vimDeleteToCharBackward',
        till: true,
        entersInsert: true,
      },
    ])(
      '$operator$findKey{char}: calls $mockFn (till=$till, insert=$entersInsert)',
      async ({ operator, findKey, mockFn, till, entersInsert }) => {
        const { result } = await renderVimHook();
        exitInsertMode(result);
        act(() => {
          result.current.handleInput(createKey({ sequence: operator }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: findKey }));
        });
        act(() => {
          result.current.handleInput(createKey({ sequence: 'o' }));
        });
        expect(mockBuffer[mockFn]).toHaveBeenCalledWith('o', 1, till);
        if (entersInsert) {
          expect(mockVimContext.setVimMode).toHaveBeenCalledWith('INSERT');
        }
      },
    );

    it('2df{char}: count is passed through to vimDeleteToCharForward', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: '2' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'd' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'f' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'o' }));
      });
      expect(mockBuffer.vimDeleteToCharForward).toHaveBeenCalledWith(
        'o',
        2,
        false,
      );
    });
  });

  describe('Yank and paste (y/p/P)', async () => {
    it('should handle yy (yank line)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      expect(mockBuffer.vimYankLine).toHaveBeenCalledWith(1);
    });

    it('should handle 2yy (yank 2 lines)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: '2' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      expect(mockBuffer.vimYankLine).toHaveBeenCalledWith(2);
    });

    it('should handle Y (yank to end of line, equivalent to y$)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'Y' }));
      });
      expect(mockBuffer.vimYankToEndOfLine).toHaveBeenCalledWith(1);
    });

    it('should handle yw (yank word forward)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'w' }));
      });
      expect(mockBuffer.vimYankWordForward).toHaveBeenCalledWith(1);
    });

    it('should handle yW (yank big word forward)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'W' }));
      });
      expect(mockBuffer.vimYankBigWordForward).toHaveBeenCalledWith(1);
    });

    it('should handle ye (yank to end of word)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'e' }));
      });
      expect(mockBuffer.vimYankWordEnd).toHaveBeenCalledWith(1);
    });

    it('should handle yE (yank to end of big word)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'E' }));
      });
      expect(mockBuffer.vimYankBigWordEnd).toHaveBeenCalledWith(1);
    });

    it('should handle y$ (yank to end of line)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'y' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: '$' }));
      });
      expect(mockBuffer.vimYankToEndOfLine).toHaveBeenCalledWith(1);
    });

    it('should handle p (paste after)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'p' }));
      });
      expect(mockBuffer.vimPasteAfter).toHaveBeenCalledWith(1);
    });

    it('should handle 2p (paste after, count 2)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: '2' }));
      });
      act(() => {
        result.current.handleInput(createKey({ sequence: 'p' }));
      });
      expect(mockBuffer.vimPasteAfter).toHaveBeenCalledWith(2);
    });

    it('should handle P (paste before)', async () => {
      const { result } = await renderVimHook();
      exitInsertMode(result);
      act(() => {
        result.current.handleInput(createKey({ sequence: 'P' }));
      });
      expect(mockBuffer.vimPasteBefore).toHaveBeenCalledWith(1);
    });

    // Integration tests using actual textBufferReducer to verify full state changes
    it('should duplicate a line below with yy then p', async () => {
      const initialState = createMockTextBufferState({
        lines: ['hello', 'world'],
        cursorRow: 0,
        cursorCol: 0,
      });
      // Simulate yy action
      let state = textBufferReducer(initialState, {
        type: 'vim_yank_line',
        payload: { count: 1 },
      });
      expect(state.yankRegister).toEqual({ text: 'hello', linewise: true });
      expect(state.lines).toEqual(['hello', 'world']); // unchanged

      // Simulate p action
      state = textBufferReducer(state, {
        type: 'vim_paste_after',
        payload: { count: 1 },
      });
      expect(state.lines).toEqual(['hello', 'hello', 'world']);
      expect(state.cursorRow).toBe(1);
      expect(state.cursorCol).toBe(0);
    });

    it('should paste a yanked word after cursor with yw then p', async () => {
      const initialState = createMockTextBufferState({
        lines: ['hello world'],
        cursorRow: 0,
        cursorCol: 0,
      });
      // Simulate yw action
      let state = textBufferReducer(initialState, {
        type: 'vim_yank_word_forward',
        payload: { count: 1 },
      });
      expect(state.yankRegister).toEqual({ text: 'hello ', linewise: false });
      expect(state.lines).toEqual(['hello world']); // unchanged

      // Move cursor to col 6 (start of 'world') and paste
      state = { ...state, cursorCol: 6 };
      state = textBufferReducer(state, {
        type: 'vim_paste_after',
        payload: { count: 1 },
      });
      // 'hello world' with paste after col 6 (between 'w' and 'o')
      // insert 'hello ' at col 7, result: 'hello whello orld'
      expect(state.lines[0]).toContain('hello ');
    });

    it('should move a word forward with dw then p', async () => {
      const initialState = createMockTextBufferState({
        lines: ['hello world'],
        cursorRow: 0,
        cursorCol: 0,
      });
      // Simulate dw (delete word, populates register)
      let state = textBufferReducer(initialState, {
        type: 'vim_delete_word_forward',
        payload: { count: 1 },
      });
      expect(state.yankRegister).toEqual({ text: 'hello ', linewise: false });
      expect(state.lines[0]).toBe('world');

      // Paste at end of 'world' (after last char)
      state = { ...state, cursorCol: 4 };
      state = textBufferReducer(state, {
        type: 'vim_paste_after',
        payload: { count: 1 },
      });
      expect(state.lines[0]).toContain('hello');
    });
  });
});
