/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { GlobTool, sortFileEntries } from './glob.js';
import { partListUnionToString } from '../core/geminiRequest.js';
import path from 'node:path';
import fs from 'node:fs/promises';
import os from 'node:os';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { ToolErrorType } from './tool-error.js';
import * as glob from 'glob';
vi.mock('glob', { spy: true });
describe('GlobTool', () => {
    let tempRootDir; // This will be the rootDirectory for the GlobTool instance
    let globTool;
    const abortSignal = new AbortController().signal;
    // Mock config for testing
    const mockConfig = {
        getFileService: () => new FileDiscoveryService(tempRootDir),
        getFileFilteringRespectGitIgnore: () => true,
        getFileFilteringOptions: () => ({
            respectGitIgnore: true,
            respectQwenIgnore: true,
        }),
        getTargetDir: () => tempRootDir,
        getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
        getFileExclusions: () => ({
            getGlobExcludes: () => [],
        }),
        getTruncateToolOutputLines: () => 1000,
    };
    beforeEach(async () => {
        // Create a unique root directory for each test run
        tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'glob-tool-root-'));
        await fs.writeFile(path.join(tempRootDir, '.git'), ''); // Fake git repo
        globTool = new GlobTool(mockConfig);
        // Create some test files and directories within this root
        // Top-level files
        await fs.writeFile(path.join(tempRootDir, 'fileA.txt'), 'contentA');
        await fs.writeFile(path.join(tempRootDir, 'FileB.TXT'), 'contentB'); // Different case for testing
        // Subdirectory and files within it
        await fs.mkdir(path.join(tempRootDir, 'sub'));
        await fs.writeFile(path.join(tempRootDir, 'sub', 'fileC.md'), 'contentC');
        await fs.writeFile(path.join(tempRootDir, 'sub', 'FileD.MD'), 'contentD'); // Different case
        // Deeper subdirectory
        await fs.mkdir(path.join(tempRootDir, 'sub', 'deep'));
        await fs.writeFile(path.join(tempRootDir, 'sub', 'deep', 'fileE.log'), 'contentE');
        // Files for mtime sorting test
        await fs.writeFile(path.join(tempRootDir, 'older.sortme'), 'older_content');
        // Ensure a noticeable difference in modification time
        await new Promise((resolve) => setTimeout(resolve, 50));
        await fs.writeFile(path.join(tempRootDir, 'newer.sortme'), 'newer_content');
        // For type coercion testing
        await fs.mkdir(path.join(tempRootDir, '123'));
    });
    afterEach(async () => {
        // Clean up the temporary root directory
        await fs.rm(tempRootDir, { recursive: true, force: true });
    });
    describe('execute', () => {
        it('should find files matching a simple pattern in the root', async () => {
            const params = { pattern: '*.txt' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 2 file(s)');
            expect(result.llmContent).toContain(path.join(tempRootDir, 'fileA.txt'));
            expect(result.llmContent).toContain(path.join(tempRootDir, 'FileB.TXT'));
            expect(result.returnDisplay).toBe('Found 2 matching file(s)');
        });
        it('should find files case-insensitively by default (pattern: *.TXT)', async () => {
            const params = { pattern: '*.TXT' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 2 file(s)');
            expect(result.llmContent).toContain(path.join(tempRootDir, 'fileA.txt'));
            expect(result.llmContent).toContain(path.join(tempRootDir, 'FileB.TXT'));
        });
        it('should find files using a pattern that includes a subdirectory', async () => {
            const params = { pattern: 'sub/*.md' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 2 file(s)');
            expect(result.llmContent).toContain(path.join(tempRootDir, 'sub', 'fileC.md'));
            expect(result.llmContent).toContain(path.join(tempRootDir, 'sub', 'FileD.MD'));
        });
        it('should find files in a specified relative path (relative to rootDir)', async () => {
            const params = { pattern: '*.md', path: 'sub' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 2 file(s)');
            expect(result.llmContent).toContain(path.join(tempRootDir, 'sub', 'fileC.md'));
            expect(result.llmContent).toContain(path.join(tempRootDir, 'sub', 'FileD.MD'));
        });
        it('should find files using a deep globstar pattern (e.g., **/*.log)', async () => {
            const params = { pattern: '**/*.log' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 1 file(s)');
            expect(result.llmContent).toContain(path.join(tempRootDir, 'sub', 'deep', 'fileE.log'));
        });
        it('should return "No files found" message when pattern matches nothing', async () => {
            const params = { pattern: '*.nonexistent' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('No files found matching pattern "*.nonexistent"');
            expect(result.returnDisplay).toBe('No files found');
        });
        it('should find files with special characters in the name', async () => {
            await fs.writeFile(path.join(tempRootDir, 'file[1].txt'), 'content');
            const params = { pattern: 'file[1].txt' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 1 file(s)');
            expect(result.llmContent).toContain(path.join(tempRootDir, 'file[1].txt'));
        });
        it('should find files with special characters like [] and () in the path', async () => {
            const filePath = path.join(tempRootDir, 'src/app/[test]/(dashboard)/testing/components/code.tsx');
            await fs.mkdir(path.dirname(filePath), { recursive: true });
            await fs.writeFile(filePath, 'content');
            const params = {
                pattern: 'src/app/[test]/(dashboard)/testing/components/code.tsx',
            };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 1 file(s)');
            expect(result.llmContent).toContain(filePath);
        });
        it('should correctly sort files by modification time (newest first)', async () => {
            const params = { pattern: '*.sortme' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            const llmContent = partListUnionToString(result.llmContent);
            expect(llmContent).toContain('Found 2 file(s)');
            // Ensure llmContent is a string for TypeScript type checking
            expect(typeof llmContent).toBe('string');
            const filesListed = llmContent
                .trim()
                .split(/\r?\n/)
                .slice(2)
                .map((line) => line.trim())
                .filter(Boolean);
            expect(filesListed).toHaveLength(2);
            expect(path.resolve(filesListed[0])).toBe(path.resolve(tempRootDir, 'newer.sortme'));
            expect(path.resolve(filesListed[1])).toBe(path.resolve(tempRootDir, 'older.sortme'));
        });
        it('should return error if path is outside workspace', async () => {
            // Bypassing validation to test execute method directly
            vi.spyOn(globTool, 'validateToolParams').mockReturnValue(null);
            const params = { pattern: '*.txt', path: '/etc' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.returnDisplay).toBe('Error: Path is not within workspace');
        });
        it('should return a GLOB_EXECUTION_ERROR on glob failure', async () => {
            vi.mocked(glob.glob).mockRejectedValue(new Error('Glob failed'));
            const params = { pattern: '*.txt' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.error?.type).toBe(ToolErrorType.GLOB_EXECUTION_ERROR);
            expect(result.llmContent).toContain('Error during glob search operation: Glob failed');
            // Reset glob.
            vi.mocked(glob.glob).mockReset();
        });
    });
    describe('validateToolParams', () => {
        it('should return null for valid parameters (pattern only)', () => {
            const params = { pattern: '*.js' };
            expect(globTool.validateToolParams(params)).toBeNull();
        });
        it('should return null for valid parameters (pattern and path)', () => {
            const params = { pattern: '*.js', path: 'sub' };
            expect(globTool.validateToolParams(params)).toBeNull();
        });
        it('should return error if pattern is missing (schema validation)', () => {
            // Need to correctly define this as an object without pattern
            const params = { path: '.' };
            // @ts-expect-error - We're intentionally creating invalid params for testing
            expect(globTool.validateToolParams(params)).toBe(`params must have required property 'pattern'`);
        });
        it('should return error if pattern is an empty string', () => {
            const params = { pattern: '' };
            expect(globTool.validateToolParams(params)).toContain("The 'pattern' parameter cannot be empty.");
        });
        it('should return error if pattern is only whitespace', () => {
            const params = { pattern: '   ' };
            expect(globTool.validateToolParams(params)).toContain("The 'pattern' parameter cannot be empty.");
        });
        it('should return error if path is provided but is not a string', () => {
            const params = {
                pattern: '*.ts',
                path: 123,
            }; // Force incorrect type
            expect(globTool.validateToolParams(params)).toBe('params/path must be string');
        });
        it("should return error if search path resolves outside the tool's root directory", () => {
            // Create a globTool instance specifically for this test, with a deeper root
            tempRootDir = path.join(tempRootDir, 'sub');
            const specificGlobTool = new GlobTool(mockConfig);
            // const params: GlobToolParams = { pattern: '*.txt', path: '..' }; // This line is unused and will be removed.
            // This should be fine as tempRootDir is still within the original tempRootDir (the parent of deeperRootDir)
            // Let's try to go further up.
            const paramsOutside = {
                pattern: '*.txt',
                path: '../../../../../../../../../../tmp', // Definitely outside
            };
            expect(specificGlobTool.validateToolParams(paramsOutside)).toContain('Path is not within workspace');
        });
        it('should return error if specified search path does not exist', async () => {
            const params = {
                pattern: '*.txt',
                path: 'nonexistent_subdir',
            };
            expect(globTool.validateToolParams(params)).toContain('Path does not exist');
        });
        it('should return error if specified search path is a file, not a directory', async () => {
            const params = { pattern: '*.txt', path: 'fileA.txt' };
            expect(globTool.validateToolParams(params)).toContain('Path is not a directory');
        });
    });
    describe('workspace boundary validation', () => {
        it('should validate search paths are within workspace boundaries', () => {
            const validPath = { pattern: '*.ts', path: 'sub' };
            const invalidPath = { pattern: '*.ts', path: '../..' };
            expect(globTool.validateToolParams(validPath)).toBeNull();
            expect(globTool.validateToolParams(invalidPath)).toContain('Path is not within workspace');
        });
        it('should work with paths in workspace subdirectories', async () => {
            const params = { pattern: '*.md', path: 'sub' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 2 file(s)');
            expect(result.llmContent).toContain('fileC.md');
            expect(result.llmContent).toContain('FileD.MD');
        });
    });
    describe('ignore file handling', () => {
        it('should respect .gitignore files by default', async () => {
            await fs.writeFile(path.join(tempRootDir, '.gitignore'), '*.ignored.txt');
            await fs.writeFile(path.join(tempRootDir, 'a.ignored.txt'), 'ignored content');
            await fs.writeFile(path.join(tempRootDir, 'b.notignored.txt'), 'not ignored content');
            const params = { pattern: '*.txt' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 3 file(s)'); // fileA.txt, FileB.TXT, b.notignored.txt
            expect(result.llmContent).not.toContain('a.ignored.txt');
        });
        it('should respect .qwenignore files by default', async () => {
            await fs.writeFile(path.join(tempRootDir, '.qwenignore'), '*.qwenignored.txt');
            await fs.writeFile(path.join(tempRootDir, 'a.qwenignored.txt'), 'ignored content');
            await fs.writeFile(path.join(tempRootDir, 'b.notignored.txt'), 'not ignored content');
            // Recreate the tool to pick up the new .qwenignore file
            globTool = new GlobTool(mockConfig);
            const params = { pattern: '*.txt' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            expect(result.llmContent).toContain('Found 3 file(s)'); // fileA.txt, FileB.TXT, b.notignored.txt
            expect(result.llmContent).not.toContain('a.qwenignored.txt');
        });
    });
    describe('file count truncation', () => {
        it('should truncate results when more than 100 files are found', async () => {
            // Create 150 test files
            for (let i = 1; i <= 150; i++) {
                await fs.writeFile(path.join(tempRootDir, `file${i}.trunctest`), `content${i}`);
            }
            const params = { pattern: '*.trunctest' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            const llmContent = partListUnionToString(result.llmContent);
            // Should report all 150 files found
            expect(llmContent).toContain('Found 150 file(s)');
            // Should include truncation notice
            expect(llmContent).toContain('[50 files truncated] ...');
            // Count the number of .trunctest files mentioned in the output
            const fileMatches = llmContent.match(/file\d+\.trunctest/g);
            expect(fileMatches).toBeDefined();
            expect(fileMatches?.length).toBe(100);
            // returnDisplay should indicate truncation
            expect(result.returnDisplay).toBe('Found 150 matching file(s) (truncated)');
        });
        it('should not truncate when exactly 100 files are found', async () => {
            // Create exactly 100 test files
            for (let i = 1; i <= 100; i++) {
                await fs.writeFile(path.join(tempRootDir, `exact${i}.trunctest`), `content${i}`);
            }
            const params = { pattern: '*.trunctest' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            // Should report all 100 files found
            expect(result.llmContent).toContain('Found 100 file(s)');
            // Should NOT include truncation notice
            expect(result.llmContent).not.toContain('truncated');
            // Should show all 100 files
            expect(result.llmContent).toContain('exact1.trunctest');
            expect(result.llmContent).toContain('exact100.trunctest');
            // returnDisplay should NOT indicate truncation
            expect(result.returnDisplay).toBe('Found 100 matching file(s)');
        });
        it('should not truncate when fewer than 100 files are found', async () => {
            // Create 50 test files
            for (let i = 1; i <= 50; i++) {
                await fs.writeFile(path.join(tempRootDir, `small${i}.trunctest`), `content${i}`);
            }
            const params = { pattern: '*.trunctest' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            // Should report all 50 files found
            expect(result.llmContent).toContain('Found 50 file(s)');
            // Should NOT include truncation notice
            expect(result.llmContent).not.toContain('truncated');
            // returnDisplay should NOT indicate truncation
            expect(result.returnDisplay).toBe('Found 50 matching file(s)');
        });
        it('should use correct singular/plural in truncation message for 1 file truncated', async () => {
            // Create 101 test files (will truncate 1 file)
            for (let i = 1; i <= 101; i++) {
                await fs.writeFile(path.join(tempRootDir, `singular${i}.trunctest`), `content${i}`);
            }
            const params = { pattern: '*.trunctest' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            // Should use singular "file" for 1 truncated file
            expect(result.llmContent).toContain('[1 file truncated] ...');
            expect(result.llmContent).not.toContain('[1 files truncated]');
        });
        it('should use correct plural in truncation message for multiple files truncated', async () => {
            // Create 105 test files (will truncate 5 files)
            for (let i = 1; i <= 105; i++) {
                await fs.writeFile(path.join(tempRootDir, `plural${i}.trunctest`), `content${i}`);
            }
            const params = { pattern: '*.trunctest' };
            const invocation = globTool.build(params);
            const result = await invocation.execute(abortSignal);
            // Should use plural "files" for multiple truncated files
            expect(result.llmContent).toContain('[5 files truncated] ...');
        });
    });
});
describe('sortFileEntries', () => {
    const nowTimestamp = new Date('2024-01-15T12:00:00.000Z').getTime();
    const oneDayInMs = 24 * 60 * 60 * 1000;
    const createFileEntry = (fullpath, mtimeDate) => ({
        fullpath: () => fullpath,
        mtimeMs: mtimeDate.getTime(),
    });
    it('should sort a mix of recent and older files correctly', () => {
        const recentTime1 = new Date(nowTimestamp - 1 * 60 * 60 * 1000); // 1 hour ago
        const recentTime2 = new Date(nowTimestamp - 2 * 60 * 60 * 1000); // 2 hours ago
        const olderTime1 = new Date(nowTimestamp - (oneDayInMs + 1 * 60 * 60 * 1000)); // 25 hours ago
        const olderTime2 = new Date(nowTimestamp - (oneDayInMs + 2 * 60 * 60 * 1000)); // 26 hours ago
        const entries = [
            createFileEntry('older_zebra.txt', olderTime2),
            createFileEntry('recent_alpha.txt', recentTime1),
            createFileEntry('older_apple.txt', olderTime1),
            createFileEntry('recent_beta.txt', recentTime2),
            createFileEntry('older_banana.txt', olderTime1), // Same mtime as apple
        ];
        const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
        const sortedPaths = sorted.map((e) => e.fullpath());
        expect(sortedPaths).toEqual([
            'recent_alpha.txt', // Recent, newest
            'recent_beta.txt', // Recent, older
            'older_apple.txt', // Older, alphabetical
            'older_banana.txt', // Older, alphabetical
            'older_zebra.txt', // Older, alphabetical
        ]);
    });
    it('should sort only recent files by mtime descending', () => {
        const recentTime1 = new Date(nowTimestamp - 1000); // Newest
        const recentTime2 = new Date(nowTimestamp - 2000);
        const recentTime3 = new Date(nowTimestamp - 3000); // Oldest recent
        const entries = [
            createFileEntry('c.txt', recentTime2),
            createFileEntry('a.txt', recentTime3),
            createFileEntry('b.txt', recentTime1),
        ];
        const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
        expect(sorted.map((e) => e.fullpath())).toEqual([
            'b.txt',
            'c.txt',
            'a.txt',
        ]);
    });
    it('should sort only older files alphabetically by path', () => {
        const olderTime = new Date(nowTimestamp - 2 * oneDayInMs); // All equally old
        const entries = [
            createFileEntry('zebra.txt', olderTime),
            createFileEntry('apple.txt', olderTime),
            createFileEntry('banana.txt', olderTime),
        ];
        const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
        expect(sorted.map((e) => e.fullpath())).toEqual([
            'apple.txt',
            'banana.txt',
            'zebra.txt',
        ]);
    });
    it('should handle an empty array', () => {
        const entries = [];
        const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
        expect(sorted).toEqual([]);
    });
    it('should correctly sort files when mtimes are identical for older files', () => {
        const olderTime = new Date(nowTimestamp - 2 * oneDayInMs);
        const entries = [
            createFileEntry('b.txt', olderTime),
            createFileEntry('a.txt', olderTime),
        ];
        const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
        expect(sorted.map((e) => e.fullpath())).toEqual(['a.txt', 'b.txt']);
    });
    it('should correctly sort files when mtimes are identical for recent files (maintaining mtime sort)', () => {
        const recentTime = new Date(nowTimestamp - 1000);
        const entries = [
            createFileEntry('b.txt', recentTime),
            createFileEntry('a.txt', recentTime),
        ];
        const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
        expect(sorted.map((e) => e.fullpath())).toContain('a.txt');
        expect(sorted.map((e) => e.fullpath())).toContain('b.txt');
        expect(sorted.length).toBe(2);
    });
    it('should use recencyThresholdMs parameter correctly', () => {
        const justOverThreshold = new Date(nowTimestamp - (1000 + 1)); // Barely older
        const justUnderThreshold = new Date(nowTimestamp - (1000 - 1)); // Barely recent
        const customThresholdMs = 1000; // 1 second
        const entries = [
            createFileEntry('older_file.txt', justOverThreshold),
            createFileEntry('recent_file.txt', justUnderThreshold),
        ];
        const sorted = sortFileEntries(entries, nowTimestamp, customThresholdMs);
        expect(sorted.map((e) => e.fullpath())).toEqual([
            'recent_file.txt',
            'older_file.txt',
        ]);
    });
});
//# sourceMappingURL=glob.test.js.map