/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
  afterEach,
  type Mock,
} from 'vitest';
import {
  handleAtCommand,
  escapeAtSymbols,
  unescapeLiteralAt,
} from './atCommandProcessor.js';
import {
  FileDiscoveryService,
  GlobTool,
  ReadManyFilesTool,
  StandardFileSystemService,
  ToolRegistry,
  COMMON_IGNORE_PATTERNS,
  GEMINI_IGNORE_FILE_NAME,
  ApprovalMode,
  // DEFAULT_FILE_EXCLUDES,
  CoreToolCallStatus,
  type Config,
  type DiscoveredMCPResource,
} from '@google/gemini-cli-core';
import * as core from '@google/gemini-cli-core';
import * as os from 'node:os';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import * as fsPromises from 'node:fs/promises';
import * as path from 'node:path';

describe('handleAtCommand', () => {
  let testRootDir: string;
  let mockConfig: Config;

  const mockAddItem: Mock<UseHistoryManagerReturn['addItem']> = vi.fn();
  const mockOnDebugMessage: Mock<(message: string) => void> = vi.fn();

  let abortController: AbortController;

  async function createTestFile(fullPath: string, fileContents: string) {
    await fsPromises.mkdir(path.dirname(fullPath), { recursive: true });
    await fsPromises.writeFile(fullPath, fileContents);
    return path.resolve(testRootDir, fullPath);
  }

  function getRelativePath(absolutePath: string): string {
    return path.relative(testRootDir, absolutePath);
  }

  beforeEach(async () => {
    vi.restoreAllMocks();
    vi.resetAllMocks();

    testRootDir = await fsPromises.mkdtemp(
      path.join(os.tmpdir(), 'folder-structure-test-'),
    );

    abortController = new AbortController();

    const getToolRegistry = vi.fn();

    const mockMessageBus = {
      publish: vi.fn(),
      subscribe: vi.fn(),
      unsubscribe: vi.fn(),
    } as unknown as core.MessageBus;

    mockConfig = {
      getToolRegistry,
      getTargetDir: () => testRootDir,
      isSandboxed: () => false,
      getExcludeTools: vi.fn(),
      getFileService: () => new FileDiscoveryService(testRootDir),
      getFileFilteringRespectGitIgnore: () => true,
      getFileFilteringRespectGeminiIgnore: () => true,
      getFileFilteringOptions: () => ({
        respectGitIgnore: true,
        respectGeminiIgnore: true,
      }),
      getFileSystemService: () => new StandardFileSystemService(),
      getEnableRecursiveFileSearch: vi.fn(() => true),
      getWorkspaceContext: () => ({
        isPathWithinWorkspace: (p: string) =>
          p.startsWith(testRootDir) || p.startsWith('/private' + testRootDir),
        getDirectories: () => [testRootDir],
      }),
      storage: {
        getProjectTempDir: () => path.join(os.tmpdir(), 'gemini-cli-temp'),
      },
      isPathAllowed(this: Config, absolutePath: string): boolean {
        if (this.interactive && path.isAbsolute(absolutePath)) {
          return true;
        }

        const workspaceContext = this.getWorkspaceContext();
        if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
          return true;
        }

        const projectTempDir = this.storage.getProjectTempDir();
        const resolvedProjectTempDir = path.resolve(projectTempDir);
        return (
          absolutePath.startsWith(resolvedProjectTempDir + path.sep) ||
          absolutePath === resolvedProjectTempDir
        );
      },
      validatePathAccess(this: Config, absolutePath: string): string | null {
        if (this.isPathAllowed(absolutePath)) {
          return null;
        }

        const workspaceDirs = this.getWorkspaceContext().getDirectories();
        const projectTempDir = this.storage.getProjectTempDir();
        return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
      },
      getMcpServers: () => ({}),
      getMcpServerCommand: () => undefined,
      getPromptRegistry: () => ({
        getPromptsByServer: () => [],
      }),
      getDebugMode: () => false,
      getWorkingDir: () => '/working/dir',
      getFileExclusions: () => ({
        getCoreIgnorePatterns: () => COMMON_IGNORE_PATTERNS,
        getDefaultExcludePatterns: () => [],
        getGlobExcludes: () => [],
        buildExcludePatterns: () => [],
        getReadManyFilesExcludes: () => [],
      }),
      getUsageStatisticsEnabled: () => false,
      getEnableExtensionReloading: () => false,
      getResourceRegistry: () => ({
        findResourceByUri: () => undefined,
        getAllResources: () => [],
      }),
      getMcpClientManager: () => ({
        getClient: () => undefined,
      }),
      getMessageBus: () => mockMessageBus,
      getApprovalMode: () => ApprovalMode.DEFAULT,
    } as unknown as Config;

    const registry = new ToolRegistry(mockConfig, mockMessageBus);
    registry.registerTool(new ReadManyFilesTool(mockConfig, mockMessageBus));
    registry.registerTool(new GlobTool(mockConfig, mockMessageBus));
    getToolRegistry.mockReturnValue(registry);
  });

  afterEach(async () => {
    abortController.abort();
    await fsPromises.rm(testRootDir, { recursive: true, force: true });
    vi.unstubAllGlobals();
  });

  it('should pass through query if no @ command is present', async () => {
    const query = 'regular user query';

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 123,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [{ text: query }],
    });
  });

  it('should pass through original query if only a lone @ symbol is present', async () => {
    const queryWithSpaces = '  @  ';

    const result = await handleAtCommand({
      query: queryWithSpaces,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 124,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [{ text: queryWithSpaces }],
    });
  });

  it('should process a valid text file path', async () => {
    const fileContent = 'This is the file content.';
    const filePath = await createTestFile(
      path.join(testRootDir, 'path', 'to', 'file.txt'),
      fileContent,
    );
    const relativePath = getRelativePath(filePath);
    const query = `@${filePath}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 125,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        { text: `@${relativePath}` },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${relativePath}:\n` },
        { text: fileContent },
        { text: '\n--- End of content ---' },
      ],
    });
    expect(mockAddItem).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'tool_group',
        tools: [
          expect.objectContaining({ status: CoreToolCallStatus.Success }),
        ],
      }),
      125,
    );
  });

  it('should process a valid directory path and convert to glob', async () => {
    const fileContent = 'This is the file content.';
    const filePath = await createTestFile(
      path.join(testRootDir, 'path', 'to', 'file.txt'),
      fileContent,
    );
    const dirPath = path.dirname(filePath);
    const relativeDirPath = getRelativePath(dirPath);
    const relativeFilePath = getRelativePath(filePath);
    const query = `@${dirPath}`;
    const resolvedGlob = path.join(relativeDirPath, '**');

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 126,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        { text: `@${resolvedGlob}` },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${relativeFilePath}:\n` },
        { text: fileContent },
        { text: '\n--- End of content ---' },
      ],
    });
    expect(mockOnDebugMessage).toHaveBeenCalledWith(
      `Path ${dirPath} resolved to directory, using glob: ${resolvedGlob}`,
    );
  });

  it('should handle query with text before and after @command', async () => {
    const fileContent = 'Markdown content.';
    const filePath = await createTestFile(
      path.join(testRootDir, 'doc.md'),
      fileContent,
    );
    const relativePath = getRelativePath(filePath);
    const textBefore = 'Explain this: ';
    const textAfter = ' in detail.';
    const query = `${textBefore}@${filePath}${textAfter}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 128,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        { text: `${textBefore}@${relativePath}${textAfter}` },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${relativePath}:\n` },
        { text: fileContent },
        { text: '\n--- End of content ---' },
      ],
    });
  });

  it('should correctly unescape paths with escaped spaces', async () => {
    const fileContent = 'This is the file content.';
    const filePath = await createTestFile(
      path.join(testRootDir, 'path', 'to', 'my file.txt'),
      fileContent,
    );

    const query = `@${core.escapePath(filePath)}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 125,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        { text: `@${getRelativePath(filePath)}` },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${getRelativePath(filePath)}:\n` },
        { text: fileContent },
        { text: '\n--- End of content ---' },
      ],
    });
    expect(mockAddItem).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'tool_group',
        tools: [
          expect.objectContaining({ status: CoreToolCallStatus.Success }),
        ],
      }),
      125,
    );
  }, 10000);

  it('should correctly handle double-quoted paths with spaces', async () => {
    // Mock platform to win32 so unescapePath strips quotes
    vi.stubGlobal(
      'process',
      Object.create(process, {
        platform: {
          get: () => 'win32',
        },
      }),
    );

    const fileContent = 'Content of file with spaces';
    const filePath = await createTestFile(
      path.join(testRootDir, 'my folder', 'my file.txt'),
      fileContent,
    );
    // On Windows, the user might provide: @"path/to/my file.txt"
    const query = `@"${filePath}"`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 126,
      signal: abortController.signal,
    });

    const relativePath = getRelativePath(filePath);
    expect(result).toEqual({
      processedQuery: [
        { text: `@${relativePath}` },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${relativePath}:\n` },
        { text: fileContent },
        { text: '\n--- End of content ---' },
      ],
    });
  });

  it('should correctly handle file paths with narrow non-breaking space (NNBSP)', async () => {
    const nnbsp = '\u202F';
    const fileContent = 'NNBSP file content.';
    const filePath = await createTestFile(
      path.join(testRootDir, `my${nnbsp}file.txt`),
      fileContent,
    );
    const relativePath = getRelativePath(filePath);
    const query = `@${filePath}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 129,
      signal: abortController.signal,
    });

    expect(result.error).toBeUndefined();
    expect(result.processedQuery).toEqual([
      { text: `@${relativePath}` },
      { text: '\n--- Content from referenced files ---' },
      { text: `\nContent from @${relativePath}:\n` },
      { text: fileContent },
      { text: '\n--- End of content ---' },
    ]);
  });

  it('should handle multiple @file references', async () => {
    const content1 = 'Content file1';
    const file1Path = await createTestFile(
      path.join(testRootDir, 'file1.txt'),
      content1,
    );
    const content2 = 'Content file2';
    const file2Path = await createTestFile(
      path.join(testRootDir, 'file2.md'),
      content2,
    );
    const query = `@${file1Path} @${file2Path}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 130,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        {
          text: `@${getRelativePath(file1Path)} @${getRelativePath(file2Path)}`,
        },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${getRelativePath(file1Path)}:\n` },
        { text: content1 },
        { text: `\nContent from @${getRelativePath(file2Path)}:\n` },
        { text: content2 },
        { text: '\n--- End of content ---' },
      ],
    });
  });

  it('should handle multiple @file references with interleaved text', async () => {
    const text1 = 'Check ';
    const content1 = 'C1';
    const file1Path = await createTestFile(
      path.join(testRootDir, 'f1.txt'),
      content1,
    );
    const text2 = ' and ';
    const content2 = 'C2';
    const file2Path = await createTestFile(
      path.join(testRootDir, 'f2.md'),
      content2,
    );
    const text3 = ' please.';
    const query = `${text1}@${file1Path}${text2}@${file2Path}${text3}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 131,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        {
          text: `${text1}@${getRelativePath(file1Path)}${text2}@${getRelativePath(file2Path)}${text3}`,
        },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${getRelativePath(file1Path)}:\n` },
        { text: content1 },
        { text: `\nContent from @${getRelativePath(file2Path)}:\n` },
        { text: content2 },
        { text: '\n--- End of content ---' },
      ],
    });
  });

  it('should handle a mix of valid, invalid, and lone @ references', async () => {
    const content1 = 'Valid content 1';
    const file1Path = await createTestFile(
      path.join(testRootDir, 'valid1.txt'),
      content1,
    );
    const invalidFile = 'nonexistent.txt';
    const content2 = 'Globbed content';
    const file2Path = await createTestFile(
      path.join(testRootDir, 'resolved', 'valid2.actual'),
      content2,
    );
    const query = `Look at @${file1Path} then @${invalidFile} and also just @ symbol, then @${file2Path}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 132,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        {
          text: `Look at @${getRelativePath(file1Path)} then @${invalidFile} and also just @ symbol, then @${getRelativePath(file2Path)}`,
        },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${getRelativePath(file2Path)}:\n` },
        { text: content2 },
        { text: `\nContent from @${getRelativePath(file1Path)}:\n` },
        { text: content1 },
        { text: '\n--- End of content ---' },
      ],
    });
    expect(mockOnDebugMessage).toHaveBeenCalledWith(
      `Path ${invalidFile} not found directly, attempting glob search.`,
    );
    expect(mockOnDebugMessage).toHaveBeenCalledWith(
      `Glob search for '**/*${invalidFile}*' found no files or an error. Path ${invalidFile} will be skipped.`,
    );
  });

  it('should return original query if all @paths are invalid or lone @', async () => {
    const query = 'Check @nonexistent.txt and @ also';

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 133,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [{ text: 'Check @nonexistent.txt and @ also' }],
    });
  });

  describe('git-aware filtering', () => {
    beforeEach(async () => {
      await fsPromises.mkdir(path.join(testRootDir, '.git'), {
        recursive: true,
      });
    });

    it('should skip git-ignored files in @ commands', async () => {
      await createTestFile(
        path.join(testRootDir, '.gitignore'),
        'node_modules/package.json',
      );
      const gitIgnoredFile = await createTestFile(
        path.join(testRootDir, 'node_modules', 'package.json'),
        'the file contents',
      );

      const query = `@${gitIgnoredFile}`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 200,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [{ text: query }],
      });
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
      );
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Ignored 1 files:\nGit-ignored: ${gitIgnoredFile}`,
      );
    });

    it('should process non-git-ignored files normally', async () => {
      await createTestFile(
        path.join(testRootDir, '.gitignore'),
        'node_modules/package.json',
      );

      const validFile = await createTestFile(
        path.join(testRootDir, 'src', 'index.ts'),
        'console.log("Hello world");',
      );
      const query = `@${validFile}`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 201,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          { text: `@${getRelativePath(validFile)}` },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(validFile)}:\n` },
          { text: 'console.log("Hello world");' },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should handle mixed git-ignored and valid files', async () => {
      await createTestFile(path.join(testRootDir, '.gitignore'), '.env');
      const validFile = await createTestFile(
        path.join(testRootDir, 'README.md'),
        '# Project README',
      );
      const gitIgnoredFile = await createTestFile(
        path.join(testRootDir, '.env'),
        'SECRET=123',
      );
      const query = `@${validFile} @${gitIgnoredFile}`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 202,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          { text: `@${getRelativePath(validFile)} @${gitIgnoredFile}` },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(validFile)}:\n` },
          { text: '# Project README' },
          { text: '\n--- End of content ---' },
        ],
      });
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
      );
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Ignored 1 files:\nGit-ignored: ${gitIgnoredFile}`,
      );
    });

    it('should always ignore .git directory files', async () => {
      const gitFile = await createTestFile(
        path.join(testRootDir, '.git', 'config'),
        '[core]\n\trepositoryformatversion = 0\n',
      );
      const query = `@${gitFile}`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 203,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [{ text: query }],
      });
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Path ${gitFile} is git-ignored and will be skipped.`,
      );
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Ignored 1 files:\nGit-ignored: ${gitFile}`,
      );
    });
  });

  describe('when recursive file search is disabled', () => {
    beforeEach(() => {
      vi.mocked(mockConfig.getEnableRecursiveFileSearch).mockReturnValue(false);
    });

    it('should not use glob search for a nonexistent file', async () => {
      const invalidFile = 'nonexistent.txt';
      const query = `@${invalidFile}`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 300,
        signal: abortController.signal,
      });

      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Glob tool not found. Path ${invalidFile} will be skipped.`,
      );
      expect(result.processedQuery).toEqual([{ text: query }]);
      expect(result.processedQuery).not.toBeNull();
      expect(result.error).toBeUndefined();
    });
  });

  describe('gemini-ignore filtering', () => {
    it('should skip gemini-ignored files in @ commands', async () => {
      await createTestFile(
        path.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
        'build/output.js',
      );
      const geminiIgnoredFile = await createTestFile(
        path.join(testRootDir, 'build', 'output.js'),
        'console.log("Hello");',
      );
      const query = `@${geminiIgnoredFile}`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 204,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [{ text: query }],
      });
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
      );
      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        `Ignored 1 files:\nGemini-ignored: ${geminiIgnoredFile}`,
      );
    });
  });
  it('should process non-ignored files when .geminiignore is present', async () => {
    await createTestFile(
      path.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
      'build/output.js',
    );
    const validFile = await createTestFile(
      path.join(testRootDir, 'src', 'index.ts'),
      'console.log("Hello world");',
    );
    const query = `@${validFile}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 205,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        { text: `@${getRelativePath(validFile)}` },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${getRelativePath(validFile)}:\n` },
        { text: 'console.log("Hello world");' },
        { text: '\n--- End of content ---' },
      ],
    });
  });

  it('should handle mixed gemini-ignored and valid files', async () => {
    await createTestFile(
      path.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
      'dist/bundle.js',
    );
    const validFile = await createTestFile(
      path.join(testRootDir, 'src', 'main.ts'),
      '// Main application entry',
    );
    const geminiIgnoredFile = await createTestFile(
      path.join(testRootDir, 'dist', 'bundle.js'),
      'console.log("bundle");',
    );
    const query = `@${validFile} @${geminiIgnoredFile}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 206,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: [
        { text: `@${getRelativePath(validFile)} @${geminiIgnoredFile}` },
        { text: '\n--- Content from referenced files ---' },
        { text: `\nContent from @${getRelativePath(validFile)}:\n` },
        { text: '// Main application entry' },
        { text: '\n--- End of content ---' },
      ],
    });
    expect(mockOnDebugMessage).toHaveBeenCalledWith(
      `Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
    );
    expect(mockOnDebugMessage).toHaveBeenCalledWith(
      `Ignored 1 files:\nGemini-ignored: ${geminiIgnoredFile}`,
    );
  });

  describe('punctuation termination in @ commands', () => {
    const punctuationTestCases = [
      {
        name: 'comma',
        fileName: 'test.txt',
        fileContent: 'File content here',
        queryTemplate: (filePath: string) =>
          `Look at @${getRelativePath(filePath)}, then explain it.`,
        messageId: 400,
      },
      {
        name: 'period',
        fileName: 'readme.md',
        fileContent: 'File content here',
        queryTemplate: (filePath: string) =>
          `Check @${getRelativePath(filePath)}. What does it say?`,
        messageId: 401,
      },
      {
        name: 'semicolon',
        fileName: 'example.js',
        fileContent: 'Code example',
        queryTemplate: (filePath: string) =>
          `Review @${getRelativePath(filePath)}; check for bugs.`,
        messageId: 402,
      },
      {
        name: 'exclamation mark',
        fileName: 'important.txt',
        fileContent: 'Important content',
        queryTemplate: (filePath: string) =>
          `Look at @${getRelativePath(filePath)}! This is critical.`,
        messageId: 403,
      },
      {
        name: 'question mark',
        fileName: 'config.json',
        fileContent: 'Config settings',
        queryTemplate: (filePath: string) =>
          `What is in @${getRelativePath(filePath)}? Please explain.`,
        messageId: 404,
      },
      {
        name: 'opening parenthesis',
        fileName: 'func.ts',
        fileContent: 'Function definition',
        queryTemplate: (filePath: string) =>
          `Analyze @${getRelativePath(filePath)}(the main function).`,
        messageId: 405,
      },
      {
        name: 'closing parenthesis',
        fileName: 'data.json',
        fileContent: 'Test data',
        queryTemplate: (filePath: string) =>
          `Use data from @${getRelativePath(filePath)}) for testing.`,
        messageId: 406,
      },
      {
        name: 'opening square bracket',
        fileName: 'array.js',
        fileContent: 'Array data',
        queryTemplate: (filePath: string) =>
          `Check @${getRelativePath(filePath)}[0] for the first element.`,
        messageId: 407,
      },
      {
        name: 'closing square bracket',
        fileName: 'list.md',
        fileContent: 'List content',
        queryTemplate: (filePath: string) =>
          `Review item @${getRelativePath(filePath)}] from the list.`,
        messageId: 408,
      },
      {
        name: 'opening curly brace',
        fileName: 'object.ts',
        fileContent: 'Object definition',
        queryTemplate: (filePath: string) =>
          `Parse @${getRelativePath(filePath)}{prop1: value1}.`,
        messageId: 409,
      },
      {
        name: 'closing curly brace',
        fileName: 'config.yaml',
        fileContent: 'Configuration',
        queryTemplate: (filePath: string) =>
          `Use settings from @${getRelativePath(filePath)}} for deployment.`,
        messageId: 410,
      },
    ];

    it.each(punctuationTestCases)(
      'should terminate @path at $name',
      async ({ fileName, fileContent, queryTemplate, messageId }) => {
        const filePath = await createTestFile(
          path.join(testRootDir, fileName),
          fileContent,
        );
        const query = queryTemplate(filePath);

        const result = await handleAtCommand({
          query,
          config: mockConfig,
          addItem: mockAddItem,
          onDebugMessage: mockOnDebugMessage,
          messageId,
          signal: abortController.signal,
        });

        expect(result).toEqual({
          processedQuery: [
            { text: query },
            { text: '\n--- Content from referenced files ---' },
            { text: `\nContent from @${getRelativePath(filePath)}:\n` },
            { text: fileContent },
            { text: '\n--- End of content ---' },
          ],
        });
      },
    );

    it('should handle multiple @paths terminated by different punctuation', async () => {
      const content1 = 'First file';
      const file1Path = await createTestFile(
        path.join(testRootDir, 'first.txt'),
        content1,
      );
      const content2 = 'Second file';
      const file2Path = await createTestFile(
        path.join(testRootDir, 'second.txt'),
        content2,
      );
      const query = `Compare @${file1Path}, @${file2Path}; what's different?`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 411,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          {
            text: `Compare @${getRelativePath(file1Path)}, @${getRelativePath(file2Path)}; what's different?`,
          },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(file1Path)}:\n` },
          { text: content1 },
          { text: `\nContent from @${getRelativePath(file2Path)}:\n` },
          { text: content2 },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should still handle escaped spaces in paths before punctuation', async () => {
      const fileContent = 'Spaced file content';
      const filePath = await createTestFile(
        path.join(testRootDir, 'spaced file.txt'),
        fileContent,
      );

      const query = `Check @${core.escapePath(filePath)}, it has spaces.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 412,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          { text: `Check @${getRelativePath(filePath)}, it has spaces.` },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should not break file paths with periods in extensions', async () => {
      const fileContent = 'TypeScript content';
      const filePath = await createTestFile(
        path.join(testRootDir, 'example.d.ts'),
        fileContent,
      );
      const query = `Analyze @${getRelativePath(filePath)} for type definitions.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 413,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          {
            text: `Analyze @${getRelativePath(filePath)} for type definitions.`,
          },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should handle file paths ending with period followed by space', async () => {
      const fileContent = 'Config content';
      const filePath = await createTestFile(
        path.join(testRootDir, 'config.json'),
        fileContent,
      );
      const query = `Check @${getRelativePath(filePath)}. This file contains settings.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 414,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          {
            text: `Check @${getRelativePath(filePath)}. This file contains settings.`,
          },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should handle comma termination with complex file paths', async () => {
      const fileContent = 'Package info';
      const filePath = await createTestFile(
        path.join(testRootDir, 'package.json'),
        fileContent,
      );
      const query = `Review @${getRelativePath(filePath)}, then check dependencies.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 415,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          {
            text: `Review @${getRelativePath(filePath)}, then check dependencies.`,
          },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should correctly handle file paths with multiple periods', async () => {
      const fileContent = 'Version info';
      const filePath = await createTestFile(
        path.join(testRootDir, 'version.1.2.3.txt'),
        fileContent,
      );
      const query = `Check @${getRelativePath(filePath)} contains version information.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 416,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          {
            text: `Check @${getRelativePath(filePath)} contains version information.`,
          },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should handle end of string termination for period and comma', async () => {
      const fileContent = 'End file content';
      const filePath = await createTestFile(
        path.join(testRootDir, 'end.txt'),
        fileContent,
      );
      const query = `Show me @${getRelativePath(filePath)}.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 417,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          { text: `Show me @${getRelativePath(filePath)}.` },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should handle files with special characters in names', async () => {
      const fileContent = 'File with special chars content';
      const filePath = await createTestFile(
        path.join(testRootDir, 'file$with&special#chars.txt'),
        fileContent,
      );
      const query = `Check @${getRelativePath(filePath)} for content.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 418,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          { text: `Check @${getRelativePath(filePath)} for content.` },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });

    it('should handle basic file names without special characters', async () => {
      const fileContent = 'Basic file content';
      const filePath = await createTestFile(
        path.join(testRootDir, 'basicfile.txt'),
        fileContent,
      );
      const query = `Check @${getRelativePath(filePath)} please.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 421,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          { text: `Check @${getRelativePath(filePath)} please.` },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${getRelativePath(filePath)}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });
    });
  });

  describe('absolute path handling', () => {
    it('should handle absolute file paths correctly', async () => {
      const fileContent = 'console.log("This is an absolute path test");';
      const relativePath = path.join('src', 'absolute-test.ts');
      const absolutePath = await createTestFile(
        path.join(testRootDir, relativePath),
        fileContent,
      );
      const query = `Check @${absolutePath} please.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 500,
        signal: abortController.signal,
      });

      expect(result).toEqual({
        processedQuery: [
          { text: `Check @${relativePath} please.` },
          { text: '\n--- Content from referenced files ---' },
          { text: `\nContent from @${relativePath}:\n` },
          { text: fileContent },
          { text: '\n--- End of content ---' },
        ],
      });

      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        expect.stringContaining(`using relative path: ${relativePath}`),
      );
    });

    it('should handle absolute directory paths correctly', async () => {
      const fileContent =
        'export default function test() { return "absolute dir test"; }';
      const subDirPath = path.join('src', 'utils');
      const fileName = 'helper.ts';
      await createTestFile(
        path.join(testRootDir, subDirPath, fileName),
        fileContent,
      );
      const absoluteDirPath = path.join(testRootDir, subDirPath);
      const query = `Check @${absoluteDirPath} please.`;

      const result = await handleAtCommand({
        query,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 501,
        signal: abortController.signal,
      });

      expect(result.processedQuery).not.toBeNull();
      expect(result.error).toBeUndefined();
      expect(result.processedQuery).toEqual(
        expect.arrayContaining([
          { text: `Check @${path.join(subDirPath, '**')} please.` },
          expect.objectContaining({
            text: '\n--- Content from referenced files ---',
          }),
        ]),
      );

      expect(mockOnDebugMessage).toHaveBeenCalledWith(
        expect.stringContaining(`using glob: ${path.join(subDirPath, '**')}`),
      );
    });
  });

  it("should not add the user's turn to history, as that is the caller's responsibility", async () => {
    // Arrange
    const fileContent = 'This is the file content.';
    const filePath = await createTestFile(
      path.join(testRootDir, 'path', 'to', 'another-file.txt'),
      fileContent,
    );
    const query = `A query with @${getRelativePath(filePath)}`;

    // Act
    await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 999,
      signal: abortController.signal,
    });

    // Assert
    // It SHOULD be called for the tool_group
    expect(mockAddItem).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'tool_group',
      }),
      999,
    );

    // It should NOT have been called for the user turn
    const userTurnCalls = mockAddItem.mock.calls.filter(
      (call) => call[0].type === 'user',
    );
    expect(userTurnCalls).toHaveLength(0);
  });

  describe('MCP resource attachments', () => {
    it('attaches MCP resource content when @serverName:uri matches registry', async () => {
      const serverName = 'server-1';
      const resourceUri = 'resource://server-1/logs';
      const prefixedUri = `${serverName}:${resourceUri}`;
      const resource = {
        serverName,
        uri: resourceUri,
        name: 'logs',
        discoveredAt: Date.now(),
      } as DiscoveredMCPResource;

      vi.spyOn(mockConfig, 'getResourceRegistry').mockReturnValue({
        findResourceByUri: (identifier: string) =>
          identifier === prefixedUri ? resource : undefined,
        getAllResources: () => [],
      } as never);

      const readResource = vi.fn().mockResolvedValue({
        contents: [{ text: 'mcp resource body' }],
      });
      vi.spyOn(mockConfig, 'getMcpClientManager').mockReturnValue({
        getClient: () => ({ readResource }),
      } as never);

      const result = await handleAtCommand({
        query: `@${prefixedUri}`,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 42,
        signal: abortController.signal,
      });

      expect(readResource).toHaveBeenCalledWith(resourceUri, {
        signal: abortController.signal,
      });
      const processedParts = Array.isArray(result.processedQuery)
        ? result.processedQuery
        : [];
      const containsResourceText = processedParts.some((part) => {
        const text = typeof part === 'string' ? part : part?.text;
        return typeof text === 'string' && text.includes('mcp resource body');
      });
      expect(containsResourceText).toBe(true);
      expect(mockAddItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'tool_group',
        }),
        expect.any(Number),
      );
    });

    it('returns an error if MCP client is unavailable', async () => {
      const serverName = 'server-1';
      const resourceUri = 'resource://server-1/logs';
      const prefixedUri = `${serverName}:${resourceUri}`;
      vi.spyOn(mockConfig, 'getResourceRegistry').mockReturnValue({
        findResourceByUri: (identifier: string) =>
          identifier === prefixedUri
            ? ({
                serverName,
                uri: resourceUri,
                discoveredAt: Date.now(),
              } as DiscoveredMCPResource)
            : undefined,
        getAllResources: () => [],
      } as never);
      vi.spyOn(mockConfig, 'getMcpClientManager').mockReturnValue({
        getClient: () => undefined,
      } as never);

      const result = await handleAtCommand({
        query: `@${prefixedUri}`,
        config: mockConfig,
        addItem: mockAddItem,
        onDebugMessage: mockOnDebugMessage,
        messageId: 42,
        signal: abortController.signal,
      });

      expect(result.processedQuery).toBeNull();
      expect(result.error).toBeDefined();
      expect(mockAddItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'tool_group',
          tools: expect.arrayContaining([
            expect.objectContaining({
              resultDisplay: expect.stringContaining(
                "MCP client for server 'server-1' is not available or not connected.",
              ),
            }),
          ]),
        }),
        expect.any(Number),
      );
    });
  });

  it('should return error if the read_many_files tool is cancelled by user', async () => {
    const fileContent = 'Some content';
    const filePath = await createTestFile(
      path.join(testRootDir, 'file.txt'),
      fileContent,
    );
    const query = `@${filePath}`;

    // Simulate user cancellation
    const mockToolInstance = {
      buildAndExecute: vi
        .fn()
        .mockRejectedValue(new Error('User cancelled operation')),
      displayName: 'Read Many Files',
      build: vi.fn(() => ({
        execute: mockToolInstance.buildAndExecute,
        getDescription: vi.fn(() => 'Mocked tool description'),
      })),
    };
    const viSpy = vi.spyOn(core, 'ReadManyFilesTool');
    viSpy.mockImplementation(
      () => mockToolInstance as unknown as core.ReadManyFilesTool,
    );

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 134,
      signal: abortController.signal,
    });

    expect(result).toEqual({
      processedQuery: null,
      error: `Exiting due to an error processing the @ command: Error reading files (file.txt): User cancelled operation`,
    });

    expect(mockAddItem).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'tool_group',
        tools: [expect.objectContaining({ status: CoreToolCallStatus.Error })],
      }),
      134,
    );
  });

  it('should include agent nudge when agents are found', async () => {
    const agentName = 'my-agent';
    const otherAgent = 'other-agent';

    // Mock getAgentRegistry on the config
    mockConfig.getAgentRegistry = vi.fn().mockReturnValue({
      getDefinition: (name: string) =>
        name === agentName || name === otherAgent ? { name } : undefined,
    });

    const query = `@${agentName} @${otherAgent}`;

    const result = await handleAtCommand({
      query,
      config: mockConfig,
      addItem: mockAddItem,
      onDebugMessage: mockOnDebugMessage,
      messageId: 600,
      signal: abortController.signal,
    });

    const expectedNudge = `\n<system_note>\nThe user has explicitly selected the following agent(s): ${agentName}, ${otherAgent}. Please use the following tool(s) to delegate the task: '${agentName}', '${otherAgent}'.\n</system_note>\n`;

    expect(result.processedQuery).toContainEqual(
      expect.objectContaining({ text: expectedNudge }),
    );
  });
});

