/**
 * @license
 * Copyright 2026 HopCode Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import * as child_process from 'child_process';
// --- Mock child_process (auto-mock, then override exec in beforeEach) ---
vi.mock('child_process');
// --- Mock context hooks ---
const mockSettings = {
    merged: {},
};
vi.mock('../contexts/SettingsContext.js', () => ({
    useSettings: () => mockSettings,
}));
const mockUIState = {
    sessionStats: {
        sessionId: 'test-session',
        lastPromptTokenCount: 100,
        metrics: {
            models: {},
            tools: { totalCalls: 0 },
            files: { totalLinesAdded: 0, totalLinesRemoved: 0 },
        },
    },
    currentModel: 'test-model',
    branchName: 'main',
};
vi.mock('../contexts/UIStateContext.js', () => ({
    useUIState: () => mockUIState,
}));
const mockConfig = {
    getTargetDir: vi.fn(() => '/test/dir'),
    getModel: vi.fn(() => 'test-model'),
    getCliVersion: vi.fn(() => '1.0.0'),
    getContentGeneratorConfig: vi.fn(() => ({ contextWindowSize: 131072 })),
};
vi.mock('../contexts/ConfigContext.js', () => ({
    useConfig: () => mockConfig,
}));
const mockVimMode = {
    vimEnabled: false,
    vimMode: 'INSERT',
};
vi.mock('../contexts/VimModeContext.js', () => ({
    useVimMode: () => mockVimMode,
}));
vi.mock('@hoptrendy/hopcode-core', async (importOriginal) => {
    const original = await importOriginal();
    return {
        ...original,
        createDebugLogger: () => ({
            log: vi.fn(),
            error: vi.fn(),
        }),
    };
});
let execCallback;
let lastExecCommand;
let stdinWrittenData;
let stdinErrorHandler;
let mockKill;
function setStatusLineConfig(config) {
    mockSettings.merged = config ? { ui: { statusLine: config } } : {};
}
describe('useStatusLine', () => {
    // Must import dynamically after mocks are set up
    let useStatusLine;
    beforeEach(async () => {
        vi.useFakeTimers();
        vi.clearAllMocks();
        lastExecCommand = undefined;
        stdinWrittenData = '';
        stdinErrorHandler = undefined;
        mockKill = vi.fn();
        // Set up exec mock implementation
        vi.mocked(child_process.exec).mockImplementation(((cmd, _opts, cb) => {
            lastExecCommand = cmd;
            execCallback = cb;
            stdinWrittenData = '';
            stdinErrorHandler = undefined;
            return {
                stdin: {
                    on: vi.fn((_event, handler) => {
                        stdinErrorHandler = handler;
                    }),
                    write: vi.fn((data) => {
                        stdinWrittenData += data;
                        return true;
                    }),
                    end: vi.fn(),
                },
                kill: mockKill,
                killed: false,
            };
        }));
        // Reset mutable mock state
        setStatusLineConfig(undefined);
        mockUIState.sessionStats.lastPromptTokenCount = 100;
        mockUIState.currentModel = 'test-model';
        mockUIState.branchName = 'main';
        mockUIState.sessionStats.metrics.tools.totalCalls = 0;
        mockUIState.sessionStats.metrics.files.totalLinesAdded = 0;
        mockUIState.sessionStats.metrics.files.totalLinesRemoved = 0;
        mockVimMode.vimEnabled = false;
        mockVimMode.vimMode = 'INSERT';
        // Dynamic import to get fresh module after mocks
        const mod = await import('./useStatusLine.js');
        useStatusLine = mod.useStatusLine;
    });
    afterEach(() => {
        vi.useRealTimers();
    });
    // --- getStatusLineConfig validation (tested through the hook) ---
    describe('config validation', () => {
        it('returns null when no statusLine config is set', () => {
            const { result } = renderHook(() => useStatusLine());
            expect(result.current.lines).toEqual([]);
            expect(child_process.exec).not.toHaveBeenCalled();
        });
        it('returns null when statusLine type is not "command"', () => {
            setStatusLineConfig({ type: 'invalid', command: 'echo hi' });
            const { result } = renderHook(() => useStatusLine());
            expect(result.current.lines).toEqual([]);
            expect(child_process.exec).not.toHaveBeenCalled();
        });
        it('returns null when command is empty string', () => {
            setStatusLineConfig({ type: 'command', command: '' });
            const { result } = renderHook(() => useStatusLine());
            expect(result.current.lines).toEqual([]);
            expect(child_process.exec).not.toHaveBeenCalled();
        });
        it('returns null when command is whitespace only', () => {
            setStatusLineConfig({ type: 'command', command: '   ' });
            const { result } = renderHook(() => useStatusLine());
            expect(result.current.lines).toEqual([]);
            expect(child_process.exec).not.toHaveBeenCalled();
        });
    });
    // --- Command execution ---
    describe('command execution', () => {
        it('executes configured command on mount', () => {
            setStatusLineConfig({ type: 'command', command: 'echo hello' });
            renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledOnce();
            expect(lastExecCommand).toBe('echo hello');
        });
        it('passes correct options to exec', () => {
            setStatusLineConfig({ type: 'command', command: 'echo hello' });
            renderHook(() => useStatusLine());
            const callArgs = vi.mocked(child_process.exec).mock.calls[0];
            const opts = callArgs[1];
            expect(opts.cwd).toBe('/test/dir');
            expect(opts.timeout).toBe(5000);
            expect(opts.maxBuffer).toBe(1024 * 10);
        });
        it('returns single line as array', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo hello' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, 'hello world\n', '');
            });
            expect(result.current.lines).toEqual(['hello world']);
        });
        it('returns all lines when stdout has multiple lines', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo lines' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, 'first line\nsecond line\n', '');
            });
            expect(result.current.lines).toEqual(['first line', 'second line']);
        });
        it('filters empty lines from output', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo lines' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, '\n\nreal content\n', '');
            });
            expect(result.current.lines).toEqual(['real content']);
        });
        it('caps output at 2 lines', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo lines' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, 'line1\nline2\nline3\nline4\n', '');
            });
            expect(result.current.lines).toEqual(['line1', 'line2']);
        });
        it('handles \\r\\n line endings', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo lines' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, 'line1\r\nline2\r\n', '');
            });
            expect(result.current.lines).toEqual(['line1', 'line2']);
        });
        it('returns empty when stdout is only newlines', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo lines' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, '\n\n', '');
            });
            expect(result.current.lines).toEqual([]);
        });
        it('returns null when command fails', async () => {
            setStatusLineConfig({ type: 'command', command: 'bad-cmd' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(new Error('command not found'), '', '');
            });
            expect(result.current.lines).toEqual([]);
        });
        it('returns null when stdout is empty', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo -n' });
            const { result } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, '', '');
            });
            expect(result.current.lines).toEqual([]);
        });
    });
    // --- stdin JSON input ---
    describe('stdin JSON input', () => {
        it('writes JSON to stdin with session context', () => {
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.session_id).toBe('test-session');
            expect(input.version).toBe('1.0.0');
            expect(input.model.display_name).toBe('test-model');
            expect(input.workspace.current_dir).toBe('/test/dir');
        });
        it('includes git branch when available', () => {
            mockUIState.branchName = 'feature/test';
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.git.branch).toBe('feature/test');
        });
        it('omits git when branchName is falsy', () => {
            mockUIState.branchName = undefined;
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.git).toBeUndefined();
        });
        it('includes vim mode when enabled', () => {
            mockVimMode.vimEnabled = true;
            mockVimMode.vimMode = 'NORMAL';
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.vim.mode).toBe('NORMAL');
        });
        it('omits vim when not enabled', () => {
            mockVimMode.vimEnabled = false;
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.vim).toBeUndefined();
        });
        it('includes context window usage data', () => {
            mockUIState.sessionStats.lastPromptTokenCount = 65536;
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.context_window.context_window_size).toBe(131072);
            expect(input.context_window.used_percentage).toBe(50);
            expect(input.context_window.remaining_percentage).toBe(50);
            expect(input.context_window.current_usage).toBe(65536);
        });
        it('includes per-model metrics and aggregated token counts', () => {
            mockUIState.sessionStats.metrics.models = {
                'test-model': {
                    api: { totalRequests: 5, totalErrors: 1, totalLatencyMs: 2000 },
                    tokens: {
                        prompt: 1000,
                        candidates: 500,
                        total: 1500,
                        cached: 200,
                        thoughts: 100,
                    },
                },
            };
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.metrics.models['test-model'].api.total_requests).toBe(5);
            expect(input.metrics.models['test-model'].api.total_errors).toBe(1);
            expect(input.metrics.models['test-model'].tokens.prompt).toBe(1000);
            expect(input.metrics.models['test-model'].tokens.completion).toBe(500);
            expect(input.metrics.models['test-model'].tokens.cached).toBe(200);
            expect(input.metrics.models['test-model'].tokens.thoughts).toBe(100);
            expect(input.context_window.total_input_tokens).toBe(1000);
            expect(input.context_window.total_output_tokens).toBe(500);
        });
        it('falls back to zero when contextWindowSize is unavailable', () => {
            mockConfig.getContentGeneratorConfig.mockReturnValueOnce(null);
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.context_window.context_window_size).toBe(0);
            expect(input.context_window.used_percentage).toBe(0);
        });
        it('falls back to "unknown" when getCliVersion returns empty', () => {
            mockConfig.getCliVersion.mockReturnValueOnce('');
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.version).toBe('unknown');
        });
        it('falls back to model from config when currentModel is empty', () => {
            mockUIState.currentModel = '';
            setStatusLineConfig({ type: 'command', command: 'cat' });
            renderHook(() => useStatusLine());
            const input = JSON.parse(stdinWrittenData);
            expect(input.model.display_name).toBe('test-model');
        });
    });
    // --- Stale generation handling ---
    describe('stale generation', () => {
        it('ignores callback from stale generation and accepts fresh one', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { result, rerender } = renderHook(() => useStatusLine());
            // Capture first callback
            const firstCallback = execCallback;
            // Trigger a state change to cause re-execution
            mockUIState.currentModel = 'new-model';
            rerender();
            // Advance debounce timer — triggers second exec
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            // Capture second (current) callback
            const secondCallback = execCallback;
            // Resolve the stale first callback — should be ignored
            await act(async () => {
                firstCallback(null, 'stale output\n', '');
            });
            expect(result.current.lines).toEqual([]);
            // Resolve the fresh second callback — should be accepted
            await act(async () => {
                secondCallback(null, 'fresh output\n', '');
            });
            expect(result.current.lines).toEqual(['fresh output']);
        });
    });
    // --- Debouncing ---
    describe('debouncing', () => {
        it('debounces rapid state changes to a single exec', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender } = renderHook(() => useStatusLine());
            // Initial mount exec
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Rapid state changes
            mockUIState.currentModel = 'model-1';
            rerender();
            mockUIState.currentModel = 'model-2';
            rerender();
            mockUIState.currentModel = 'model-3';
            rerender();
            // Before debounce expires, no additional execs
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // After debounce
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            // Should have exactly one debounced exec (total 2: mount + debounced)
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
    });
    // --- Config removal clears output ---
    describe('config removal', () => {
        it('clears output when config is removed', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo hello' });
            const { result, rerender } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, 'hello\n', '');
            });
            expect(result.current.lines).toEqual(['hello']);
            // Remove config
            setStatusLineConfig(undefined);
            rerender();
            expect(result.current.lines).toEqual([]);
        });
        it('cancels pending debounce and kills child when config is removed', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo hello' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Trigger a debounced update (timer is pending)
            mockUIState.currentModel = 'new-model';
            rerender();
            // Remove config before debounce fires
            setStatusLineConfig(undefined);
            rerender();
            expect(mockKill).toHaveBeenCalled();
            // Advancing past debounce should not trigger another exec
            const callsBefore = vi.mocked(child_process.exec).mock.calls.length;
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(vi.mocked(child_process.exec).mock.calls.length).toBe(callsBefore);
        });
    });
    // --- Cleanup on unmount ---
    describe('cleanup', () => {
        it('kills active child process on unmount', () => {
            setStatusLineConfig({ type: 'command', command: 'sleep 10' });
            const { unmount } = renderHook(() => useStatusLine());
            expect(mockKill).not.toHaveBeenCalled();
            unmount();
            expect(mockKill).toHaveBeenCalled();
        });
        it('clears debounce timer on unmount', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender, unmount } = renderHook(() => useStatusLine());
            // Trigger a debounced update
            mockUIState.currentModel = 'new-model';
            rerender();
            // Unmount before debounce fires
            unmount();
            // Advance past debounce — should not cause additional exec
            const callsBefore = vi.mocked(child_process.exec).mock.calls.length;
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(vi.mocked(child_process.exec).mock.calls.length).toBe(callsBefore);
        });
    });
    // --- stdin error handling ---
    describe('stdin error handling', () => {
        it('silently handles EPIPE errors', () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            renderHook(() => useStatusLine());
            expect(stdinErrorHandler).toBeDefined();
            const epipeError = new Error('EPIPE');
            epipeError.code = 'EPIPE';
            // Should not throw
            expect(() => stdinErrorHandler(epipeError)).not.toThrow();
        });
        it('logs non-EPIPE stdin errors', () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            renderHook(() => useStatusLine());
            expect(stdinErrorHandler).toBeDefined();
            const otherError = new Error('EIO');
            otherError.code = 'EIO';
            // Should not throw but should log (we can't easily check debugLog here,
            // but we verify it doesn't crash)
            expect(() => stdinErrorHandler(otherError)).not.toThrow();
        });
    });
    // --- Command change triggers immediate re-execution ---
    describe('command change', () => {
        it('re-executes immediately when command changes', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo first' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Resolve first exec
            await act(async () => {
                execCallback(null, 'first\n', '');
            });
            // Change command
            setStatusLineConfig({ type: 'command', command: 'echo second' });
            rerender();
            // Should re-execute immediately (not debounced)
            expect(child_process.exec).toHaveBeenCalledTimes(2);
            expect(lastExecCommand).toBe('echo second');
        });
        it('cancels pending debounce when command changes', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo first' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Trigger a debounced update
            mockUIState.currentModel = 'new-model';
            rerender();
            // Change command before debounce fires
            setStatusLineConfig({ type: 'command', command: 'echo second' });
            rerender();
            // Immediate re-exec from command change (mount + command change = 2)
            expect(child_process.exec).toHaveBeenCalledTimes(2);
            // Debounce fires but should not cause a third exec (was cleared)
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
    });
    // --- State change triggers ---
    describe('state change triggers', () => {
        it('triggers update when prompt token count changes', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            mockUIState.sessionStats.lastPromptTokenCount = 200;
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
        it('triggers update when branch changes', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            mockUIState.branchName = 'feature/new';
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
        it('triggers update when tool calls change', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            mockUIState.sessionStats.metrics.tools.totalCalls = 5;
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
        it('triggers update when vim mode is toggled off', async () => {
            mockVimMode.vimEnabled = true;
            mockVimMode.vimMode = 'NORMAL';
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Disable vim — effectiveVim changes from 'NORMAL' to undefined
            mockVimMode.vimEnabled = false;
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
        it('triggers update when file lines change', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            mockUIState.sessionStats.metrics.files.totalLinesAdded = 50;
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
    });
    // --- Process killed on new update ---
    describe('process management', () => {
        it('kills previous process when starting new execution', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { rerender } = renderHook(() => useStatusLine());
            const firstKill = mockKill;
            // Trigger re-execution via state change
            mockUIState.currentModel = 'new-model';
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(firstKill).toHaveBeenCalled();
        });
    });
    // --- Spawn failure handling (issue #3264) ---
    //
    // On macOS with Node 22, exec() can throw synchronously with EBADF when
    // stdio pipe setup fails. The throw must not escape doUpdate() — or the
    // setTimeout callback — or the whole CLI crashes.
    describe('spawn failure handling', () => {
        it('does not crash when exec throws synchronously (EBADF)', () => {
            vi.mocked(child_process.exec).mockImplementationOnce((() => {
                const err = new Error('spawn EBADF');
                err.code = 'EBADF';
                throw err;
            }));
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            let result;
            expect(() => {
                result = renderHook(() => useStatusLine()).result;
            }).not.toThrow();
            expect(result.current.lines).toEqual([]);
        });
        it('recovers on subsequent state changes after a sync exec failure', async () => {
            // First call throws, subsequent calls succeed with the default mock.
            // Verifies activeChildRef and generationRef don't get wedged.
            vi.mocked(child_process.exec).mockImplementationOnce((() => {
                const err = new Error('spawn EBADF');
                err.code = 'EBADF';
                throw err;
            }));
            setStatusLineConfig({ type: 'command', command: 'echo test' });
            const { result, rerender } = renderHook(() => useStatusLine());
            expect(result.current.lines).toEqual([]);
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Trigger a re-execution via state change — should use the default mock.
            mockUIState.currentModel = 'new-model';
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
            await act(async () => {
                execCallback(null, 'recovered\n', '');
            });
            expect(result.current.lines).toEqual(['recovered']);
        });
    });
    // --- Output deduplication (cuts unnecessary Footer re-renders) ---
    describe('output deduplication', () => {
        it('preserves the same lines array reference when output is unchanged', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo same' });
            const { result, rerender } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, 'same output\n', '');
            });
            const firstRef = result.current.lines;
            expect(firstRef).toEqual(['same output']);
            // Trigger another exec with identical output (e.g. via state change).
            mockUIState.currentModel = 'new-model';
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            await act(async () => {
                execCallback(null, 'same output\n', '');
            });
            // Reference preserved → React can skip the Footer re-render.
            expect(result.current.lines).toBe(firstRef);
        });
        it('produces a new reference when output changes', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo tick' });
            const { result, rerender } = renderHook(() => useStatusLine());
            await act(async () => {
                execCallback(null, 'first\n', '');
            });
            const firstRef = result.current.lines;
            expect(firstRef).toEqual(['first']);
            mockUIState.currentModel = 'new-model';
            rerender();
            await act(async () => {
                vi.advanceTimersByTime(300);
            });
            await act(async () => {
                execCallback(null, 'second\n', '');
            });
            expect(result.current.lines).not.toBe(firstRef);
            expect(result.current.lines).toEqual(['second']);
        });
    });
    // --- refreshInterval (periodic refresh) ---
    describe('refreshInterval', () => {
        it('re-executes the command every N seconds', async () => {
            setStatusLineConfig({
                type: 'command',
                command: 'echo tick',
                refreshInterval: 2,
            });
            renderHook(() => useStatusLine());
            // Mount executes once immediately
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            await act(async () => {
                execCallback(null, 'tick 1\n', '');
            });
            // First interval tick after 2s — previous exec has completed, so
            // the tick is free to spawn a new one.
            await act(async () => {
                vi.advanceTimersByTime(2000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
            await act(async () => {
                execCallback(null, 'tick 2\n', '');
            });
            // Second interval tick after another 2s
            await act(async () => {
                vi.advanceTimersByTime(2000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(3);
        });
        it('does not start an interval when refreshInterval is omitted', async () => {
            setStatusLineConfig({ type: 'command', command: 'echo static' });
            renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            await act(async () => {
                vi.advanceTimersByTime(60_000);
            });
            // Still only the mount exec — no periodic refresh
            expect(child_process.exec).toHaveBeenCalledTimes(1);
        });
        it('rejects refreshInterval < 1 (no interval scheduled)', async () => {
            setStatusLineConfig({
                type: 'command',
                command: 'echo tick',
                refreshInterval: 0.5,
            });
            renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            await act(async () => {
                vi.advanceTimersByTime(60_000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(1);
        });
        it('rejects non-finite refreshInterval (no interval scheduled)', async () => {
            setStatusLineConfig({
                type: 'command',
                command: 'echo tick',
                refreshInterval: Number.POSITIVE_INFINITY,
            });
            renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            await act(async () => {
                vi.advanceTimersByTime(60_000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(1);
        });
        it('clears the interval when config is removed', async () => {
            setStatusLineConfig({
                type: 'command',
                command: 'echo tick',
                refreshInterval: 1,
            });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Remove the config — the interval should be torn down.
            setStatusLineConfig(undefined);
            rerender();
            const callsAfterRemoval = vi.mocked(child_process.exec).mock.calls.length;
            await act(async () => {
                vi.advanceTimersByTime(10_000);
            });
            expect(vi.mocked(child_process.exec).mock.calls.length).toBe(callsAfterRemoval);
        });
        it('reschedules when refreshInterval changes', async () => {
            setStatusLineConfig({
                type: 'command',
                command: 'echo tick',
                refreshInterval: 5,
            });
            const { rerender } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            await act(async () => {
                execCallback(null, 'tick\n', '');
            });
            // 2s passes — not yet a tick on the 5s schedule.
            await act(async () => {
                vi.advanceTimersByTime(2000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // Swap to a 1s interval — the old 5s timer must be cleared, not kept.
            setStatusLineConfig({
                type: 'command',
                command: 'echo tick',
                refreshInterval: 1,
            });
            rerender();
            // 1s later — fires on the new schedule.
            await act(async () => {
                vi.advanceTimersByTime(1000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
        it('clears the interval on unmount', async () => {
            setStatusLineConfig({
                type: 'command',
                command: 'echo tick',
                refreshInterval: 1,
            });
            const { unmount } = renderHook(() => useStatusLine());
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            unmount();
            const callsAfterUnmount = vi.mocked(child_process.exec).mock.calls.length;
            await act(async () => {
                vi.advanceTimersByTime(10_000);
            });
            expect(vi.mocked(child_process.exec).mock.calls.length).toBe(callsAfterUnmount);
        });
        it('skips periodic ticks while a previous exec is still running', async () => {
            // Starvation regression (#3383 review): with refreshInterval < command
            // latency, if every tick called doUpdate() it would kill the in-flight
            // child, generation++ would stale the eventual callback, and the user
            // would never see any output. This test asserts BOTH the guard (exec
            // call count) AND the user-visible result (rendered lines).
            setStatusLineConfig({
                type: 'command',
                command: 'slow-command',
                refreshInterval: 1,
            });
            const { result } = renderHook(() => useStatusLine());
            // Mount exec: child is spawned, callback NOT yet resolved — child is
            // still "running" from the hook's perspective.
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            const pendingCallback = execCallback;
            // Several interval ticks pass while the first exec is in flight.
            // Each tick must detect the running child and skip doUpdate().
            await act(async () => {
                vi.advanceTimersByTime(1000);
            });
            await act(async () => {
                vi.advanceTimersByTime(1000);
            });
            await act(async () => {
                vi.advanceTimersByTime(1000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(1);
            // First exec finally completes — activeChildRef clears. Without the
            // guard, generationRef would have bumped 3 times above and the next
            // line would be ignored as stale, leaving `lines` permanently empty.
            await act(async () => {
                pendingCallback(null, 'done\n', '');
            });
            expect(result.current.lines).toEqual(['done']);
            // Next tick is now free to spawn a new exec.
            await act(async () => {
                vi.advanceTimersByTime(1000);
            });
            expect(child_process.exec).toHaveBeenCalledTimes(2);
        });
    });
});
//# sourceMappingURL=useStatusLine.test.js.map