/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, afterEach, vi } from 'vitest';
import { getTokenAtCursor, escapeShellPath, resolvePathCompletions, scanPathExecutables, } from './useShellCompletion.js';
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
describe('useShellCompletion utilities', () => {
    describe('getTokenAtCursor', () => {
        it('should return empty token struct for empty line', () => {
            expect(getTokenAtCursor('', 0)).toEqual({
                token: '',
                start: 0,
                end: 0,
                isFirstToken: true,
                tokens: [''],
                cursorIndex: 0,
                commandToken: '',
            });
        });
        it('should extract the first token at cursor position 0', () => {
            const result = getTokenAtCursor('git status', 3);
            expect(result).toEqual({
                token: 'git',
                start: 0,
                end: 3,
                isFirstToken: true,
                tokens: ['git', 'status'],
                cursorIndex: 0,
                commandToken: 'git',
            });
        });
        it('should extract the second token when cursor is on it', () => {
            const result = getTokenAtCursor('git status', 7);
            expect(result).toEqual({
                token: 'status',
                start: 4,
                end: 10,
                isFirstToken: false,
                tokens: ['git', 'status'],
                cursorIndex: 1,
                commandToken: 'git',
            });
        });
        it('should handle cursor at start of second token', () => {
            const result = getTokenAtCursor('git status', 4);
            expect(result).toEqual({
                token: 'status',
                start: 4,
                end: 10,
                isFirstToken: false,
                tokens: ['git', 'status'],
                cursorIndex: 1,
                commandToken: 'git',
            });
        });
        it('should handle escaped spaces', () => {
            const result = getTokenAtCursor('cat my\\ file.txt', 16);
            expect(result).toEqual({
                token: 'my file.txt',
                start: 4,
                end: 16,
                isFirstToken: false,
                tokens: ['cat', 'my file.txt'],
                cursorIndex: 1,
                commandToken: 'cat',
            });
        });
        it('should handle single-quoted strings', () => {
            const result = getTokenAtCursor("cat 'my file.txt'", 17);
            expect(result).toEqual({
                token: 'my file.txt',
                start: 4,
                end: 17,
                isFirstToken: false,
                tokens: ['cat', 'my file.txt'],
                cursorIndex: 1,
                commandToken: 'cat',
            });
        });
        it('should handle double-quoted strings', () => {
            const result = getTokenAtCursor('cat "my file.txt"', 17);
            expect(result).toEqual({
                token: 'my file.txt',
                start: 4,
                end: 17,
                isFirstToken: false,
                tokens: ['cat', 'my file.txt'],
                cursorIndex: 1,
                commandToken: 'cat',
            });
        });
        it('should handle cursor past all tokens (trailing space)', () => {
            const result = getTokenAtCursor('git ', 4);
            expect(result).toEqual({
                token: '',
                start: 4,
                end: 4,
                isFirstToken: false,
                tokens: ['git', ''],
                cursorIndex: 1,
                commandToken: 'git',
            });
        });
        it('should handle cursor in the middle of a word', () => {
            const result = getTokenAtCursor('git checkout main', 7);
            expect(result).toEqual({
                token: 'checkout',
                start: 4,
                end: 12,
                isFirstToken: false,
                tokens: ['git', 'checkout', 'main'],
                cursorIndex: 1,
                commandToken: 'git',
            });
        });
        it('should mark isFirstToken correctly for first word', () => {
            const result = getTokenAtCursor('gi', 2);
            expect(result?.isFirstToken).toBe(true);
        });
        it('should mark isFirstToken correctly for second word', () => {
            const result = getTokenAtCursor('git sta', 7);
            expect(result?.isFirstToken).toBe(false);
        });
        it('should handle cursor in whitespace between tokens', () => {
            const result = getTokenAtCursor('git  status', 4);
            expect(result).toEqual({
                token: '',
                start: 4,
                end: 4,
                isFirstToken: false,
                tokens: ['git', '', 'status'],
                cursorIndex: 1,
                commandToken: 'git',
            });
        });
    });
    describe('escapeShellPath', () => {
        const isWin = process.platform === 'win32';
        it('should escape spaces', () => {
            expect(escapeShellPath('my file.txt')).toBe(isWin ? 'my file.txt' : 'my\\ file.txt');
        });
        it('should escape parentheses', () => {
            expect(escapeShellPath('file (copy).txt')).toBe(isWin ? 'file (copy).txt' : 'file\\ \\(copy\\).txt');
        });
        it('should not escape normal characters', () => {
            expect(escapeShellPath('normal-file.txt')).toBe('normal-file.txt');
        });
        it('should escape tabs, newlines, carriage returns, and backslashes', () => {
            if (isWin) {
                expect(escapeShellPath('a\tb')).toBe('a\tb');
                expect(escapeShellPath('a\nb')).toBe('a\nb');
                expect(escapeShellPath('a\rb')).toBe('a\rb');
                expect(escapeShellPath('a\\b')).toBe('a\\b');
            }
            else {
                expect(escapeShellPath('a\tb')).toBe('a\\\tb');
                expect(escapeShellPath('a\nb')).toBe('a\\\nb');
                expect(escapeShellPath('a\rb')).toBe('a\\\rb');
                expect(escapeShellPath('a\\b')).toBe('a\\\\b');
            }
        });
        it('should handle empty string', () => {
            expect(escapeShellPath('')).toBe('');
        });
    });
    describe('resolvePathCompletions', () => {
        let tmpDir;
        afterEach(async () => {
            if (tmpDir) {
                await cleanupTmpDir(tmpDir);
            }
        });
        it('should list directory contents for empty partial', async () => {
            const structure = {
                'file.txt': '',
                subdir: {},
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('', tmpDir);
            const values = results.map((s) => s.label);
            expect(values).toContain('subdir/');
            expect(values).toContain('file.txt');
        });
        it('should filter by prefix', async () => {
            const structure = {
                'abc.txt': '',
                'def.txt': '',
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('a', tmpDir);
            expect(results).toHaveLength(1);
            expect(results[0].label).toBe('abc.txt');
        });
        it('should match case-insensitively', async () => {
            const structure = {
                Desktop: {},
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('desk', tmpDir);
            expect(results).toHaveLength(1);
            expect(results[0].label).toBe('Desktop/');
        });
        it('should append trailing slash to directories', async () => {
            const structure = {
                mydir: {},
                'myfile.txt': '',
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('my', tmpDir);
            const dirSuggestion = results.find((s) => s.label.startsWith('mydir'));
            expect(dirSuggestion?.label).toBe('mydir/');
            expect(dirSuggestion?.description).toBe('directory');
        });
        it('should hide dotfiles by default', async () => {
            const structure = {
                '.hidden': '',
                visible: '',
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('', tmpDir);
            const labels = results.map((s) => s.label);
            expect(labels).not.toContain('.hidden');
            expect(labels).toContain('visible');
        });
        it('should show dotfiles when query starts with a dot', async () => {
            const structure = {
                '.hidden': '',
                '.bashrc': '',
                visible: '',
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('.h', tmpDir);
            const labels = results.map((s) => s.label);
            expect(labels).toContain('.hidden');
        });
        it('should show dotfiles in the current directory when query is exactly "."', async () => {
            const structure = {
                '.hidden': '',
                '.bashrc': '',
                visible: '',
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('.', tmpDir);
            const labels = results.map((s) => s.label);
            expect(labels).toContain('.hidden');
            expect(labels).toContain('.bashrc');
            expect(labels).not.toContain('visible');
        });
        it('should handle dotfile completions within a subdirectory', async () => {
            const structure = {
                subdir: {
                    '.secret': '',
                    'public.txt': '',
                },
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('subdir/.', tmpDir);
            const labels = results.map((s) => s.label);
            expect(labels).toContain('.secret');
            expect(labels).not.toContain('public.txt');
        });
        it('should strip leading quotes to resolve inner directory contents', async () => {
            const structure = {
                src: {
                    'index.ts': '',
                },
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('"src/', tmpDir);
            expect(results).toHaveLength(1);
            expect(results[0].label).toBe('index.ts');
            const resultsSingleQuote = await resolvePathCompletions("'src/", tmpDir);
            expect(resultsSingleQuote).toHaveLength(1);
            expect(resultsSingleQuote[0].label).toBe('index.ts');
        });
        it('should properly escape resolutions with spaces inside stripped quote queries', async () => {
            const structure = {
                'Folder With Spaces': {},
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('"Fo', tmpDir);
            expect(results).toHaveLength(1);
            expect(results[0].label).toBe('Folder With Spaces/');
            expect(results[0].value).toBe(escapeShellPath('Folder With Spaces/'));
        });
        it('should return empty array for non-existent directory', async () => {
            const results = await resolvePathCompletions('/nonexistent/path/foo', '/tmp');
            expect(results).toEqual([]);
        });
        it('should handle tilde expansion', async () => {
            // Just ensure ~ doesn't throw
            const results = await resolvePathCompletions('~/', '/tmp');
            // We can't assert specific files since it depends on the test runner's home
            expect(Array.isArray(results)).toBe(true);
        });
        it('should escape special characters in results', async () => {
            const isWin = process.platform === 'win32';
            const structure = {
                'my file.txt': '',
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('my', tmpDir);
            expect(results).toHaveLength(1);
            expect(results[0].value).toBe(isWin ? 'my file.txt' : 'my\\ file.txt');
        });
        it('should sort directories before files', async () => {
            const structure = {
                'b-file.txt': '',
                'a-dir': {},
            };
            tmpDir = await createTmpDir(structure);
            const results = await resolvePathCompletions('', tmpDir);
            expect(results[0].description).toBe('directory');
            expect(results[1].description).toBe('file');
        });
    });
    describe('scanPathExecutables', () => {
        it('should return an array of executables', async () => {
            const results = await scanPathExecutables();
            expect(Array.isArray(results)).toBe(true);
            // Very basic sanity check: common commands should be found
            if (process.platform !== 'win32') {
                expect(results).toContain('ls');
            }
            else {
                expect(results).toContain('dir');
                expect(results).toContain('cls');
                expect(results).toContain('copy');
            }
        });
        it('should support abort signal', async () => {
            const controller = new AbortController();
            controller.abort();
            const results = await scanPathExecutables(controller.signal);
            // May return empty or partial depending on timing
            expect(Array.isArray(results)).toBe(true);
        });
        it('should handle empty PATH', async () => {
            vi.stubEnv('PATH', '');
            const results = await scanPathExecutables();
            if (process.platform === 'win32') {
                expect(results.length).toBeGreaterThan(0);
                expect(results).toContain('dir');
            }
            else {
                expect(results).toEqual([]);
            }
            vi.unstubAllEnvs();
        });
    });
});
//# sourceMappingURL=useShellCompletion.test.js.map