/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { act, useState } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { useAtCompletion } from './useAtCompletion.js';
import { FileSearchFactory } from '@google/gemini-cli-core';
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
// Test harness to capture the state from the hook's callbacks.
function useTestHarnessForAtCompletion(enabled, pattern, config, cwd) {
    const [suggestions, setSuggestions] = useState([]);
    const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
    useAtCompletion({
        enabled,
        pattern,
        config,
        cwd,
        setSuggestions,
        setIsLoadingSuggestions,
    });
    return { suggestions, isLoadingSuggestions };
}
describe('useAtCompletion', () => {
    let testRootDir;
    let mockConfig;
    beforeEach(() => {
        mockConfig = {
            getFileFilteringOptions: vi.fn(() => ({
                respectGitIgnore: true,
                respectGeminiIgnore: true,
            })),
            getEnableRecursiveFileSearch: () => true,
            getFileFilteringDisableFuzzySearch: () => false,
        };
        vi.clearAllMocks();
    });
    afterEach(async () => {
        if (testRootDir) {
            await cleanupTmpDir(testRootDir);
        }
        vi.restoreAllMocks();
    });
    describe('File Search Logic', () => {
        it('should perform a recursive search for an empty pattern', async () => {
            const structure = {
                'file.txt': '',
                src: {
                    'index.js': '',
                    components: ['Button.tsx', 'Button with spaces.tsx'],
                },
            };
            testRootDir = await createTmpDir(structure);
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir));
            await waitFor(() => {
                expect(result.current.suggestions.length).toBeGreaterThan(5);
            });
            expect(result.current.suggestions.length).toBeGreaterThan(0);
            expect(result.current.suggestions.map((s) => s.value)).toEqual([
                'src/',
                'src/components/',
                'file.txt',
                'src/components/Button\\ with\\ spaces.tsx',
                'src/components/Button.tsx',
                'src/index.js',
            ]);
        });
        it('should correctly filter the recursive list based on a pattern', async () => {
            const structure = {
                'file.txt': '',
                src: {
                    'index.js': '',
                    components: {
                        'Button.tsx': '',
                    },
                },
            };
            testRootDir = await createTmpDir(structure);
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, 'src/', mockConfig, testRootDir));
            await waitFor(() => {
                expect(result.current.suggestions.length).toBeGreaterThan(0);
            });
            expect(result.current.suggestions.map((s) => s.value)).toEqual([
                'src/',
                'src/components/',
                'src/index.js',
                'src/components/Button.tsx',
            ]);
        });
        it('should append a trailing slash to directory paths in suggestions', async () => {
            const structure = {
                'file.txt': '',
                dir: {},
            };
            testRootDir = await createTmpDir(structure);
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir));
            await waitFor(() => {
                expect(result.current.suggestions.length).toBeGreaterThan(0);
            });
            expect(result.current.suggestions.map((s) => s.value)).toEqual([
                'dir/',
                'file.txt',
            ]);
        });
        it('should perform a case-insensitive search by lowercasing the pattern', async () => {
            testRootDir = await createTmpDir({ 'cRaZycAsE.txt': '' });
            const fileSearch = FileSearchFactory.create({
                projectRoot: testRootDir,
                ignoreDirs: [],
                useGitignore: false,
                useGeminiignore: false,
                cache: false,
                cacheTtl: 0,
                enableRecursiveFileSearch: true,
                disableFuzzySearch: false,
            });
            await fileSearch.initialize();
            vi.spyOn(FileSearchFactory, 'create').mockReturnValue(fileSearch);
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, 'CrAzYCaSe', mockConfig, testRootDir));
            // The hook should find 'cRaZycAsE.txt' even though the pattern is 'CrAzYCaSe'.
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'cRaZycAsE.txt',
                ]);
            });
        });
    });
    describe('UI State and Loading Behavior', () => {
        it('should be in a loading state during initial file system crawl', async () => {
            testRootDir = await createTmpDir({});
            // Mock FileSearch to be slow to catch the loading state
            const mockFileSearch = {
                initialize: vi.fn().mockImplementation(async () => {
                    await new Promise((resolve) => setTimeout(resolve, 50));
                }),
                search: vi.fn().mockResolvedValue([]),
            };
            vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch);
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir));
            // It's initially true because the effect runs synchronously.
            await waitFor(() => {
                expect(result.current.isLoadingSuggestions).toBe(true);
            });
            // Wait for the loading to complete.
            await waitFor(() => {
                expect(result.current.isLoadingSuggestions).toBe(false);
            });
        });
        it('should NOT show a loading indicator for subsequent searches that complete under 200ms', async () => {
            const structure = { 'a.txt': '', 'b.txt': '' };
            testRootDir = await createTmpDir(structure);
            const { result, rerender } = renderHook(({ pattern }) => useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir), { initialProps: { pattern: 'a' } });
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'a.txt',
                ]);
            });
            expect(result.current.isLoadingSuggestions).toBe(false);
            rerender({ pattern: 'b' });
            // Wait for the final result
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'b.txt',
                ]);
            });
            expect(result.current.isLoadingSuggestions).toBe(false);
        });
        it('should show a loading indicator and clear old suggestions for subsequent searches that take longer than 200ms', async () => {
            const structure = { 'a.txt': '', 'b.txt': '' };
            testRootDir = await createTmpDir(structure);
            const realFileSearch = FileSearchFactory.create({
                projectRoot: testRootDir,
                ignoreDirs: [],
                useGitignore: true,
                useGeminiignore: true,
                cache: false,
                cacheTtl: 0,
                enableRecursiveFileSearch: true,
                disableFuzzySearch: false,
            });
            await realFileSearch.initialize();
            // Mock that returns results immediately but we'll control timing with fake timers
            const mockFileSearch = {
                initialize: vi.fn().mockResolvedValue(undefined),
                search: vi
                    .fn()
                    .mockImplementation(async (pattern, options) => realFileSearch.search(pattern, options)),
            };
            vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch);
            const { result, rerender } = renderHook(({ pattern }) => useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir), { initialProps: { pattern: 'a' } });
            // Wait for the initial search to complete (using real timers)
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'a.txt',
                ]);
            });
            // Now switch to fake timers for precise control of the loading behavior
            vi.useFakeTimers();
            // Trigger the second search
            act(() => {
                rerender({ pattern: 'b' });
            });
            // Initially, loading should be false (before 200ms timer)
            expect(result.current.isLoadingSuggestions).toBe(false);
            // Advance time by exactly 200ms to trigger the loading state
            act(() => {
                vi.advanceTimersByTime(200);
            });
            // Now loading should be true and suggestions should be cleared
            expect(result.current.isLoadingSuggestions).toBe(true);
            expect(result.current.suggestions).toEqual([]);
            // Switch back to real timers for the final waitFor
            vi.useRealTimers();
            // Wait for the search results to be processed
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'b.txt',
                ]);
            });
            expect(result.current.isLoadingSuggestions).toBe(false);
        });
        it('should abort the previous search when a new one starts', async () => {
            const structure = { 'a.txt': '', 'b.txt': '' };
            testRootDir = await createTmpDir(structure);
            const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
            const mockFileSearch = {
                initialize: vi.fn().mockResolvedValue(undefined),
                search: vi.fn().mockImplementation(async (pattern) => {
                    const delay = pattern === 'a' ? 500 : 50;
                    await new Promise((resolve) => setTimeout(resolve, delay));
                    return [pattern];
                }),
            };
            vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch);
            const { result, rerender } = renderHook(({ pattern }) => useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir), { initialProps: { pattern: 'a' } });
            // Wait for the hook to be ready (initialization is complete)
            await waitFor(() => {
                expect(mockFileSearch.search).toHaveBeenCalledWith('a', expect.any(Object));
            });
            // Now that the first search is in-flight, trigger the second one.
            act(() => {
                rerender({ pattern: 'b' });
            });
            // The abort should have been called for the first search.
            expect(abortSpy).toHaveBeenCalledTimes(1);
            // Wait for the final result, which should be from the second, faster search.
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual(['b']);
            }, { timeout: 1000 });
            // The search spy should have been called for both patterns.
            expect(mockFileSearch.search).toHaveBeenCalledWith('b', expect.any(Object));
        });
    });
    describe('State Management', () => {
        it('should reset the state when disabled after being in a READY state', async () => {
            const structure = { 'a.txt': '' };
            testRootDir = await createTmpDir(structure);
            const { result, rerender } = renderHook(({ enabled }) => useTestHarnessForAtCompletion(enabled, 'a', mockConfig, testRootDir), { initialProps: { enabled: true } });
            // Wait for the hook to be ready and have suggestions
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'a.txt',
                ]);
            });
            // Now, disable the hook
            rerender({ enabled: false });
            // The suggestions should be cleared immediately because of the RESET action
            expect(result.current.suggestions).toEqual([]);
        });
        it('should reset the state when disabled after being in an ERROR state', async () => {
            testRootDir = await createTmpDir({});
            // Force an error during initialization
            const mockFileSearch = {
                initialize: vi
                    .fn()
                    .mockRejectedValue(new Error('Initialization failed')),
                search: vi.fn(),
            };
            vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch);
            const { result, rerender } = renderHook(({ enabled }) => useTestHarnessForAtCompletion(enabled, '', mockConfig, testRootDir), { initialProps: { enabled: true } });
            // Wait for the hook to enter the error state
            await waitFor(() => {
                expect(result.current.isLoadingSuggestions).toBe(false);
            });
            expect(result.current.suggestions).toEqual([]); // No suggestions on error
            // Now, disable the hook
            rerender({ enabled: false });
            // The state should still be reset (though visually it's the same)
            // We can't directly inspect the internal state, but we can ensure it doesn't crash
            // and the suggestions remain empty.
            expect(result.current.suggestions).toEqual([]);
        });
    });
    describe('Filtering and Configuration', () => {
        it('should respect .gitignore files', async () => {
            const gitignoreContent = ['dist/', '*.log'].join('\n');
            const structure = {
                '.git': {},
                '.gitignore': gitignoreContent,
                dist: {},
                'test.log': '',
                src: {},
            };
            testRootDir = await createTmpDir(structure);
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir));
            await waitFor(() => {
                expect(result.current.suggestions.length).toBeGreaterThan(0);
            });
            expect(result.current.suggestions.map((s) => s.value)).toEqual([
                'src/',
                '.gitignore',
            ]);
        });
        it('should work correctly when config is undefined', async () => {
            const structure = {
                node_modules: {},
                src: {},
            };
            testRootDir = await createTmpDir(structure);
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, '', undefined, testRootDir));
            await waitFor(() => {
                expect(result.current.suggestions.length).toBeGreaterThan(0);
            });
            expect(result.current.suggestions.map((s) => s.value)).toEqual([
                'node_modules/',
                'src/',
            ]);
        });
        it('should reset and re-initialize when the cwd changes', async () => {
            const structure1 = { 'file1.txt': '' };
            const rootDir1 = await createTmpDir(structure1);
            const structure2 = { 'file2.txt': '' };
            const rootDir2 = await createTmpDir(structure2);
            const { result, rerender } = renderHook(({ cwd, pattern }) => useTestHarnessForAtCompletion(true, pattern, mockConfig, cwd), {
                initialProps: {
                    cwd: rootDir1,
                    pattern: 'file',
                },
            });
            // Wait for initial suggestions from the first directory
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'file1.txt',
                ]);
            });
            // Change the CWD
            act(() => {
                rerender({ cwd: rootDir2, pattern: 'file' });
            });
            // After CWD changes, suggestions should be cleared and it should load again.
            await waitFor(() => {
                expect(result.current.isLoadingSuggestions).toBe(true);
                expect(result.current.suggestions).toEqual([]);
            });
            // Wait for the new suggestions from the second directory
            await waitFor(() => {
                expect(result.current.suggestions.map((s) => s.value)).toEqual([
                    'file2.txt',
                ]);
            });
            expect(result.current.isLoadingSuggestions).toBe(false);
            await cleanupTmpDir(rootDir1);
            await cleanupTmpDir(rootDir2);
        });
        it('should perform a non-recursive search when enableRecursiveFileSearch is false', async () => {
            const structure = {
                'file.txt': '',
                src: {
                    'index.js': '',
                },
            };
            testRootDir = await createTmpDir(structure);
            const nonRecursiveConfig = {
                getEnableRecursiveFileSearch: () => false,
                getFileFilteringOptions: vi.fn(() => ({
                    respectGitIgnore: true,
                    respectGeminiIgnore: true,
                })),
                getFileFilteringDisableFuzzySearch: () => false,
            };
            const { result } = renderHook(() => useTestHarnessForAtCompletion(true, '', nonRecursiveConfig, testRootDir));
            await waitFor(() => {
                expect(result.current.suggestions.length).toBeGreaterThan(0);
            });
            // Should only contain top-level items
            expect(result.current.suggestions.map((s) => s.value)).toEqual([
                'src/',
                'file.txt',
            ]);
        });
    });
});
//# sourceMappingURL=useAtCompletion.test.js.map