describe('escapeAtSymbols', () => {
  it('escapes a bare @ symbol', () => {
    expect(escapeAtSymbols('test@domain.com')).toBe('test\\@domain.com');
  });

  it('escapes a leading @ symbol', () => {
    expect(escapeAtSymbols('@scope/pkg')).toBe('\\@scope/pkg');
  });

  it('escapes multiple @ symbols', () => {
    expect(escapeAtSymbols('a@b and c@d')).toBe('a\\@b and c\\@d');
  });

  it('does not double-escape an already escaped @', () => {
    expect(escapeAtSymbols('test\\@domain.com')).toBe('test\\@domain.com');
  });

  it('returns text with no @ unchanged', () => {
    expect(escapeAtSymbols('hello world')).toBe('hello world');
  });

  it('returns empty string unchanged', () => {
    expect(escapeAtSymbols('')).toBe('');
  });
});

describe('unescapeLiteralAt', () => {
  it('unescapes \\@ to @', () => {
    expect(unescapeLiteralAt('test\\@domain.com')).toBe('test@domain.com');
  });

  it('unescapes a leading \\@', () => {
    expect(unescapeLiteralAt('\\@scope/pkg')).toBe('@scope/pkg');
  });

  it('unescapes multiple \\@ sequences', () => {
    expect(unescapeLiteralAt('a\\@b and c\\@d')).toBe('a@b and c@d');
  });

  it('returns text with no \\@ unchanged', () => {
    expect(unescapeLiteralAt('hello world')).toBe('hello world');
  });

  it('returns empty string unchanged', () => {
    expect(unescapeLiteralAt('')).toBe('');
  });

  it('roundtrips correctly with escapeAtSymbols', () => {
    const input = 'user@example.com and @scope/pkg';
    expect(unescapeLiteralAt(escapeAtSymbols(input))).toBe(input);
  });
});
