/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { FatalConfigError, ideContextStore, coreEvents, } from '@google/gemini-cli-core';
import { loadTrustedFolders, TrustLevel, isWorkspaceTrusted, resetTrustedFoldersForTesting, } from './trustedFolders.js';
import { loadEnvironment } from './settings.js';
import { createMockSettings } from '../test-utils/settings.js';
// We explicitly do NOT mock 'fs' or 'proper-lockfile' here to ensure
// we are testing the actual behavior on the real file system.
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        homedir: () => '/mock/home/user',
        isHeadlessMode: vi.fn(() => false),
        coreEvents: {
            emitFeedback: vi.fn(),
        },
    };
});
describe('Trusted Folders', () => {
    let tempDir;
    let trustedFoldersPath;
    beforeEach(() => {
        // Create a temporary directory for each test
        tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-'));
        trustedFoldersPath = path.join(tempDir, 'trustedFolders.json');
        // Set the environment variable to point to the temp file
        vi.stubEnv('GEMINI_CLI_TRUSTED_FOLDERS_PATH', trustedFoldersPath);
        // Reset the internal state
        resetTrustedFoldersForTesting();
        vi.clearAllMocks();
    });
    afterEach(() => {
        // Clean up the temporary directory
        fs.rmSync(tempDir, { recursive: true, force: true });
        vi.unstubAllEnvs();
    });
    describe('Locking & Concurrency', () => {
        it('setValue should handle concurrent calls correctly using real lockfile', async () => {
            // Initialize the file
            fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
            const loadedFolders = loadTrustedFolders();
            // Start two concurrent calls
            // These will race to acquire the lock on the real file system
            const p1 = loadedFolders.setValue('/path1', TrustLevel.TRUST_FOLDER);
            const p2 = loadedFolders.setValue('/path2', TrustLevel.TRUST_FOLDER);
            await Promise.all([p1, p2]);
            // Verify final state in the file
            const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
            const config = JSON.parse(content);
            expect(config).toEqual({
                '/path1': TrustLevel.TRUST_FOLDER,
                '/path2': TrustLevel.TRUST_FOLDER,
            });
        });
    });
    describe('Loading & Parsing', () => {
        it('should load empty rules if no files exist', () => {
            const { rules, errors } = loadTrustedFolders();
            expect(rules).toEqual([]);
            expect(errors).toEqual([]);
        });
        it('should load rules from the configuration file', () => {
            const config = {
                '/user/folder': TrustLevel.TRUST_FOLDER,
            };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            const { rules, errors } = loadTrustedFolders();
            expect(rules).toEqual([
                { path: '/user/folder', trustLevel: TrustLevel.TRUST_FOLDER },
            ]);
            expect(errors).toEqual([]);
        });
        it('should handle JSON parsing errors gracefully', () => {
            fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
            const { rules, errors } = loadTrustedFolders();
            expect(rules).toEqual([]);
            expect(errors.length).toBe(1);
            expect(errors[0].path).toBe(trustedFoldersPath);
            expect(errors[0].message).toContain('Unexpected token');
        });
        it('should handle non-object JSON gracefully', () => {
            fs.writeFileSync(trustedFoldersPath, 'null', 'utf-8');
            const { rules, errors } = loadTrustedFolders();
            expect(rules).toEqual([]);
            expect(errors.length).toBe(1);
            expect(errors[0].message).toContain('not a valid JSON object');
        });
        it('should handle invalid trust levels gracefully', () => {
            const config = {
                '/path': 'INVALID_LEVEL',
            };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            const { rules, errors } = loadTrustedFolders();
            expect(rules).toEqual([]);
            expect(errors.length).toBe(1);
            expect(errors[0].message).toContain('Invalid trust level "INVALID_LEVEL"');
        });
        it('should support JSON with comments', () => {
            const content = `
        {
          // This is a comment
          "/path": "TRUST_FOLDER"
        }
      `;
            fs.writeFileSync(trustedFoldersPath, content, 'utf-8');
            const { rules, errors } = loadTrustedFolders();
            expect(rules).toEqual([
                { path: '/path', trustLevel: TrustLevel.TRUST_FOLDER },
            ]);
            expect(errors).toEqual([]);
        });
    });
    describe('isPathTrusted', () => {
        function setup(config) {
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            return loadTrustedFolders();
        }
        it('provides a method to determine if a path is trusted', () => {
            const folders = setup({
                './myfolder': TrustLevel.TRUST_FOLDER,
                '/trustedparent/trustme': TrustLevel.TRUST_PARENT,
                '/user/folder': TrustLevel.TRUST_FOLDER,
                '/secret': TrustLevel.DO_NOT_TRUST,
                '/secret/publickeys': TrustLevel.TRUST_FOLDER,
            });
            // We need to resolve relative paths for comparison since the implementation uses realpath
            const resolvedMyFolder = path.resolve('./myfolder');
            expect(folders.isPathTrusted('/secret')).toBe(false);
            expect(folders.isPathTrusted('/user/folder')).toBe(true);
            expect(folders.isPathTrusted('/secret/publickeys/public.pem')).toBe(true);
            expect(folders.isPathTrusted('/user/folder/harhar')).toBe(true);
            expect(folders.isPathTrusted(path.join(resolvedMyFolder, 'somefile.jpg'))).toBe(true);
            expect(folders.isPathTrusted('/trustedparent/someotherfolder')).toBe(true);
            expect(folders.isPathTrusted('/trustedparent/trustme')).toBe(true);
            // No explicit rule covers this file
            expect(folders.isPathTrusted('/secret/bankaccounts.json')).toBe(false);
            expect(folders.isPathTrusted('/secret/mine/privatekey.pem')).toBe(false);
            expect(folders.isPathTrusted('/user/someotherfolder')).toBe(undefined);
        });
        it('prioritizes the longest matching path (precedence)', () => {
            const folders = setup({
                '/a': TrustLevel.TRUST_FOLDER,
                '/a/b': TrustLevel.DO_NOT_TRUST,
                '/a/b/c': TrustLevel.TRUST_FOLDER,
                '/parent/trustme': TrustLevel.TRUST_PARENT,
                '/parent/trustme/butnotthis': TrustLevel.DO_NOT_TRUST,
            });
            expect(folders.isPathTrusted('/a/b/c/d')).toBe(true);
            expect(folders.isPathTrusted('/a/b/x')).toBe(false);
            expect(folders.isPathTrusted('/a/x')).toBe(true);
            expect(folders.isPathTrusted('/parent/trustme/butnotthis/file')).toBe(false);
            expect(folders.isPathTrusted('/parent/other')).toBe(true);
        });
    });
    describe('setValue', () => {
        it('should update the user config and save it atomically', async () => {
            fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
            const loadedFolders = loadTrustedFolders();
            await loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
            expect(loadedFolders.user.config['/new/path']).toBe(TrustLevel.TRUST_FOLDER);
            const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
            const config = JSON.parse(content);
            expect(config['/new/path']).toBe(TrustLevel.TRUST_FOLDER);
        });
        it('should throw FatalConfigError if there were load errors', async () => {
            fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
            const loadedFolders = loadTrustedFolders();
            expect(loadedFolders.errors.length).toBe(1);
            await expect(loadedFolders.setValue('/some/path', TrustLevel.TRUST_FOLDER)).rejects.toThrow(FatalConfigError);
        });
        it('should report corrupted config via coreEvents.emitFeedback and still succeed', async () => {
            // Initialize with valid JSON
            fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
            const loadedFolders = loadTrustedFolders();
            // Corrupt the file after initial load
            fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
            await loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
            expect(coreEvents.emitFeedback).toHaveBeenCalledWith('error', expect.stringContaining('may be corrupted'), expect.any(Error));
            // Should have overwritten the corrupted file with new valid config
            const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
            const config = JSON.parse(content);
            expect(config).toEqual({ '/new/path': TrustLevel.TRUST_FOLDER });
        });
    });
    describe('isWorkspaceTrusted Integration', () => {
        const mockSettings = {
            security: {
                folderTrust: {
                    enabled: true,
                },
            },
        };
        it('should return true for a directly trusted folder', () => {
            const config = { '/projectA': TrustLevel.TRUST_FOLDER };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
                isTrusted: true,
                source: 'file',
            });
        });
        it('should return true for a child of a trusted folder', () => {
            const config = { '/projectA': TrustLevel.TRUST_FOLDER };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/projectA/src')).toEqual({
                isTrusted: true,
                source: 'file',
            });
        });
        it('should return true for a child of a trusted parent folder', () => {
            const config = { '/projectB/somefile.txt': TrustLevel.TRUST_PARENT };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/projectB')).toEqual({
                isTrusted: true,
                source: 'file',
            });
        });
        it('should return false for a directly untrusted folder', () => {
            const config = { '/untrusted': TrustLevel.DO_NOT_TRUST };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/untrusted')).toEqual({
                isTrusted: false,
                source: 'file',
            });
        });
        it('should return false for a child of an untrusted folder', () => {
            const config = { '/untrusted': TrustLevel.DO_NOT_TRUST };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/untrusted/src').isTrusted).toBe(false);
        });
        it('should return undefined when no rules match', () => {
            fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/other').isTrusted).toBeUndefined();
        });
        it('should prioritize specific distrust over parent trust', () => {
            const config = {
                '/projectA': TrustLevel.TRUST_FOLDER,
                '/projectA/untrusted': TrustLevel.DO_NOT_TRUST,
            };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/projectA/untrusted')).toEqual({
                isTrusted: false,
                source: 'file',
            });
        });
        it('should use workspaceDir instead of process.cwd() when provided', () => {
            const config = {
                '/projectA': TrustLevel.TRUST_FOLDER,
                '/untrusted': TrustLevel.DO_NOT_TRUST,
            };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            vi.spyOn(process, 'cwd').mockImplementation(() => '/untrusted');
            // process.cwd() is untrusted, but workspaceDir is trusted
            expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
                isTrusted: true,
                source: 'file',
            });
        });
        it('should handle path normalization', () => {
            const config = { '/home/user/projectA': TrustLevel.TRUST_FOLDER };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/home/user/../user/projectA')).toEqual({
                isTrusted: true,
                source: 'file',
            });
        });
        it('should prioritize IDE override over file config', () => {
            const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            ideContextStore.set({ workspaceState: { isTrusted: true } });
            try {
                expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
                    isTrusted: true,
                    source: 'ide',
                });
            }
            finally {
                ideContextStore.clear();
            }
        });
        it('should return false when IDE override is false', () => {
            const config = { '/projectA': TrustLevel.TRUST_FOLDER };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            ideContextStore.set({ workspaceState: { isTrusted: false } });
            try {
                expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
                    isTrusted: false,
                    source: 'ide',
                });
            }
            finally {
                ideContextStore.clear();
            }
        });
        it('should throw FatalConfigError when the config file is invalid', () => {
            fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
            expect(() => isWorkspaceTrusted(mockSettings, '/any')).toThrow(FatalConfigError);
        });
        it('should always return true if folderTrust setting is disabled', () => {
            const disabledSettings = {
                security: { folderTrust: { enabled: false } },
            };
            expect(isWorkspaceTrusted(disabledSettings, '/any')).toEqual({
                isTrusted: true,
                source: undefined,
            });
        });
    });
    describe('isWorkspaceTrusted headless mode', () => {
        const mockSettings = {
            security: {
                folderTrust: {
                    enabled: true,
                },
            },
        };
        it('should return true when isHeadlessMode is true, ignoring config', async () => {
            const geminiCore = await import('@google/gemini-cli-core');
            vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
            expect(isWorkspaceTrusted(mockSettings)).toEqual({
                isTrusted: true,
                source: undefined,
            });
        });
        it('should fall back to config when isHeadlessMode is false', async () => {
            const geminiCore = await import('@google/gemini-cli-core');
            vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(false);
            const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            expect(isWorkspaceTrusted(mockSettings, '/projectA').isTrusted).toBe(false);
        });
    });
    describe('Trusted Folders Caching', () => {
        it('should cache the loaded folders object', () => {
            // First call should load and cache
            const folders1 = loadTrustedFolders();
            // Second call should return the same instance from cache
            const folders2 = loadTrustedFolders();
            expect(folders1).toBe(folders2);
            // Resetting should clear the cache
            resetTrustedFoldersForTesting();
            // Third call should return a new instance
            const folders3 = loadTrustedFolders();
            expect(folders3).not.toBe(folders1);
        });
    });
    describe('invalid trust levels', () => {
        it('should create a comprehensive error message for invalid trust level', () => {
            const config = { '/user/folder': 'INVALID_TRUST_LEVEL' };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            const { errors } = loadTrustedFolders();
            const possibleValues = Object.values(TrustLevel).join(', ');
            expect(errors.length).toBe(1);
            expect(errors[0].message).toBe(`Invalid trust level "INVALID_TRUST_LEVEL" for path "/user/folder". Possible values are: ${possibleValues}.`);
        });
    });
    describe('Symlinks Support', () => {
        const mockSettings = {
            security: { folderTrust: { enabled: true } },
        };
        it('should trust a folder if the rule matches the realpath', () => {
            // Create a real directory and a symlink
            const realDir = path.join(tempDir, 'real');
            const symlinkDir = path.join(tempDir, 'symlink');
            fs.mkdirSync(realDir);
            fs.symlinkSync(realDir, symlinkDir);
            // Rule uses realpath
            const config = { [realDir]: TrustLevel.TRUST_FOLDER };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            // Check against symlink path
            expect(isWorkspaceTrusted(mockSettings, symlinkDir).isTrusted).toBe(true);
        });
    });
    describe('Verification: Auth and Trust Interaction', () => {
        it('should verify loadEnvironment returns early when untrusted', () => {
            const untrustedDir = path.join(tempDir, 'untrusted');
            fs.mkdirSync(untrustedDir);
            const config = { [untrustedDir]: TrustLevel.DO_NOT_TRUST };
            fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
            const envPath = path.join(untrustedDir, '.env');
            fs.writeFileSync(envPath, 'GEMINI_API_KEY=secret', 'utf-8');
            vi.stubEnv('GEMINI_API_KEY', '');
            const settings = createMockSettings({
                security: { folderTrust: { enabled: true } },
            });
            loadEnvironment(settings.merged, untrustedDir);
            expect(process.env['GEMINI_API_KEY']).toBe('');
            vi.unstubAllEnvs();
        });
    });
});
//# sourceMappingURL=trustedFolders.test.js.map