/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { AuthType } from '@google/gemini-cli-core';
import * as fsPromises from 'node:fs/promises';
import { chatCommand, debugCommand } from './chatCommand.js';
import { serializeHistoryToMarkdown, exportHistoryToFile, } from '../utils/historyExportUtils.js';
import path from 'node:path';
vi.mock('fs/promises', () => ({
    stat: vi.fn(),
    readdir: vi.fn().mockResolvedValue(['file1.txt', 'file2.txt']),
    writeFile: vi.fn(),
}));
vi.mock('../utils/historyExportUtils.js', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        exportHistoryToFile: vi.fn(),
    };
});
describe('chatCommand', () => {
    const mockFs = vi.mocked(fsPromises);
    const mockExport = vi.mocked(exportHistoryToFile);
    let mockContext;
    let mockGetChat;
    let mockSaveCheckpoint;
    let mockLoadCheckpoint;
    let mockDeleteCheckpoint;
    let mockGetHistory;
    const getSubCommand = (name) => {
        const subCommand = chatCommand.subCommands?.find((cmd) => cmd.name === name);
        if (!subCommand) {
            throw new Error(`/chat ${name} command not found.`);
        }
        return subCommand;
    };
    beforeEach(() => {
        mockGetHistory = vi.fn().mockReturnValue([]);
        mockGetChat = vi.fn().mockReturnValue({
            getHistory: mockGetHistory,
        });
        mockSaveCheckpoint = vi.fn().mockResolvedValue(undefined);
        mockLoadCheckpoint = vi.fn().mockResolvedValue({ history: [] });
        mockDeleteCheckpoint = vi.fn().mockResolvedValue(true);
        mockContext = createMockCommandContext({
            services: {
                config: {
                    getProjectRoot: () => '/project/root',
                    getGeminiClient: () => ({
                        getChat: mockGetChat,
                    }),
                    storage: {
                        getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
                    },
                    getContentGeneratorConfig: () => ({
                        authType: AuthType.LOGIN_WITH_GOOGLE,
                    }),
                },
                logger: {
                    saveCheckpoint: mockSaveCheckpoint,
                    loadCheckpoint: mockLoadCheckpoint,
                    deleteCheckpoint: mockDeleteCheckpoint,
                    initialize: vi.fn().mockResolvedValue(undefined),
                },
            },
        });
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });
    it('should have the correct main command definition', () => {
        expect(chatCommand.name).toBe('chat');
        expect(chatCommand.description).toBe('Manage conversation history');
        expect(chatCommand.subCommands).toHaveLength(5);
    });
    describe('list subcommand', () => {
        let listCommand;
        beforeEach(() => {
            listCommand = getSubCommand('list');
        });
        it('should add a chat_list item to the UI', async () => {
            const fakeFiles = ['checkpoint-test1.json', 'checkpoint-test2.json'];
            const date1 = new Date();
            const date2 = new Date(date1.getTime() + 1000);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            mockFs.readdir.mockResolvedValue(fakeFiles);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            mockFs.stat.mockImplementation(async (path) => {
                if (path.endsWith('test1.json')) {
                    return { mtime: date1 };
                }
                return { mtime: date2 };
            });
            await listCommand?.action?.(mockContext, '');
            expect(mockContext.ui.addItem).toHaveBeenCalledWith({
                type: 'chat_list',
                chats: [
                    {
                        name: 'test1',
                        mtime: date1.toISOString(),
                    },
                    {
                        name: 'test2',
                        mtime: date2.toISOString(),
                    },
                ],
            });
        });
    });
    describe('save subcommand', () => {
        let saveCommand;
        const tag = 'my-tag';
        let mockCheckpointExists;
        beforeEach(() => {
            saveCommand = getSubCommand('save');
            mockCheckpointExists = vi.fn().mockResolvedValue(false);
            mockContext.services.logger.checkpointExists = mockCheckpointExists;
        });
        it('should return an error if tag is missing', async () => {
            const result = await saveCommand?.action?.(mockContext, '  ');
            expect(result).toEqual({
                type: 'message',
                messageType: 'error',
                content: 'Missing tag. Usage: /chat save <tag>',
            });
        });
        it('should inform if conversation history is empty or only contains system context', async () => {
            mockGetHistory.mockReturnValue([]);
            let result = await saveCommand?.action?.(mockContext, tag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: 'No conversation found to save.',
            });
            mockGetHistory.mockReturnValue([
                { role: 'user', parts: [{ text: 'context for our chat' }] },
            ]);
            result = await saveCommand?.action?.(mockContext, tag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: 'No conversation found to save.',
            });
            mockGetHistory.mockReturnValue([
                { role: 'user', parts: [{ text: 'context for our chat' }] },
                { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
                { role: 'user', parts: [{ text: 'Hello, how are you?' }] },
            ]);
            result = await saveCommand?.action?.(mockContext, tag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: `Conversation checkpoint saved with tag: ${tag}.`,
            });
        });
        it('should return confirm_action if checkpoint already exists', async () => {
            mockCheckpointExists.mockResolvedValue(true);
            mockContext.invocation = {
                raw: `/chat save ${tag}`,
                name: 'save',
                args: tag,
            };
            const result = await saveCommand?.action?.(mockContext, tag);
            expect(mockCheckpointExists).toHaveBeenCalledWith(tag);
            expect(mockSaveCheckpoint).not.toHaveBeenCalled();
            expect(result).toMatchObject({
                type: 'confirm_action',
                originalInvocation: { raw: `/chat save ${tag}` },
            });
            // Check that prompt is a React element
            expect(result).toHaveProperty('prompt');
        });
        it('should save the conversation if overwrite is confirmed', async () => {
            const history = [
                { role: 'user', parts: [{ text: 'context for our chat' }] },
                { role: 'user', parts: [{ text: 'hello' }] },
            ];
            mockGetHistory.mockReturnValue(history);
            mockContext.overwriteConfirmed = true;
            const result = await saveCommand?.action?.(mockContext, tag);
            expect(mockCheckpointExists).not.toHaveBeenCalled(); // Should skip existence check
            expect(mockSaveCheckpoint).toHaveBeenCalledWith({ history, authType: AuthType.LOGIN_WITH_GOOGLE }, tag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: `Conversation checkpoint saved with tag: ${tag}.`,
            });
        });
    });
    describe('resume subcommand', () => {
        const goodTag = 'good-tag';
        const badTag = 'bad-tag';
        let resumeCommand;
        beforeEach(() => {
            resumeCommand = getSubCommand('resume');
        });
        it('should return an error if tag is missing', async () => {
            const result = await resumeCommand?.action?.(mockContext, '');
            expect(result).toEqual({
                type: 'message',
                messageType: 'error',
                content: 'Missing tag. Usage: /chat resume <tag>',
            });
        });
        it('should inform if checkpoint is not found', async () => {
            mockLoadCheckpoint.mockResolvedValue({ history: [] });
            const result = await resumeCommand?.action?.(mockContext, badTag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: `No saved checkpoint found with tag: ${badTag}.`,
            });
        });
        it('should resume a conversation with matching authType', async () => {
            const conversation = [
                { role: 'user', parts: [{ text: 'system setup' }] },
                { role: 'user', parts: [{ text: 'hello gemini' }] },
                { role: 'model', parts: [{ text: 'hello world' }] },
            ];
            mockLoadCheckpoint.mockResolvedValue({
                history: conversation,
                authType: AuthType.LOGIN_WITH_GOOGLE,
            });
            const result = await resumeCommand?.action?.(mockContext, goodTag);
            expect(result).toEqual({
                type: 'load_history',
                history: [
                    { type: 'user', text: 'hello gemini' },
                    { type: 'gemini', text: 'hello world' },
                ],
                clientHistory: conversation,
            });
        });
        it('should block resuming a conversation with mismatched authType', async () => {
            const conversation = [
                { role: 'user', parts: [{ text: 'system setup' }] },
                { role: 'user', parts: [{ text: 'hello gemini' }] },
                { role: 'model', parts: [{ text: 'hello world' }] },
            ];
            mockLoadCheckpoint.mockResolvedValue({
                history: conversation,
                authType: AuthType.USE_GEMINI,
            });
            const result = await resumeCommand?.action?.(mockContext, goodTag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'error',
                content: `Cannot resume chat. It was saved with a different authentication method (${AuthType.USE_GEMINI}) than the current one (${AuthType.LOGIN_WITH_GOOGLE}).`,
            });
        });
        it('should resume a legacy conversation without authType', async () => {
            const conversation = [
                { role: 'user', parts: [{ text: 'system setup' }] },
                { role: 'user', parts: [{ text: 'hello gemini' }] },
                { role: 'model', parts: [{ text: 'hello world' }] },
            ];
            mockLoadCheckpoint.mockResolvedValue({ history: conversation });
            const result = await resumeCommand?.action?.(mockContext, goodTag);
            expect(result).toEqual({
                type: 'load_history',
                history: [
                    { type: 'user', text: 'hello gemini' },
                    { type: 'gemini', text: 'hello world' },
                ],
                clientHistory: conversation,
            });
        });
        describe('completion', () => {
            it('should provide completion suggestions', async () => {
                const fakeFiles = ['checkpoint-alpha.json', 'checkpoint-beta.json'];
                mockFs.readdir.mockImplementation((async (_) => fakeFiles));
                mockFs.stat.mockImplementation((async (_) => ({
                    mtime: new Date(),
                })));
                const result = await resumeCommand?.completion?.(mockContext, 'a');
                expect(result).toEqual(['alpha']);
            });
            it('should suggest filenames sorted by modified time (newest first)', async () => {
                const fakeFiles = ['checkpoint-test1.json', 'checkpoint-test2.json'];
                const date = new Date();
                mockFs.readdir.mockImplementation((async (_) => fakeFiles));
                mockFs.stat.mockImplementation((async (path) => {
                    if (path.endsWith('test1.json')) {
                        return { mtime: date };
                    }
                    return { mtime: new Date(date.getTime() + 1000) };
                }));
                const result = await resumeCommand?.completion?.(mockContext, '');
                // Sort items by last modified time (newest first)
                expect(result).toEqual(['test2', 'test1']);
            });
        });
    });
    describe('delete subcommand', () => {
        let deleteCommand;
        const tag = 'my-tag';
        beforeEach(() => {
            deleteCommand = getSubCommand('delete');
        });
        it('should return an error if tag is missing', async () => {
            const result = await deleteCommand?.action?.(mockContext, '  ');
            expect(result).toEqual({
                type: 'message',
                messageType: 'error',
                content: 'Missing tag. Usage: /chat delete <tag>',
            });
        });
        it('should return an error if checkpoint is not found', async () => {
            mockDeleteCheckpoint.mockResolvedValue(false);
            const result = await deleteCommand?.action?.(mockContext, tag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'error',
                content: `Error: No checkpoint found with tag '${tag}'.`,
            });
        });
        it('should delete the conversation', async () => {
            const result = await deleteCommand?.action?.(mockContext, tag);
            expect(mockDeleteCheckpoint).toHaveBeenCalledWith(tag);
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: `Conversation checkpoint '${tag}' has been deleted.`,
            });
        });
        describe('completion', () => {
            it('should provide completion suggestions', async () => {
                const fakeFiles = ['checkpoint-alpha.json', 'checkpoint-beta.json'];
                mockFs.readdir.mockImplementation((async (_) => fakeFiles));
                mockFs.stat.mockImplementation((async (_) => ({
                    mtime: new Date(),
                })));
                const result = await deleteCommand?.completion?.(mockContext, 'a');
                expect(result).toEqual(['alpha']);
            });
        });
    });
    describe('share subcommand', () => {
        let shareCommand;
        const mockHistory = [
            { role: 'user', parts: [{ text: 'context' }] },
            { role: 'model', parts: [{ text: 'context response' }] },
            { role: 'user', parts: [{ text: 'Hello' }] },
            { role: 'model', parts: [{ text: 'Hi there!' }] },
        ];
        beforeEach(() => {
            shareCommand = getSubCommand('share');
            vi.spyOn(process, 'cwd').mockReturnValue(path.resolve('/usr/local/google/home/myuser/gemini-cli'));
            vi.spyOn(Date, 'now').mockReturnValue(1234567890);
            mockGetHistory.mockReturnValue(mockHistory);
            mockFs.writeFile.mockClear();
        });
        it('should default to a json file if no path is provided', async () => {
            const result = await shareCommand?.action?.(mockContext, '');
            const expectedPath = path.join(process.cwd(), 'gemini-conversation-1234567890.json');
            expect(mockExport).toHaveBeenCalledWith({
                history: mockHistory,
                filePath: expectedPath,
            });
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: `Conversation shared to ${expectedPath}`,
            });
        });
        it('should share the conversation to a JSON file', async () => {
            const filePath = 'my-chat.json';
            const result = await shareCommand?.action?.(mockContext, filePath);
            const expectedPath = path.join(process.cwd(), 'my-chat.json');
            expect(mockExport).toHaveBeenCalledWith({
                history: mockHistory,
                filePath: expectedPath,
            });
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: `Conversation shared to ${expectedPath}`,
            });
        });
        it('should share the conversation to a Markdown file', async () => {
            const filePath = 'my-chat.md';
            const result = await shareCommand?.action?.(mockContext, filePath);
            const expectedPath = path.join(process.cwd(), 'my-chat.md');
            expect(mockExport).toHaveBeenCalledWith({
                history: mockHistory,
                filePath: expectedPath,
            });
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: `Conversation shared to ${expectedPath}`,
            });
        });
        it('should return an error for unsupported file extensions', async () => {
            const filePath = 'my-chat.txt';
            const result = await shareCommand?.action?.(mockContext, filePath);
            expect(mockExport).not.toHaveBeenCalled();
            expect(result).toEqual({
                type: 'message',
                messageType: 'error',
                content: 'Invalid file format. Only .md and .json are supported.',
            });
        });
        it('should inform if there is no conversation to share', async () => {
            mockGetHistory.mockReturnValue([
                { role: 'user', parts: [{ text: 'context' }] },
            ]);
            const result = await shareCommand?.action?.(mockContext, 'my-chat.json');
            expect(mockExport).not.toHaveBeenCalled();
            expect(result).toEqual({
                type: 'message',
                messageType: 'info',
                content: 'No conversation found to share.',
            });
        });
        it('should handle errors during file writing', async () => {
            const error = new Error('Permission denied');
            mockExport.mockRejectedValue(error);
            const result = await shareCommand?.action?.(mockContext, 'my-chat.json');
            expect(result).toEqual({
                type: 'message',
                messageType: 'error',
                content: `Error sharing conversation: ${error.message}`,
            });
        });
        it('should output valid JSON schema', async () => {
            const filePath = 'my-chat.json';
            await shareCommand?.action?.(mockContext, filePath);
            const expectedPath = path.join(process.cwd(), 'my-chat.json');
            expect(mockExport).toHaveBeenCalledWith({
                history: mockHistory,
                filePath: expectedPath,
            });
        });
        it('should output correct markdown format', async () => {
            const filePath = 'my-chat.md';
            await shareCommand?.action?.(mockContext, filePath);
            const expectedPath = path.join(process.cwd(), 'my-chat.md');
            expect(mockExport).toHaveBeenCalledWith({
                history: mockHistory,
                filePath: expectedPath,
            });
        });
    });
    describe('serializeHistoryToMarkdown', () => {
        it('should correctly serialize chat history to Markdown with icons', () => {
            const history = [
                { role: 'user', parts: [{ text: 'Hello' }] },
                { role: 'model', parts: [{ text: 'Hi there!' }] },
                { role: 'user', parts: [{ text: 'How are you?' }] },
            ];
            const expectedMarkdown = '## USER 🧑‍💻\n\nHello\n\n---\n\n' +
                '## MODEL ✨\n\nHi there!\n\n---\n\n' +
                '## USER 🧑‍💻\n\nHow are you?';
            const result = serializeHistoryToMarkdown(history);
            expect(result).toBe(expectedMarkdown);
        });
        it('should handle empty history', () => {
            const history = [];
            const result = serializeHistoryToMarkdown(history);
            expect(result).toBe('');
        });
        it('should handle items with no text parts', () => {
            const history = [
                { role: 'user', parts: [{ text: 'Hello' }] },
                { role: 'model', parts: [] },
                { role: 'user', parts: [{ text: 'How are you?' }] },
            ];
            const expectedMarkdown = `## USER 🧑‍💻

Hello

---

## MODEL ✨



---

## USER 🧑‍💻

How are you?`;
            const result = serializeHistoryToMarkdown(history);
            expect(result).toBe(expectedMarkdown);
        });
        it('should correctly serialize function calls and responses', () => {
            const history = [
                {
                    role: 'user',
                    parts: [{ text: 'Please call a function.' }],
                },
                {
                    role: 'model',
                    parts: [
                        {
                            functionCall: {
                                name: 'my-function',
                                args: { arg1: 'value1' },
                            },
                        },
                    ],
                },
                {
                    role: 'user',
                    parts: [
                        {
                            functionResponse: {
                                name: 'my-function',
                                response: { result: 'success' },
                            },
                        },
                    ],
                },
            ];
            const expectedMarkdown = `## USER 🧑‍💻

Please call a function.

---

## MODEL ✨

**Tool Command**:
\`\`\`json
{
  "name": "my-function",
  "args": {
    "arg1": "value1"
  }
}
\`\`\`

---

## USER 🧑‍💻

**Tool Response**:
\`\`\`json
{
  "name": "my-function",
  "response": {
    "result": "success"
  }
}
\`\`\``;
            const result = serializeHistoryToMarkdown(history);
            expect(result).toBe(expectedMarkdown);
        });
        it('should handle items with undefined role', () => {
            const history = [
                { role: 'user', parts: [{ text: 'Hello' }] },
                { parts: [{ text: 'Hi there!' }] },
            ];
            const expectedMarkdown = `## USER 🧑‍💻

Hello

---

## MODEL ✨

Hi there!`;
            const result = serializeHistoryToMarkdown(history);
            expect(result).toBe(expectedMarkdown);
        });
        describe('debug subcommand', () => {
            let mockGetLatestApiRequest;
            beforeEach(() => {
                mockGetLatestApiRequest = vi.fn();
                mockContext.services.config.getLatestApiRequest =
                    mockGetLatestApiRequest;
                vi.spyOn(process, 'cwd').mockReturnValue('/project/root');
                vi.spyOn(Date, 'now').mockReturnValue(1234567890);
                mockFs.writeFile.mockClear();
            });
            it('should return an error if no API request is found', async () => {
                mockGetLatestApiRequest.mockReturnValue(undefined);
                const result = await debugCommand.action?.(mockContext, '');
                expect(result).toEqual({
                    type: 'message',
                    messageType: 'error',
                    content: 'No recent API request found to export.',
                });
                expect(mockFs.writeFile).not.toHaveBeenCalled();
            });
            it('should convert and write the API request to a json file', async () => {
                const mockRequest = {
                    contents: [{ role: 'user', parts: [{ text: 'test' }] }],
                };
                mockGetLatestApiRequest.mockReturnValue(mockRequest);
                const result = await debugCommand.action?.(mockContext, '');
                const expectedFilename = 'gcli-request-1234567890.json';
                const expectedPath = path.join('/project/root', expectedFilename);
                expect(mockFs.writeFile).toHaveBeenCalledWith(expectedPath, expect.stringContaining('"role": "user"'));
                expect(result).toEqual({
                    type: 'message',
                    messageType: 'info',
                    content: `Debug API request saved to ${expectedFilename}`,
                });
            });
            it('should handle errors during file write', async () => {
                const mockRequest = { contents: [] };
                mockGetLatestApiRequest.mockReturnValue(mockRequest);
                mockFs.writeFile.mockRejectedValue(new Error('Write failed'));
                const result = await debugCommand.action?.(mockContext, '');
                expect(result).toEqual({
                    type: 'message',
                    messageType: 'error',
                    content: 'Error saving debug request: Write failed',
                });
            });
        });
    });
});
//# sourceMappingURL=chatCommand.test.js.map