/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Storage } from '../config/storage.js';
import { addMemory, dismissInboxSkill, listInboxSkills, listMemoryFiles, moveInboxSkill, refreshMemory, showMemory, } from './memory.js';
import * as memoryDiscovery from '../utils/memoryDiscovery.js';
vi.mock('../utils/memoryDiscovery.js', () => ({
    refreshServerHierarchicalMemory: vi.fn(),
}));
vi.mock('../config/storage.js', () => ({
    Storage: {
        getUserSkillsDir: vi.fn(),
    },
}));
const mockRefresh = vi.mocked(memoryDiscovery.refreshServerHierarchicalMemory);
describe('memory commands', () => {
    let mockConfig;
    beforeEach(() => {
        mockConfig = {
            getUserMemory: vi.fn(),
            getGeminiMdFileCount: vi.fn(),
            getGeminiMdFilePaths: vi.fn(),
            isJitContextEnabled: vi.fn(),
            updateSystemInstructionIfInitialized: vi
                .fn()
                .mockResolvedValue(undefined),
        };
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });
    describe('showMemory', () => {
        it('should show memory content if it exists', () => {
            vi.mocked(mockConfig.getUserMemory).mockReturnValue('some memory content');
            vi.mocked(mockConfig.getGeminiMdFileCount).mockReturnValue(1);
            const result = showMemory(mockConfig);
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('info');
                expect(result.content).toContain('Current memory content from 1 file(s)');
                expect(result.content).toContain('some memory content');
            }
        });
        it('should show a message if memory is empty', () => {
            vi.mocked(mockConfig.getUserMemory).mockReturnValue('');
            vi.mocked(mockConfig.getGeminiMdFileCount).mockReturnValue(0);
            const result = showMemory(mockConfig);
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('info');
                expect(result.content).toBe('Memory is currently empty.');
            }
        });
    });
    describe('addMemory', () => {
        it('should return a tool action to save memory', () => {
            const result = addMemory('new memory');
            expect(result.type).toBe('tool');
            if (result.type === 'tool') {
                expect(result.toolName).toBe('save_memory');
                expect(result.toolArgs).toEqual({ fact: 'new memory' });
            }
        });
        it('should trim the arguments', () => {
            const result = addMemory('  new memory  ');
            expect(result.type).toBe('tool');
            if (result.type === 'tool') {
                expect(result.toolArgs).toEqual({ fact: 'new memory' });
            }
        });
        it('should return an error if args are empty', () => {
            const result = addMemory('');
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('error');
                expect(result.content).toBe('Usage: /memory add <text to remember>');
            }
        });
        it('should return an error if args are just whitespace', () => {
            const result = addMemory('   ');
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('error');
                expect(result.content).toBe('Usage: /memory add <text to remember>');
            }
        });
        it('should return an error if args are undefined', () => {
            const result = addMemory(undefined);
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('error');
                expect(result.content).toBe('Usage: /memory add <text to remember>');
            }
        });
    });
    describe('refreshMemory', () => {
        it('should refresh memory and show success message', async () => {
            mockRefresh.mockResolvedValue({
                memoryContent: { project: 'refreshed content' },
                fileCount: 2,
                filePaths: [],
            });
            const result = await refreshMemory(mockConfig);
            expect(mockRefresh).toHaveBeenCalledWith(mockConfig);
            expect(mockConfig.updateSystemInstructionIfInitialized).toHaveBeenCalled();
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('info');
                expect(result.content).toBe('Memory reloaded successfully. Loaded 33 characters from 2 file(s)');
            }
        });
        it('should show a message if no memory content is found after refresh', async () => {
            mockRefresh.mockResolvedValue({
                memoryContent: { project: '' },
                fileCount: 0,
                filePaths: [],
            });
            const result = await refreshMemory(mockConfig);
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('info');
                expect(result.content).toBe('Memory reloaded successfully. No memory content found');
            }
        });
    });
    describe('listMemoryFiles', () => {
        it('should list the memory files in use', () => {
            const filePaths = ['/path/to/GEMINI.md', '/other/path/GEMINI.md'];
            vi.mocked(mockConfig.getGeminiMdFilePaths).mockReturnValue(filePaths);
            const result = listMemoryFiles(mockConfig);
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('info');
                expect(result.content).toContain('There are 2 GEMINI.md file(s) in use:');
                expect(result.content).toContain(filePaths.join('\n'));
            }
        });
        it('should show a message if no memory files are in use', () => {
            vi.mocked(mockConfig.getGeminiMdFilePaths).mockReturnValue([]);
            const result = listMemoryFiles(mockConfig);
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('info');
                expect(result.content).toBe('No GEMINI.md files in use.');
            }
        });
        it('should show a message if file paths are undefined', () => {
            vi.mocked(mockConfig.getGeminiMdFilePaths).mockReturnValue(undefined);
            const result = listMemoryFiles(mockConfig);
            expect(result.type).toBe('message');
            if (result.type === 'message') {
                expect(result.messageType).toBe('info');
                expect(result.content).toBe('No GEMINI.md files in use.');
            }
        });
    });
    describe('listInboxSkills', () => {
        let tmpDir;
        let skillsDir;
        let memoryTempDir;
        let inboxConfig;
        async function writeSkillMd(dirName, name, description) {
            const dir = path.join(skillsDir, dirName);
            await fs.mkdir(dir, { recursive: true });
            await fs.writeFile(path.join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${description}\n---\nBody content here\n`);
        }
        beforeEach(async () => {
            tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'inbox-test-'));
            skillsDir = path.join(tmpDir, 'skills-memory');
            memoryTempDir = path.join(tmpDir, 'memory-temp');
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(memoryTempDir, { recursive: true });
            inboxConfig = {
                storage: {
                    getProjectSkillsMemoryDir: () => skillsDir,
                    getProjectMemoryTempDir: () => memoryTempDir,
                    getProjectSkillsDir: () => path.join(tmpDir, 'project-skills'),
                },
            };
        });
        afterEach(async () => {
            await fs.rm(tmpDir, { recursive: true, force: true });
        });
        it('should return inbox skills with name, description, and extractedAt', async () => {
            await writeSkillMd('my-skill', 'my-skill', 'A test skill');
            await writeSkillMd('other-skill', 'other-skill', 'Another skill');
            const stateContent = JSON.stringify({
                runs: [
                    {
                        runAt: '2025-01-15T10:00:00Z',
                        sessionIds: ['sess-1'],
                        skillsCreated: ['my-skill'],
                    },
                    {
                        runAt: '2025-01-16T12:00:00Z',
                        sessionIds: ['sess-2'],
                        skillsCreated: ['other-skill'],
                    },
                ],
            });
            await fs.writeFile(path.join(memoryTempDir, '.extraction-state.json'), stateContent);
            const skills = await listInboxSkills(inboxConfig);
            expect(skills).toHaveLength(2);
            const mySkill = skills.find((s) => s.dirName === 'my-skill');
            expect(mySkill).toBeDefined();
            expect(mySkill.name).toBe('my-skill');
            expect(mySkill.description).toBe('A test skill');
            expect(mySkill.extractedAt).toBe('2025-01-15T10:00:00Z');
            const otherSkill = skills.find((s) => s.dirName === 'other-skill');
            expect(otherSkill).toBeDefined();
            expect(otherSkill.name).toBe('other-skill');
            expect(otherSkill.description).toBe('Another skill');
            expect(otherSkill.extractedAt).toBe('2025-01-16T12:00:00Z');
        });
        it('should return an empty array when the inbox is empty', async () => {
            const skills = await listInboxSkills(inboxConfig);
            expect(skills).toEqual([]);
        });
        it('should return an empty array when the inbox directory does not exist', async () => {
            const missingConfig = {
                storage: {
                    getProjectSkillsMemoryDir: () => path.join(tmpDir, 'nonexistent-dir'),
                    getProjectMemoryTempDir: () => memoryTempDir,
                },
            };
            const skills = await listInboxSkills(missingConfig);
            expect(skills).toEqual([]);
        });
    });
    describe('moveInboxSkill', () => {
        let tmpDir;
        let skillsDir;
        let globalSkillsDir;
        let projectSkillsDir;
        let moveConfig;
        async function writeSkillMd(dirName, name, description) {
            const dir = path.join(skillsDir, dirName);
            await fs.mkdir(dir, { recursive: true });
            await fs.writeFile(path.join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${description}\n---\nBody content here\n`);
        }
        beforeEach(async () => {
            tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'move-test-'));
            skillsDir = path.join(tmpDir, 'skills-memory');
            globalSkillsDir = path.join(tmpDir, 'global-skills');
            projectSkillsDir = path.join(tmpDir, 'project-skills');
            await fs.mkdir(skillsDir, { recursive: true });
            moveConfig = {
                storage: {
                    getProjectSkillsMemoryDir: () => skillsDir,
                    getProjectSkillsDir: () => projectSkillsDir,
                },
            };
            vi.mocked(Storage.getUserSkillsDir).mockReturnValue(globalSkillsDir);
        });
        afterEach(async () => {
            await fs.rm(tmpDir, { recursive: true, force: true });
        });
        it('should move a skill to global skills directory', async () => {
            await writeSkillMd('my-skill', 'my-skill', 'A test skill');
            const result = await moveInboxSkill(moveConfig, 'my-skill', 'global');
            expect(result.success).toBe(true);
            expect(result.message).toBe('Moved "my-skill" to ~/.gemini/skills.');
            // Verify the skill was copied to global
            const targetSkill = await fs.readFile(path.join(globalSkillsDir, 'my-skill', 'SKILL.md'), 'utf-8');
            expect(targetSkill).toContain('name: my-skill');
            // Verify the skill was removed from inbox
            await expect(fs.access(path.join(skillsDir, 'my-skill'))).rejects.toThrow();
        });
        it('should move a skill to project skills directory', async () => {
            await writeSkillMd('my-skill', 'my-skill', 'A test skill');
            const result = await moveInboxSkill(moveConfig, 'my-skill', 'project');
            expect(result.success).toBe(true);
            expect(result.message).toBe('Moved "my-skill" to .gemini/skills.');
            // Verify the skill was copied to project
            const targetSkill = await fs.readFile(path.join(projectSkillsDir, 'my-skill', 'SKILL.md'), 'utf-8');
            expect(targetSkill).toContain('name: my-skill');
            // Verify the skill was removed from inbox
            await expect(fs.access(path.join(skillsDir, 'my-skill'))).rejects.toThrow();
        });
        it('should return an error when the source skill does not exist', async () => {
            const result = await moveInboxSkill(moveConfig, 'nonexistent', 'global');
            expect(result.success).toBe(false);
            expect(result.message).toBe('Skill "nonexistent" not found in inbox.');
        });
        it('should reject invalid skill directory names', async () => {
            const result = await moveInboxSkill(moveConfig, '../escape', 'global');
            expect(result.success).toBe(false);
            expect(result.message).toBe('Invalid skill name.');
        });
        it('should return an error when the target already exists', async () => {
            await writeSkillMd('my-skill', 'my-skill', 'A test skill');
            // Pre-create the target
            const targetDir = path.join(globalSkillsDir, 'my-skill');
            await fs.mkdir(targetDir, { recursive: true });
            await fs.writeFile(path.join(targetDir, 'SKILL.md'), 'existing content');
            const result = await moveInboxSkill(moveConfig, 'my-skill', 'global');
            expect(result.success).toBe(false);
            expect(result.message).toBe('A skill named "my-skill" already exists in global skills.');
        });
        it('should detect conflicts based on the normalized skill name', async () => {
            await writeSkillMd('inbox-skill', 'gke:prs-troubleshooter', 'A test skill');
            await fs.mkdir(path.join(globalSkillsDir, 'existing-gke-prs-troubleshooter'), { recursive: true });
            await fs.writeFile(path.join(globalSkillsDir, 'existing-gke-prs-troubleshooter', 'SKILL.md'), [
                '---',
                'name: gke-prs-troubleshooter',
                'description: Existing skill',
                '---',
                'Existing body content',
                '',
            ].join('\n'));
            const result = await moveInboxSkill(moveConfig, 'inbox-skill', 'global');
            expect(result.success).toBe(false);
            expect(result.message).toBe('A skill named "gke-prs-troubleshooter" already exists in global skills.');
            await expect(fs.access(path.join(skillsDir, 'inbox-skill', 'SKILL.md'))).resolves.toBeUndefined();
            await expect(fs.access(path.join(globalSkillsDir, 'inbox-skill'))).rejects.toThrow();
        });
    });
    describe('dismissInboxSkill', () => {
        let tmpDir;
        let skillsDir;
        let dismissConfig;
        async function writeSkillMd(dirName, name, description) {
            const dir = path.join(skillsDir, dirName);
            await fs.mkdir(dir, { recursive: true });
            await fs.writeFile(path.join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${description}\n---\nBody content here\n`);
        }
        beforeEach(async () => {
            tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dismiss-test-'));
            skillsDir = path.join(tmpDir, 'skills-memory');
            await fs.mkdir(skillsDir, { recursive: true });
            dismissConfig = {
                storage: {
                    getProjectSkillsMemoryDir: () => skillsDir,
                },
            };
        });
        afterEach(async () => {
            await fs.rm(tmpDir, { recursive: true, force: true });
        });
        it('should remove a skill from the inbox', async () => {
            await writeSkillMd('my-skill', 'my-skill', 'A test skill');
            const result = await dismissInboxSkill(dismissConfig, 'my-skill');
            expect(result.success).toBe(true);
            expect(result.message).toBe('Dismissed "my-skill" from inbox.');
            // Verify the skill directory was removed
            await expect(fs.access(path.join(skillsDir, 'my-skill'))).rejects.toThrow();
        });
        it('should return an error when the skill does not exist', async () => {
            const result = await dismissInboxSkill(dismissConfig, 'nonexistent');
            expect(result.success).toBe(false);
            expect(result.message).toBe('Skill "nonexistent" not found in inbox.');
        });
        it('should reject invalid skill directory names', async () => {
            const result = await dismissInboxSkill(dismissConfig, 'nested\\skill');
            expect(result.success).toBe(false);
            expect(result.message).toBe('Invalid skill name.');
        });
    });
});
//# sourceMappingURL=memory.test.js.map