/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useSelectionList, } from './useSelectionList.js';
import { useKeypress } from './useKeypress.js';
vi.mock('./useKeypress.js');
let activeKeypressHandler = null;
describe('useSelectionList', () => {
    const mockOnSelect = vi.fn();
    const mockOnHighlight = vi.fn();
    const items = [
        { value: 'A' },
        { value: 'B', disabled: true },
        { value: 'C' },
        { value: 'D' },
    ];
    beforeEach(() => {
        activeKeypressHandler = null;
        vi.mocked(useKeypress).mockImplementation((handler, options) => {
            if (options?.isActive) {
                activeKeypressHandler = handler;
            }
            else {
                activeKeypressHandler = null;
            }
        });
        mockOnSelect.mockClear();
        mockOnHighlight.mockClear();
    });
    const pressKey = (name, sequence = name) => {
        act(() => {
            if (activeKeypressHandler) {
                const key = {
                    name,
                    sequence,
                    ctrl: false,
                    meta: false,
                    shift: false,
                    paste: false,
                };
                activeKeypressHandler(key);
            }
            else {
                throw new Error(`Test attempted to press key (${name}) but the keypress handler is not active. Ensure the hook is focused (isFocused=true) and the list is not empty.`);
            }
        });
    };
    describe('Initialization', () => {
        it('should initialize with the default index (0) if enabled', () => {
            const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
            expect(result.current.activeIndex).toBe(0);
        });
        it('should initialize with the provided initialIndex if enabled', () => {
            const { result } = renderHook(() => useSelectionList({
                items,
                initialIndex: 2,
                onSelect: mockOnSelect,
            }));
            expect(result.current.activeIndex).toBe(2);
        });
        it('should handle an empty list gracefully', () => {
            const { result } = renderHook(() => useSelectionList({ items: [], onSelect: mockOnSelect }));
            expect(result.current.activeIndex).toBe(0);
        });
        it('should find the next enabled item (downwards) if initialIndex is disabled', () => {
            const { result } = renderHook(() => useSelectionList({
                items,
                initialIndex: 1,
                onSelect: mockOnSelect,
            }));
            expect(result.current.activeIndex).toBe(2);
        });
        it('should wrap around to find the next enabled item if initialIndex is disabled', () => {
            const wrappingItems = [
                { value: 'A' },
                { value: 'B', disabled: true },
                { value: 'C', disabled: true },
            ];
            const { result } = renderHook(() => useSelectionList({
                items: wrappingItems,
                initialIndex: 2,
                onSelect: mockOnSelect,
            }));
            expect(result.current.activeIndex).toBe(0);
        });
        it('should default to 0 if initialIndex is out of bounds', () => {
            const { result } = renderHook(() => useSelectionList({
                items,
                initialIndex: 10,
                onSelect: mockOnSelect,
            }));
            expect(result.current.activeIndex).toBe(0);
            const { result: resultNeg } = renderHook(() => useSelectionList({
                items,
                initialIndex: -1,
                onSelect: mockOnSelect,
            }));
            expect(resultNeg.current.activeIndex).toBe(0);
        });
        it('should stick to the initial index if all items are disabled', () => {
            const allDisabled = [
                { value: 'A', disabled: true },
                { value: 'B', disabled: true },
            ];
            const { result } = renderHook(() => useSelectionList({
                items: allDisabled,
                initialIndex: 1,
                onSelect: mockOnSelect,
            }));
            expect(result.current.activeIndex).toBe(1);
        });
    });
    describe('Keyboard Navigation (Up/Down/J/K)', () => {
        it('should move down with "j" and "down" keys, skipping disabled items', () => {
            const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
            expect(result.current.activeIndex).toBe(0);
            pressKey('j');
            expect(result.current.activeIndex).toBe(2);
            pressKey('down');
            expect(result.current.activeIndex).toBe(3);
        });
        it('should move up with "k" and "up" keys, skipping disabled items', () => {
            const { result } = renderHook(() => useSelectionList({ items, initialIndex: 3, onSelect: mockOnSelect }));
            expect(result.current.activeIndex).toBe(3);
            pressKey('k');
            expect(result.current.activeIndex).toBe(2);
            pressKey('up');
            expect(result.current.activeIndex).toBe(0);
        });
        it('should wrap navigation correctly', () => {
            const { result } = renderHook(() => useSelectionList({
                items,
                initialIndex: items.length - 1,
                onSelect: mockOnSelect,
            }));
            expect(result.current.activeIndex).toBe(3);
            pressKey('down');
            expect(result.current.activeIndex).toBe(0);
            pressKey('up');
            expect(result.current.activeIndex).toBe(3);
        });
        it('should call onHighlight when index changes', () => {
            renderHook(() => useSelectionList({
                items,
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
            }));
            pressKey('down');
            expect(mockOnHighlight).toHaveBeenCalledTimes(1);
            expect(mockOnHighlight).toHaveBeenCalledWith('C');
        });
        it('should not move or call onHighlight if navigation results in the same index (e.g., single item)', () => {
            const singleItem = [{ value: 'A' }];
            const { result } = renderHook(() => useSelectionList({
                items: singleItem,
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
            }));
            pressKey('down');
            expect(result.current.activeIndex).toBe(0);
            expect(mockOnHighlight).not.toHaveBeenCalled();
        });
        it('should not move or call onHighlight if all items are disabled', () => {
            const allDisabled = [
                { value: 'A', disabled: true },
                { value: 'B', disabled: true },
            ];
            const { result } = renderHook(() => useSelectionList({
                items: allDisabled,
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
            }));
            const initialIndex = result.current.activeIndex;
            pressKey('down');
            expect(result.current.activeIndex).toBe(initialIndex);
            expect(mockOnHighlight).not.toHaveBeenCalled();
        });
    });
    describe('Selection (Enter)', () => {
        it('should call onSelect when "return" is pressed on enabled item', () => {
            renderHook(() => useSelectionList({
                items,
                initialIndex: 2,
                onSelect: mockOnSelect,
            }));
            pressKey('return');
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
            expect(mockOnSelect).toHaveBeenCalledWith('C');
        });
        it('should not call onSelect if the active item is disabled', () => {
            const { result } = renderHook(() => useSelectionList({
                items,
                onSelect: mockOnSelect,
            }));
            act(() => result.current.setActiveIndex(1));
            pressKey('return');
            expect(mockOnSelect).not.toHaveBeenCalled();
        });
    });
    describe('Keyboard Navigation Robustness (Rapid Input)', () => {
        it('should handle rapid navigation and selection robustly (avoiding stale state)', () => {
            const { result } = renderHook(() => useSelectionList({
                items, // A, B(disabled), C, D. Initial index 0 (A).
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
            }));
            // Simulate rapid inputs with separate act blocks to allow effects to run
            if (!activeKeypressHandler)
                throw new Error('Handler not active');
            const handler = activeKeypressHandler;
            const press = (name) => {
                const key = {
                    name,
                    sequence: name,
                    ctrl: false,
                    meta: false,
                    shift: false,
                    paste: false,
                };
                handler(key);
            };
            // 1. Press Down. Should move 0 (A) -> 2 (C).
            act(() => {
                press('down');
            });
            // 2. Press Down again. Should move 2 (C) -> 3 (D).
            act(() => {
                press('down');
            });
            // 3. Press Enter. Should select D.
            act(() => {
                press('return');
            });
            expect(result.current.activeIndex).toBe(3);
            expect(mockOnHighlight).toHaveBeenCalledTimes(2);
            expect(mockOnHighlight).toHaveBeenNthCalledWith(1, 'C');
            expect(mockOnHighlight).toHaveBeenNthCalledWith(2, 'D');
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
            expect(mockOnSelect).toHaveBeenCalledWith('D');
            expect(mockOnSelect).not.toHaveBeenCalledWith('A');
        });
        it('should handle ultra-rapid input (multiple presses in single act) without stale state', () => {
            const { result } = renderHook(() => useSelectionList({
                items, // A, B(disabled), C, D. Initial index 0 (A).
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
            }));
            // Simulate ultra-rapid inputs where all keypresses happen faster than React can re-render
            act(() => {
                if (!activeKeypressHandler)
                    throw new Error('Handler not active');
                const handler = activeKeypressHandler;
                const press = (name) => {
                    const key = {
                        name,
                        sequence: name,
                        ctrl: false,
                        meta: false,
                        shift: false,
                        paste: false,
                    };
                    handler(key);
                };
                // All presses happen in same render cycle - React batches the state updates
                press('down'); // Should move 0 (A) -> 2 (C)
                press('down'); // Should move 2 (C) -> 3 (D)
                press('return'); // Should select D
            });
            expect(result.current.activeIndex).toBe(3);
            expect(mockOnHighlight).toHaveBeenCalledWith('D');
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
            expect(mockOnSelect).toHaveBeenCalledWith('D');
        });
    });
    describe('Focus Management (isFocused)', () => {
        it('should activate the keypress handler when focused (default) and items exist', () => {
            const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
            expect(activeKeypressHandler).not.toBeNull();
            pressKey('down');
            expect(result.current.activeIndex).toBe(2);
        });
        it('should not activate the keypress handler when isFocused is false', () => {
            renderHook(() => useSelectionList({ items, onSelect: mockOnSelect, isFocused: false }));
            expect(activeKeypressHandler).toBeNull();
            expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
        });
        it('should not activate the keypress handler when items list is empty', () => {
            renderHook(() => useSelectionList({
                items: [],
                onSelect: mockOnSelect,
                isFocused: true,
            }));
            expect(activeKeypressHandler).toBeNull();
            expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
        });
        it('should activate/deactivate when isFocused prop changes', () => {
            const { result, rerender } = renderHook((props) => useSelectionList({ items, onSelect: mockOnSelect, ...props }), { initialProps: { isFocused: false } });
            expect(activeKeypressHandler).toBeNull();
            rerender({ isFocused: true });
            expect(activeKeypressHandler).not.toBeNull();
            pressKey('down');
            expect(result.current.activeIndex).toBe(2);
            rerender({ isFocused: false });
            expect(activeKeypressHandler).toBeNull();
            expect(() => pressKey('down')).toThrow(/keypress handler is not active/);
        });
    });
    describe('Numeric Quick Selection (showNumbers=true)', () => {
        beforeEach(() => {
            vi.useFakeTimers();
        });
        afterEach(() => {
            vi.useRealTimers();
        });
        const shortList = items;
        const longList = Array.from({ length: 15 }, (_, i) => ({ value: `Item ${i + 1}` }));
        const pressNumber = (num) => pressKey(num, num);
        it('should not respond to numbers if showNumbers is false (default)', () => {
            const { result } = renderHook(() => useSelectionList({ items: shortList, onSelect: mockOnSelect }));
            pressNumber('1');
            expect(result.current.activeIndex).toBe(0);
            expect(mockOnSelect).not.toHaveBeenCalled();
        });
        it('should select item immediately if the number cannot be extended (unambiguous)', () => {
            const { result } = renderHook(() => useSelectionList({
                items: shortList,
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
                showNumbers: true,
            }));
            pressNumber('3');
            expect(result.current.activeIndex).toBe(2);
            expect(mockOnHighlight).toHaveBeenCalledWith('C');
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
            expect(mockOnSelect).toHaveBeenCalledWith('C');
            expect(vi.getTimerCount()).toBe(0);
        });
        it('should highlight and wait for timeout if the number can be extended (ambiguous)', () => {
            const { result } = renderHook(() => useSelectionList({
                items: longList,
                initialIndex: 1, // Start at index 1 so pressing "1" (index 0) causes a change
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
                showNumbers: true,
            }));
            pressNumber('1');
            expect(result.current.activeIndex).toBe(0);
            expect(mockOnHighlight).toHaveBeenCalledWith('Item 1');
            expect(mockOnSelect).not.toHaveBeenCalled();
            expect(vi.getTimerCount()).toBe(1);
            act(() => {
                vi.advanceTimersByTime(1000);
            });
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
            expect(mockOnSelect).toHaveBeenCalledWith('Item 1');
        });
        it('should handle multi-digit input correctly', () => {
            const { result } = renderHook(() => useSelectionList({
                items: longList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressNumber('1');
            expect(mockOnSelect).not.toHaveBeenCalled();
            pressNumber('2');
            expect(result.current.activeIndex).toBe(11);
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
            expect(mockOnSelect).toHaveBeenCalledWith('Item 12');
        });
        it('should reset buffer if input becomes invalid (out of bounds)', () => {
            const { result } = renderHook(() => useSelectionList({
                items: shortList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressNumber('5');
            expect(result.current.activeIndex).toBe(0);
            expect(mockOnSelect).not.toHaveBeenCalled();
            pressNumber('3');
            expect(result.current.activeIndex).toBe(2);
            expect(mockOnSelect).toHaveBeenCalledWith('C');
        });
        it('should allow "0" as subsequent digit, but ignore as first digit', () => {
            const { result } = renderHook(() => useSelectionList({
                items: longList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressNumber('0');
            expect(result.current.activeIndex).toBe(0);
            expect(mockOnSelect).not.toHaveBeenCalled();
            // Timer should be running to clear the '0' input buffer
            expect(vi.getTimerCount()).toBe(1);
            // Press '1', then '0' (Item 10, index 9)
            pressNumber('1');
            pressNumber('0');
            expect(result.current.activeIndex).toBe(9);
            expect(mockOnSelect).toHaveBeenCalledWith('Item 10');
        });
        it('should clear the initial "0" input after timeout', () => {
            renderHook(() => useSelectionList({
                items: longList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressNumber('0');
            act(() => vi.advanceTimersByTime(1000)); // Timeout the '0' input
            pressNumber('1');
            expect(mockOnSelect).not.toHaveBeenCalled(); // Should be waiting for second digit
            act(() => vi.advanceTimersByTime(1000)); // Timeout '1'
            expect(mockOnSelect).toHaveBeenCalledWith('Item 1');
        });
        it('should highlight but not select a disabled item (immediate selection case)', () => {
            const { result } = renderHook(() => useSelectionList({
                items: shortList, // B (index 1, number 2) is disabled
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
                showNumbers: true,
            }));
            pressNumber('2');
            expect(result.current.activeIndex).toBe(1);
            expect(mockOnHighlight).toHaveBeenCalledWith('B');
            // Should not select immediately, even though 20 > 4
            expect(mockOnSelect).not.toHaveBeenCalled();
        });
        it('should highlight but not select a disabled item (timeout case)', () => {
            // Create a list where the ambiguous prefix points to a disabled item
            const disabledAmbiguousList = [
                { value: 'Item 1 Disabled', disabled: true },
                ...longList.slice(1),
            ];
            const { result } = renderHook(() => useSelectionList({
                items: disabledAmbiguousList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressNumber('1');
            expect(result.current.activeIndex).toBe(0);
            expect(vi.getTimerCount()).toBe(1);
            act(() => {
                vi.advanceTimersByTime(1000);
            });
            // Should not select after timeout
            expect(mockOnSelect).not.toHaveBeenCalled();
        });
        it('should clear the number buffer if a non-numeric key (e.g., navigation) is pressed', () => {
            const { result } = renderHook(() => useSelectionList({
                items: longList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressNumber('1');
            expect(vi.getTimerCount()).toBe(1);
            pressKey('down');
            expect(result.current.activeIndex).toBe(1);
            expect(vi.getTimerCount()).toBe(0);
            pressNumber('3');
            // Should select '3', not '13'
            expect(result.current.activeIndex).toBe(2);
        });
        it('should clear the number buffer if "return" is pressed', () => {
            renderHook(() => useSelectionList({
                items: longList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressNumber('1');
            pressKey('return');
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
            expect(vi.getTimerCount()).toBe(0);
            act(() => {
                vi.advanceTimersByTime(1000);
            });
            expect(mockOnSelect).toHaveBeenCalledTimes(1);
        });
    });
    describe('Reactivity (Dynamic Updates)', () => {
        it('should update activeIndex when initialIndex prop changes', () => {
            const { result, rerender } = renderHook(({ initialIndex }) => useSelectionList({
                items,
                onSelect: mockOnSelect,
                initialIndex,
            }), { initialProps: { initialIndex: 0 } });
            rerender({ initialIndex: 2 });
            expect(result.current.activeIndex).toBe(2);
        });
        it('should validate index when initialIndex prop changes to a disabled item', () => {
            const { result, rerender } = renderHook(({ initialIndex }) => useSelectionList({
                items,
                onSelect: mockOnSelect,
                initialIndex,
            }), { initialProps: { initialIndex: 0 } });
            rerender({ initialIndex: 1 });
            expect(result.current.activeIndex).toBe(2);
        });
        it('should adjust activeIndex if items change and the initialIndex is now out of bounds', () => {
            const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
                onSelect: mockOnSelect,
                initialIndex: 3,
                items: testItems,
            }), { initialProps: { items } });
            expect(result.current.activeIndex).toBe(3);
            const shorterItems = [{ value: 'X' }, { value: 'Y' }];
            rerender({ items: shorterItems }); // Length 2
            // The useEffect syncs based on the initialIndex (3) which is now out of bounds. It defaults to 0.
            expect(result.current.activeIndex).toBe(0);
        });
        it('should adjust activeIndex if items change and the initialIndex becomes disabled', () => {
            const initialItems = [{ value: 'A' }, { value: 'B' }, { value: 'C' }];
            const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
                onSelect: mockOnSelect,
                initialIndex: 1,
                items: testItems,
            }), { initialProps: { items: initialItems } });
            expect(result.current.activeIndex).toBe(1);
            const newItems = [
                { value: 'A' },
                { value: 'B', disabled: true },
                { value: 'C' },
            ];
            rerender({ items: newItems });
            expect(result.current.activeIndex).toBe(2);
        });
        it('should reset to 0 if items change to an empty list', () => {
            const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
                onSelect: mockOnSelect,
                initialIndex: 2,
                items: testItems,
            }), { initialProps: { items } });
            rerender({ items: [] });
            expect(result.current.activeIndex).toBe(0);
        });
        it('should not reset activeIndex when items are deeply equal', () => {
            const initialItems = [
                { value: 'A' },
                { value: 'B', disabled: true },
                { value: 'C' },
                { value: 'D' },
            ];
            const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
                initialIndex: 2,
                items: testItems,
            }), { initialProps: { items: initialItems } });
            expect(result.current.activeIndex).toBe(2);
            act(() => {
                result.current.setActiveIndex(3);
            });
            expect(result.current.activeIndex).toBe(3);
            mockOnHighlight.mockClear();
            // Create new array with same content (deeply equal but not identical)
            const newItems = [
                { value: 'A' },
                { value: 'B', disabled: true },
                { value: 'C' },
                { value: 'D' },
            ];
            rerender({ items: newItems });
            // Active index should remain the same since items are deeply equal
            expect(result.current.activeIndex).toBe(3);
            // onHighlight should NOT be called since the index didn't change
            expect(mockOnHighlight).not.toHaveBeenCalled();
        });
        it('should update activeIndex when items change structurally', () => {
            const initialItems = [
                { value: 'A' },
                { value: 'B', disabled: true },
                { value: 'C' },
                { value: 'D' },
            ];
            const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
                onSelect: mockOnSelect,
                onHighlight: mockOnHighlight,
                initialIndex: 3,
                items: testItems,
            }), { initialProps: { items: initialItems } });
            expect(result.current.activeIndex).toBe(3);
            mockOnHighlight.mockClear();
            // Change item values (not deeply equal)
            const newItems = [{ value: 'X' }, { value: 'Y' }, { value: 'Z' }];
            rerender({ items: newItems });
            // Active index should update based on initialIndex and new items
            expect(result.current.activeIndex).toBe(0);
        });
        it('should handle partial changes in items array', () => {
            const initialItems = [{ value: 'A' }, { value: 'B' }, { value: 'C' }];
            const { result, rerender } = renderHook(({ items: testItems }) => useSelectionList({
                onSelect: mockOnSelect,
                initialIndex: 1,
                items: testItems,
            }), { initialProps: { items: initialItems } });
            expect(result.current.activeIndex).toBe(1);
            // Change only one item's disabled status
            const newItems = [
                { value: 'A' },
                { value: 'B', disabled: true },
                { value: 'C' },
            ];
            rerender({ items: newItems });
            // Should find next valid index since current became disabled
            expect(result.current.activeIndex).toBe(2);
        });
    });
    describe('Manual Control', () => {
        it('should allow manual setting of active index via setActiveIndex', () => {
            const { result } = renderHook(() => useSelectionList({ items, onSelect: mockOnSelect }));
            act(() => {
                result.current.setActiveIndex(3);
            });
            expect(result.current.activeIndex).toBe(3);
            act(() => {
                result.current.setActiveIndex(1);
            });
            expect(result.current.activeIndex).toBe(1);
        });
    });
    describe('Cleanup', () => {
        beforeEach(() => {
            vi.useFakeTimers();
        });
        afterEach(() => {
            vi.useRealTimers();
        });
        it('should clear timeout on unmount when timer is active', () => {
            const longList = Array.from({ length: 15 }, (_, i) => ({ value: `Item ${i + 1}` }));
            const { unmount } = renderHook(() => useSelectionList({
                items: longList,
                onSelect: mockOnSelect,
                showNumbers: true,
            }));
            pressKey('1', '1');
            expect(vi.getTimerCount()).toBe(1);
            act(() => {
                vi.advanceTimersByTime(500);
            });
            expect(mockOnSelect).not.toHaveBeenCalled();
            unmount();
            expect(vi.getTimerCount()).toBe(0);
            act(() => {
                vi.advanceTimersByTime(1000);
            });
            expect(mockOnSelect).not.toHaveBeenCalled();
        });
    });
});
//# sourceMappingURL=useSelectionList.test.js.map