/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GoogleGenAI, } from '@google/genai';
import { findIndexAfterFraction, GeminiClient } from './client.js';
import { AuthType } from './contentGenerator.js';
import { Config } from '../config/config.js';
import { GeminiEventType, Turn } from './turn.js';
import { getCoreSystemPrompt } from './prompts.js';
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { setSimulate429 } from '../utils/testUtils.js';
import { tokenLimit } from './tokenLimits.js';
import { ideContext } from '../ide/ideContext.js';
// --- Mocks ---
const mockChatCreateFn = vi.fn();
const mockGenerateContentFn = vi.fn();
const mockEmbedContentFn = vi.fn();
const mockTurnRunFn = vi.fn();
vi.mock('@google/genai');
vi.mock('./turn', () => {
    // Define a mock class that has the same shape as the real Turn
    class MockTurn {
        pendingToolCalls = [];
        // The run method is a property that holds our mock function
        run = mockTurnRunFn;
        constructor() {
            // The constructor can be empty or do some mock setup
        }
    }
    // Export the mock class as 'Turn'
    return {
        Turn: MockTurn,
        GeminiEventType: {
            MaxSessionTurns: 'MaxSessionTurns',
            ChatCompressed: 'ChatCompressed',
        },
    };
});
vi.mock('../config/config.js');
vi.mock('./prompts');
vi.mock('../utils/getFolderStructure', () => ({
    getFolderStructure: vi.fn().mockResolvedValue('Mock Folder Structure'),
}));
vi.mock('../utils/errorReporting', () => ({ reportError: vi.fn() }));
vi.mock('../utils/nextSpeakerChecker', () => ({
    checkNextSpeaker: vi.fn().mockResolvedValue(null),
}));
vi.mock('../utils/generateContentResponseUtilities', () => ({
    getResponseText: (result) => result.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
        undefined,
    getFunctionCalls: (result) => {
        // Extract function calls from the response
        const parts = result.candidates?.[0]?.content?.parts;
        if (!parts) {
            return undefined;
        }
        const functionCallParts = parts
            .filter((part) => !!part.functionCall)
            .map((part) => part.functionCall);
        return functionCallParts.length > 0 ? functionCallParts : undefined;
    },
}));
vi.mock('../telemetry/index.js', () => ({
    logApiRequest: vi.fn(),
    logApiResponse: vi.fn(),
    logApiError: vi.fn(),
}));
vi.mock('../ide/ideContext.js');
describe('findIndexAfterFraction', () => {
    const history = [
        { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66
        { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68
        { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66
        { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68
        { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65
    ];
    // Total length: 333
    it('should throw an error for non-positive numbers', () => {
        expect(() => findIndexAfterFraction(history, 0)).toThrow('Fraction must be between 0 and 1');
    });
    it('should throw an error for a fraction greater than or equal to 1', () => {
        expect(() => findIndexAfterFraction(history, 1)).toThrow('Fraction must be between 0 and 1');
    });
    it('should handle a fraction in the middle', () => {
        // 333 * 0.5 = 166.5
        // 0: 66
        // 1: 66 + 68 = 134
        // 2: 134 + 66 = 200
        // 200 >= 166.5, so index is 2
        expect(findIndexAfterFraction(history, 0.5)).toBe(2);
    });
    it('should handle a fraction that results in the last index', () => {
        // 333 * 0.9 = 299.7
        // ...
        // 3: 200 + 68 = 268
        // 4: 268 + 65 = 333
        // 333 >= 299.7, so index is 4
        expect(findIndexAfterFraction(history, 0.9)).toBe(4);
    });
    it('should handle an empty history', () => {
        expect(findIndexAfterFraction([], 0.5)).toBe(0);
    });
    it('should handle a history with only one item', () => {
        expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(0);
    });
    it('should handle history with weird parts', () => {
        const historyWithEmptyParts = [
            { role: 'user', parts: [{ text: 'Message 1' }] },
            { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
            { role: 'user', parts: [{ text: 'Message 2' }] },
        ];
        expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(1);
    });
});
describe('Gemini Client (client.ts)', () => {
    let client;
    beforeEach(async () => {
        vi.resetAllMocks();
        // Disable 429 simulation for tests
        setSimulate429(false);
        // Set up the mock for GoogleGenAI constructor and its methods
        const MockedGoogleGenAI = vi.mocked(GoogleGenAI);
        MockedGoogleGenAI.mockImplementation(() => {
            const mock = {
                chats: { create: mockChatCreateFn },
                models: {
                    generateContent: mockGenerateContentFn,
                    embedContent: mockEmbedContentFn,
                },
            };
            return mock;
        });
        mockChatCreateFn.mockResolvedValue({});
        mockGenerateContentFn.mockResolvedValue({
            candidates: [
                {
                    content: {
                        parts: [
                            {
                                functionCall: {
                                    name: 'respond_in_schema',
                                    args: { key: 'value' },
                                },
                            },
                        ],
                    },
                },
            ],
        });
        // Because the GeminiClient constructor kicks off an async process (startChat)
        // that depends on a fully-formed Config object, we need to mock the
        // entire implementation of Config for these tests.
        const mockToolRegistry = {
            getFunctionDeclarations: vi.fn().mockReturnValue([]),
            getTool: vi.fn().mockReturnValue(null),
        };
        const fileService = new FileDiscoveryService('/test/dir');
        const contentGeneratorConfig = {
            model: 'test-model',
            apiKey: 'test-key',
            vertexai: false,
            authType: AuthType.USE_GEMINI,
        };
        const mockConfigObject = {
            getContentGeneratorConfig: vi
                .fn()
                .mockReturnValue(contentGeneratorConfig),
            getToolRegistry: vi.fn().mockResolvedValue(mockToolRegistry),
            getModel: vi.fn().mockReturnValue('test-model'),
            getEmbeddingModel: vi.fn().mockReturnValue('test-embedding-model'),
            getApiKey: vi.fn().mockReturnValue('test-key'),
            getVertexAI: vi.fn().mockReturnValue(false),
            getUserAgent: vi.fn().mockReturnValue('test-agent'),
            getUserMemory: vi.fn().mockReturnValue(''),
            getFullContext: vi.fn().mockReturnValue(false),
            getSessionId: vi.fn().mockReturnValue('test-session-id'),
            getProxy: vi.fn().mockReturnValue(undefined),
            getWorkingDir: vi.fn().mockReturnValue('/test/dir'),
            getFileService: vi.fn().mockReturnValue(fileService),
            getMaxSessionTurns: vi.fn().mockReturnValue(0),
            getSessionTokenLimit: vi.fn().mockReturnValue(32000),
            getMaxFolderItems: vi.fn().mockReturnValue(20),
            getQuotaErrorOccurred: vi.fn().mockReturnValue(false),
            setQuotaErrorOccurred: vi.fn(),
            getNoBrowser: vi.fn().mockReturnValue(false),
            getSystemPromptMappings: vi.fn().mockReturnValue(undefined),
            getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
            getIdeModeFeature: vi.fn().mockReturnValue(false),
            getIdeMode: vi.fn().mockReturnValue(true),
            getWorkspaceContext: vi.fn().mockReturnValue({
                getDirectories: vi.fn().mockReturnValue(['/test/dir']),
            }),
            getGeminiClient: vi.fn(),
            setFallbackMode: vi.fn(),
            getDebugMode: vi.fn().mockReturnValue(false),
            getCliVersion: vi.fn().mockReturnValue('1.0.0'),
        };
        const MockedConfig = vi.mocked(Config, true);
        MockedConfig.mockImplementation(() => mockConfigObject);
        // We can instantiate the client here since Config is mocked
        // and the constructor will use the mocked GoogleGenAI
        client = new GeminiClient(new Config({ sessionId: 'test-session-id' }));
        mockConfigObject.getGeminiClient.mockReturnValue(client);
        await client.initialize(contentGeneratorConfig);
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });
    // NOTE: The following tests for startChat were removed due to persistent issues with
    // the @google/genai mock. Specifically, the mockChatCreateFn (representing instance.chats.create)
    // was not being detected as called by the GeminiClient instance.
    // This likely points to a subtle issue in how the GoogleGenerativeAI class constructor
    // and its instance methods are mocked and then used by the class under test.
    // For future debugging, ensure that the `this.client` in `GeminiClient` (which is an
    // instance of the mocked GoogleGenerativeAI) correctly has its `chats.create` method
    // pointing to `mockChatCreateFn`.
    // it('startChat should call getCoreSystemPrompt with userMemory and pass to chats.create', async () => { ... });
    // it('startChat should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
    // NOTE: The following tests for generateJson were removed due to persistent issues with
    // the @google/genai mock, similar to the startChat tests. The mockGenerateContentFn
    // (representing instance.models.generateContent) was not being detected as called, or the mock
    // was not preventing an actual API call (leading to API key errors).
    // For future debugging, ensure `this.client.models.generateContent` in `GeminiClient` correctly
    // uses the `mockGenerateContentFn`.
    // it('generateJson should call getCoreSystemPrompt with userMemory and pass to generateContent', async () => { ... });
    // it('generateJson should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
    describe('generateEmbedding', () => {
        const texts = ['hello world', 'goodbye world'];
        const testEmbeddingModel = 'test-embedding-model';
        it('should call embedContent with correct parameters and return embeddings', async () => {
            const mockEmbeddings = [
                [0.1, 0.2, 0.3],
                [0.4, 0.5, 0.6],
            ];
            const mockResponse = {
                embeddings: [
                    { values: mockEmbeddings[0] },
                    { values: mockEmbeddings[1] },
                ],
            };
            mockEmbedContentFn.mockResolvedValue(mockResponse);
            const result = await client.generateEmbedding(texts);
            expect(mockEmbedContentFn).toHaveBeenCalledTimes(1);
            expect(mockEmbedContentFn).toHaveBeenCalledWith({
                model: testEmbeddingModel,
                contents: texts,
            });
            expect(result).toEqual(mockEmbeddings);
        });
        it('should return an empty array if an empty array is passed', async () => {
            const result = await client.generateEmbedding([]);
            expect(result).toEqual([]);
            expect(mockEmbedContentFn).not.toHaveBeenCalled();
        });
        it('should throw an error if API response has no embeddings array', async () => {
            mockEmbedContentFn.mockResolvedValue({}); // No `embeddings` key
            await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
        });
        it('should throw an error if API response has an empty embeddings array', async () => {
            const mockResponse = {
                embeddings: [],
            };
            mockEmbedContentFn.mockResolvedValue(mockResponse);
            await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
        });
        it('should throw an error if API returns a mismatched number of embeddings', async () => {
            const mockResponse = {
                embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
            };
            mockEmbedContentFn.mockResolvedValue(mockResponse);
            await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
        });
        it('should throw an error if any embedding has nullish values', async () => {
            const mockResponse = {
                embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
            };
            mockEmbedContentFn.mockResolvedValue(mockResponse);
            await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
        });
        it('should throw an error if any embedding has an empty values array', async () => {
            const mockResponse = {
                embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
            };
            mockEmbedContentFn.mockResolvedValue(mockResponse);
            await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
        });
        it('should propagate errors from the API call', async () => {
            const apiError = new Error('API Failure');
            mockEmbedContentFn.mockRejectedValue(apiError);
            await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
        });
    });
    describe('generateContent', () => {
        it('should call generateContent with the correct parameters', async () => {
            const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
            const generationConfig = { temperature: 0.5 };
            const abortSignal = new AbortController().signal;
            // Mock countTokens
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            await client.generateContent(contents, generationConfig, abortSignal);
            expect(mockGenerateContentFn).toHaveBeenCalledWith({
                model: 'test-model',
                config: {
                    abortSignal,
                    systemInstruction: getCoreSystemPrompt(''),
                    temperature: 0.5,
                    topP: 1,
                },
                contents,
            }, 'test-session-id');
        });
    });
    describe('generateJson', () => {
        it('should call generateContent with the correct parameters', async () => {
            const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
            const schema = { type: 'string' };
            const abortSignal = new AbortController().signal;
            // Mock countTokens
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            const result = await client.generateJson(contents, schema, abortSignal);
            expect(result).toEqual({ key: 'value' });
            expect(mockGenerateContentFn).toHaveBeenCalledWith({
                model: 'test-model', // Should use current model from config
                config: {
                    abortSignal,
                    systemInstruction: getCoreSystemPrompt(''),
                    temperature: 0,
                    topP: 1,
                    tools: [
                        {
                            functionDeclarations: [
                                {
                                    name: 'respond_in_schema',
                                    description: 'Provide the response in provided schema',
                                    parameters: schema,
                                },
                            ],
                        },
                    ],
                },
                contents,
            }, 'test-session-id');
        });
        it('should allow overriding model and config', async () => {
            const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
            const schema = { type: 'string' };
            const abortSignal = new AbortController().signal;
            const customModel = 'custom-json-model';
            const customConfig = { temperature: 0.9, topK: 20 };
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            const result = await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
            expect(result).toEqual({ key: 'value' });
            expect(mockGenerateContentFn).toHaveBeenCalledWith({
                model: customModel,
                config: {
                    abortSignal,
                    systemInstruction: getCoreSystemPrompt(''),
                    temperature: 0.9,
                    topP: 1, // from default
                    topK: 20,
                    tools: [
                        {
                            functionDeclarations: [
                                {
                                    name: 'respond_in_schema',
                                    description: 'Provide the response in provided schema',
                                    parameters: schema,
                                },
                            ],
                        },
                    ],
                },
                contents,
            }, 'test-session-id');
        });
    });
    describe('addHistory', () => {
        it('should call chat.addHistory with the provided content', async () => {
            const mockChat = {
                addHistory: vi.fn(),
            };
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            client['chat'] = mockChat;
            const newContent = {
                role: 'user',
                parts: [{ text: 'New history item' }],
            };
            await client.addHistory(newContent);
            expect(mockChat.addHistory).toHaveBeenCalledWith(newContent);
        });
    });
    describe('resetChat', () => {
        it('should create a new chat session, clearing the old history', async () => {
            // 1. Get the initial chat instance and add some history.
            const initialChat = client.getChat();
            const initialHistory = await client.getHistory();
            await client.addHistory({
                role: 'user',
                parts: [{ text: 'some old message' }],
            });
            const historyWithOldMessage = await client.getHistory();
            expect(historyWithOldMessage.length).toBeGreaterThan(initialHistory.length);
            // 2. Call resetChat.
            await client.resetChat();
            // 3. Get the new chat instance and its history.
            const newChat = client.getChat();
            const newHistory = await client.getHistory();
            // 4. Assert that the chat instance is new and the history is reset.
            expect(newChat).not.toBe(initialChat);
            expect(newHistory.length).toBe(initialHistory.length);
            expect(JSON.stringify(newHistory)).not.toContain('some old message');
        });
    });
    describe('tryCompressChat', () => {
        const mockCountTokens = vi.fn();
        const mockSendMessage = vi.fn();
        const mockGetHistory = vi.fn();
        beforeEach(() => {
            vi.mock('./tokenLimits', () => ({
                tokenLimit: vi.fn(),
            }));
            client['contentGenerator'] = {
                countTokens: mockCountTokens,
            };
            client['chat'] = {
                getHistory: mockGetHistory,
                addHistory: vi.fn(),
                setHistory: vi.fn(),
                sendMessage: mockSendMessage,
            };
        });
        it('should not trigger summarization if token count is below threshold', async () => {
            const MOCKED_TOKEN_LIMIT = 1000;
            vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
            mockGetHistory.mockReturnValue([
                { role: 'user', parts: [{ text: '...history...' }] },
            ]);
            mockCountTokens.mockResolvedValue({
                totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
            });
            const initialChat = client.getChat();
            const result = await client.tryCompressChat('prompt-id-2');
            const newChat = client.getChat();
            expect(tokenLimit).toHaveBeenCalled();
            expect(result).toBeNull();
            expect(newChat).toBe(initialChat);
        });
        it('should trigger summarization if token count is at threshold', async () => {
            const MOCKED_TOKEN_LIMIT = 1000;
            vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
            mockGetHistory.mockReturnValue([
                { role: 'user', parts: [{ text: '...history...' }] },
            ]);
            const originalTokenCount = 1000 * 0.7;
            const newTokenCount = 100;
            mockCountTokens
                .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
                .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
            // Mock the summary response from the chat
            mockSendMessage.mockResolvedValue({
                role: 'model',
                parts: [{ text: 'This is a summary.' }],
            });
            const initialChat = client.getChat();
            const result = await client.tryCompressChat('prompt-id-3');
            const newChat = client.getChat();
            expect(tokenLimit).toHaveBeenCalled();
            expect(mockSendMessage).toHaveBeenCalled();
            // Assert that summarization happened and returned the correct stats
            expect(result).toEqual({
                originalTokenCount,
                newTokenCount,
            });
            // Assert that the chat was reset
            expect(newChat).not.toBe(initialChat);
        });
        it('should not compress across a function call response', async () => {
            const MOCKED_TOKEN_LIMIT = 1000;
            vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
            mockGetHistory.mockReturnValue([
                { role: 'user', parts: [{ text: '...history 1...' }] },
                { role: 'model', parts: [{ text: '...history 2...' }] },
                { role: 'user', parts: [{ text: '...history 3...' }] },
                { role: 'model', parts: [{ text: '...history 4...' }] },
                { role: 'user', parts: [{ text: '...history 5...' }] },
                { role: 'model', parts: [{ text: '...history 6...' }] },
                { role: 'user', parts: [{ text: '...history 7...' }] },
                { role: 'model', parts: [{ text: '...history 8...' }] },
                // Normally we would break here, but we have a function response.
                {
                    role: 'user',
                    parts: [{ functionResponse: { name: '...history 8...' } }],
                },
                { role: 'model', parts: [{ text: '...history 10...' }] },
                // Instead we will break here.
                { role: 'user', parts: [{ text: '...history 10...' }] },
            ]);
            const originalTokenCount = 1000 * 0.7;
            const newTokenCount = 100;
            mockCountTokens
                .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
                .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
            // Mock the summary response from the chat
            mockSendMessage.mockResolvedValue({
                role: 'model',
                parts: [{ text: 'This is a summary.' }],
            });
            const initialChat = client.getChat();
            const result = await client.tryCompressChat('prompt-id-3');
            const newChat = client.getChat();
            expect(tokenLimit).toHaveBeenCalled();
            expect(mockSendMessage).toHaveBeenCalled();
            // Assert that summarization happened and returned the correct stats
            expect(result).toEqual({
                originalTokenCount,
                newTokenCount,
            });
            // Assert that the chat was reset
            expect(newChat).not.toBe(initialChat);
            // 1. standard start context message
            // 2. standard canned user start message
            // 3. compressed summary message
            // 4. standard canned user summary message
            // 5. The last user message (not the last 3 because that would start with a function response)
            expect(newChat.getHistory().length).toEqual(5);
        });
        it('should always trigger summarization when force is true, regardless of token count', async () => {
            mockGetHistory.mockReturnValue([
                { role: 'user', parts: [{ text: '...history...' }] },
            ]);
            const originalTokenCount = 10; // Well below threshold
            const newTokenCount = 5;
            mockCountTokens
                .mockResolvedValueOnce({ totalTokens: originalTokenCount })
                .mockResolvedValueOnce({ totalTokens: newTokenCount });
            // Mock the summary response from the chat
            mockSendMessage.mockResolvedValue({
                role: 'model',
                parts: [{ text: 'This is a summary.' }],
            });
            const initialChat = client.getChat();
            const result = await client.tryCompressChat('prompt-id-1', true); // force = true
            const newChat = client.getChat();
            expect(mockSendMessage).toHaveBeenCalled();
            expect(result).toEqual({
                originalTokenCount,
                newTokenCount,
            });
            // Assert that the chat was reset
            expect(newChat).not.toBe(initialChat);
        });
    });
    describe('sendMessageStream', () => {
        it('should include IDE context when ideModeFeature is enabled', async () => {
            // Arrange
            vi.mocked(ideContext.getIdeContext).mockReturnValue({
                workspaceState: {
                    openFiles: [
                        {
                            path: '/path/to/active/file.ts',
                            timestamp: Date.now(),
                            isActive: true,
                            selectedText: 'hello',
                            cursor: { line: 5, character: 10 },
                        },
                        {
                            path: '/path/to/recent/file1.ts',
                            timestamp: Date.now(),
                        },
                        {
                            path: '/path/to/recent/file2.ts',
                            timestamp: Date.now(),
                        },
                    ],
                },
            });
            vi.spyOn(client['config'], 'getIdeModeFeature').mockReturnValue(true);
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Hello' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            const initialRequest = [{ text: 'Hi' }];
            // Act
            const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
            for await (const _ of stream) {
                // consume stream
            }
            // Assert
            expect(ideContext.getIdeContext).toHaveBeenCalled();
            const expectedContext = `
This is the file that the user is looking at:
- Path: /path/to/active/file.ts
This is the cursor position in the file:
- Cursor Position: Line 5, Character 10
This is the selected text in the file:
- hello
Here are some other files the user has open, with the most recent at the top:
- /path/to/recent/file1.ts
- /path/to/recent/file2.ts
      `.trim();
            const expectedRequest = [{ text: expectedContext }, ...initialRequest];
            expect(mockTurnRunFn).toHaveBeenCalledWith(expectedRequest, expect.any(Object));
        });
        it('should not add context if ideModeFeature is enabled but no open files', async () => {
            // Arrange
            vi.mocked(ideContext.getIdeContext).mockReturnValue({
                workspaceState: {
                    openFiles: [],
                },
            });
            vi.spyOn(client['config'], 'getIdeModeFeature').mockReturnValue(true);
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Hello' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            const initialRequest = [{ text: 'Hi' }];
            // Act
            const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
            for await (const _ of stream) {
                // consume stream
            }
            // Assert
            expect(ideContext.getIdeContext).toHaveBeenCalled();
            expect(mockTurnRunFn).toHaveBeenCalledWith(initialRequest, expect.any(Object));
        });
        it('should add context if ideModeFeature is enabled and there is one active file', async () => {
            // Arrange
            vi.mocked(ideContext.getIdeContext).mockReturnValue({
                workspaceState: {
                    openFiles: [
                        {
                            path: '/path/to/active/file.ts',
                            timestamp: Date.now(),
                            isActive: true,
                            selectedText: 'hello',
                            cursor: { line: 5, character: 10 },
                        },
                    ],
                },
            });
            vi.spyOn(client['config'], 'getIdeModeFeature').mockReturnValue(true);
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Hello' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            const initialRequest = [{ text: 'Hi' }];
            // Act
            const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
            for await (const _ of stream) {
                // consume stream
            }
            // Assert
            expect(ideContext.getIdeContext).toHaveBeenCalled();
            const expectedContext = `
This is the file that the user is looking at:
- Path: /path/to/active/file.ts
This is the cursor position in the file:
- Cursor Position: Line 5, Character 10
This is the selected text in the file:
- hello
      `.trim();
            const expectedRequest = [{ text: expectedContext }, ...initialRequest];
            expect(mockTurnRunFn).toHaveBeenCalledWith(expectedRequest, expect.any(Object));
        });
        it('should add context if ideModeFeature is enabled and there are open files but no active file', async () => {
            // Arrange
            vi.mocked(ideContext.getIdeContext).mockReturnValue({
                workspaceState: {
                    openFiles: [
                        {
                            path: '/path/to/recent/file1.ts',
                            timestamp: Date.now(),
                        },
                        {
                            path: '/path/to/recent/file2.ts',
                            timestamp: Date.now(),
                        },
                    ],
                },
            });
            vi.spyOn(client['config'], 'getIdeModeFeature').mockReturnValue(true);
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Hello' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            const initialRequest = [{ text: 'Hi' }];
            // Act
            const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
            for await (const _ of stream) {
                // consume stream
            }
            // Assert
            expect(ideContext.getIdeContext).toHaveBeenCalled();
            const expectedContext = `
Here are some files the user has open, with the most recent at the top:
- /path/to/recent/file1.ts
- /path/to/recent/file2.ts
      `.trim();
            const expectedRequest = [{ text: expectedContext }, ...initialRequest];
            expect(mockTurnRunFn).toHaveBeenCalledWith(expectedRequest, expect.any(Object));
        });
        it('should return the turn instance after the stream is complete', async () => {
            // Arrange
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Hello' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            // Act
            const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
            // Consume the stream manually to get the final return value.
            let finalResult;
            while (true) {
                const result = await stream.next();
                if (result.done) {
                    finalResult = result.value;
                    break;
                }
            }
            // Assert
            expect(finalResult).toBeInstanceOf(Turn);
        });
        it('should stop infinite loop after MAX_TURNS when nextSpeaker always returns model', async () => {
            // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
            const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
            const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
            mockCheckNextSpeaker.mockResolvedValue({
                next_speaker: 'model',
                reasoning: 'Test case - always continue',
            });
            // Mock Turn to have no pending tool calls (which would allow nextSpeaker check)
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Continue...' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            // Use a signal that never gets aborted
            const abortController = new AbortController();
            const signal = abortController.signal;
            // Act - Start the stream that should loop
            const stream = client.sendMessageStream([{ text: 'Start conversation' }], signal, 'prompt-id-2');
            // Count how many stream events we get
            let eventCount = 0;
            let finalResult;
            // Consume the stream and count iterations
            while (true) {
                const result = await stream.next();
                if (result.done) {
                    finalResult = result.value;
                    break;
                }
                eventCount++;
                // Safety check to prevent actual infinite loop in test
                if (eventCount > 200) {
                    abortController.abort();
                    throw new Error('Test exceeded expected event limit - possible actual infinite loop');
                }
            }
            // Assert
            expect(finalResult).toBeInstanceOf(Turn);
            // Debug: Check how many times checkNextSpeaker was called
            const callCount = mockCheckNextSpeaker.mock.calls.length;
            // If infinite loop protection is working, checkNextSpeaker should be called many times
            // but stop at MAX_TURNS (100). Since each recursive call should trigger checkNextSpeaker,
            // we expect it to be called multiple times before hitting the limit
            expect(mockCheckNextSpeaker).toHaveBeenCalled();
            // The test should demonstrate that the infinite loop protection works:
            // - If checkNextSpeaker is called many times (close to MAX_TURNS), it shows the loop was happening
            // - If it's only called once, the recursive behavior might not be triggered
            if (callCount === 0) {
                throw new Error('checkNextSpeaker was never called - the recursive condition was not met');
            }
            else if (callCount === 1) {
                // This might be expected behavior if the turn has pending tool calls or other conditions prevent recursion
                console.log('checkNextSpeaker called only once - no infinite loop occurred');
            }
            else {
                console.log(`checkNextSpeaker called ${callCount} times - infinite loop protection worked`);
                // If called multiple times, we expect it to be stopped before MAX_TURNS
                expect(callCount).toBeLessThanOrEqual(100); // Should not exceed MAX_TURNS
            }
            // The stream should produce events and eventually terminate
            expect(eventCount).toBeGreaterThanOrEqual(1);
            expect(eventCount).toBeLessThan(200); // Should not exceed our safety limit
        });
        it('should yield MaxSessionTurns and stop when session turn limit is reached', async () => {
            // Arrange
            const MAX_SESSION_TURNS = 5;
            vi.spyOn(client['config'], 'getMaxSessionTurns').mockReturnValue(MAX_SESSION_TURNS);
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Hello' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            // Act & Assert
            // Run up to the limit
            for (let i = 0; i < MAX_SESSION_TURNS; i++) {
                const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-4');
                // consume stream
                for await (const _event of stream) {
                    // do nothing
                }
            }
            // This call should exceed the limit
            const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-5');
            const events = [];
            for await (const event of stream) {
                events.push(event);
            }
            expect(events).toEqual([{ type: GeminiEventType.MaxSessionTurns }]);
            expect(mockTurnRunFn).toHaveBeenCalledTimes(MAX_SESSION_TURNS);
        });
        it('should respect MAX_TURNS limit even when turns parameter is set to a large value', async () => {
            // This test verifies that the infinite loop protection works even when
            // someone tries to bypass it by calling with a very large turns value
            // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
            const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
            const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
            mockCheckNextSpeaker.mockResolvedValue({
                next_speaker: 'model',
                reasoning: 'Test case - always continue',
            });
            // Mock Turn to have no pending tool calls (which would allow nextSpeaker check)
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Continue...' };
            })();
            mockTurnRunFn.mockReturnValue(mockStream);
            const mockChat = {
                addHistory: vi.fn(),
                getHistory: vi.fn().mockReturnValue([]),
            };
            client['chat'] = mockChat;
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            // Use a signal that never gets aborted
            const abortController = new AbortController();
            const signal = abortController.signal;
            // Act - Start the stream with an extremely high turns value
            // This simulates a case where the turns protection is bypassed
            const stream = client.sendMessageStream([{ text: 'Start conversation' }], signal, 'prompt-id-3', Number.MAX_SAFE_INTEGER);
            // Count how many stream events we get
            let eventCount = 0;
            const maxTestIterations = 1000; // Higher limit to show the loop continues
            // Consume the stream and count iterations
            try {
                while (true) {
                    const result = await stream.next();
                    if (result.done) {
                        break;
                    }
                    eventCount++;
                    // This test should hit this limit, demonstrating the infinite loop
                    if (eventCount > maxTestIterations) {
                        abortController.abort();
                        // This is the expected behavior - we hit the infinite loop
                        break;
                    }
                }
            }
            catch (error) {
                // If the test framework times out, that also demonstrates the infinite loop
                console.error('Test timed out or errored:', error);
            }
            // Assert that the fix works - the loop should stop at MAX_TURNS
            const callCount = mockCheckNextSpeaker.mock.calls.length;
            // With the fix: even when turns is set to a very high value,
            // the loop should stop at MAX_TURNS (100)
            expect(callCount).toBeLessThanOrEqual(100); // Should not exceed MAX_TURNS
            expect(eventCount).toBeLessThanOrEqual(200); // Should have reasonable number of events
            console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
                `${eventCount} events generated (properly bounded by MAX_TURNS)`);
        });
    });
    describe('generateContent', () => {
        it('should use current model from config for content generation', async () => {
            const initialModel = client['config'].getModel();
            const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
            const currentModel = initialModel + '-changed';
            vi.spyOn(client['config'], 'getModel').mockReturnValueOnce(currentModel);
            const mockGenerator = {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
                generateContent: mockGenerateContentFn,
            };
            client['contentGenerator'] = mockGenerator;
            await client.generateContent(contents, {}, new AbortController().signal);
            expect(mockGenerateContentFn).not.toHaveBeenCalledWith({
                model: initialModel,
                config: expect.any(Object),
                contents,
            });
            expect(mockGenerateContentFn).toHaveBeenCalledWith({
                model: currentModel,
                config: expect.any(Object),
                contents,
            }, 'test-session-id');
        });
    });
    describe('tryCompressChat', () => {
        it('should use current model from config for token counting after sendMessage', async () => {
            const initialModel = client['config'].getModel();
            const mockCountTokens = vi
                .fn()
                .mockResolvedValueOnce({ totalTokens: 100000 })
                .mockResolvedValueOnce({ totalTokens: 5000 });
            const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
            const mockChatHistory = [
                { role: 'user', parts: [{ text: 'Long conversation' }] },
                { role: 'model', parts: [{ text: 'Long response' }] },
            ];
            const mockChat = {
                getHistory: vi.fn().mockReturnValue(mockChatHistory),
                setHistory: vi.fn(),
                sendMessage: mockSendMessage,
            };
            const mockGenerator = {
                countTokens: mockCountTokens,
            };
            // mock the model has been changed between calls of `countTokens`
            const firstCurrentModel = initialModel + '-changed-1';
            const secondCurrentModel = initialModel + '-changed-2';
            vi.spyOn(client['config'], 'getModel')
                .mockReturnValueOnce(firstCurrentModel)
                .mockReturnValueOnce(secondCurrentModel);
            client['chat'] = mockChat;
            client['contentGenerator'] = mockGenerator;
            client['startChat'] = vi.fn().mockResolvedValue(mockChat);
            const result = await client.tryCompressChat('prompt-id-4', true);
            expect(mockCountTokens).toHaveBeenCalledTimes(2);
            expect(mockCountTokens).toHaveBeenNthCalledWith(1, {
                model: firstCurrentModel,
                contents: mockChatHistory,
            });
            expect(mockCountTokens).toHaveBeenNthCalledWith(2, {
                model: secondCurrentModel,
                contents: expect.any(Array),
            });
            expect(result).toEqual({
                originalTokenCount: 100000,
                newTokenCount: 5000,
            });
        });
    });
    describe('handleFlashFallback', () => {
        it('should use current model from config when checking for fallback', async () => {
            const initialModel = client['config'].getModel();
            const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL;
            // mock config been changed
            const currentModel = initialModel + '-changed';
            const getModelSpy = vi.spyOn(client['config'], 'getModel');
            getModelSpy.mockReturnValue(currentModel);
            const mockFallbackHandler = vi.fn().mockResolvedValue(true);
            client['config'].flashFallbackHandler = mockFallbackHandler;
            client['config'].setModel = vi.fn();
            const result = await client['handleFlashFallback'](AuthType.LOGIN_WITH_GOOGLE);
            expect(result).toBe(fallbackModel);
            expect(mockFallbackHandler).toHaveBeenCalledWith(currentModel, fallbackModel, undefined);
        });
    });
});
//# sourceMappingURL=client.test.js.map