/**
 * @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,
  normalizePath,
} from '@google/gemini-cli-core';
import {
  loadTrustedFolders,
  TrustLevel,
  isWorkspaceTrusted,
  resetTrustedFoldersForTesting,
} from './trustedFolders.js';
import { loadEnvironment, type Settings } 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<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    homedir: () => '/mock/home/user',
    isHeadlessMode: vi.fn(() => false),
    coreEvents: Object.assign(
      Object.create(Object.getPrototypeOf(actual.coreEvents)),
      actual.coreEvents,
      {
        emitFeedback: vi.fn(),
      },
    ),
    FatalConfigError: actual.FatalConfigError,
  };
});

describe('Trusted Folders', () => {
  let tempDir: string;
  let trustedFoldersPath: string;

  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();
    delete process.env['GEMINI_CLI_TRUST_WORKSPACE'];
  });

  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(
        path.resolve('/path1'),
        TrustLevel.TRUST_FOLDER,
      );
      const p2 = loadedFolders.setValue(
        path.resolve('/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({
        [normalizePath('/path1')]: TrustLevel.TRUST_FOLDER,
        [normalizePath('/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 = {
        [normalizePath('/user/folder')]: TrustLevel.TRUST_FOLDER,
      };
      fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');

      const { rules, errors } = loadTrustedFolders();
      expect(rules).toEqual([
        {
          path: normalizePath('/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
          "${normalizePath('/path').replaceAll('\\', '\\\\')}": "TRUST_FOLDER"
        }
      `;
      fs.writeFileSync(trustedFoldersPath, content, 'utf-8');

      const { rules, errors } = loadTrustedFolders();
      expect(rules).toEqual([
        { path: normalizePath('/path'), trustLevel: TrustLevel.TRUST_FOLDER },
      ]);
      expect(errors).toEqual([]);
    });
  });

  describe('isPathTrusted', () => {
    function setup(config: Record<string, TrustLevel>) {
      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(
        normalizePath('/new/path'),
        TrustLevel.TRUST_FOLDER,
      );

      expect(loadedFolders.user.config[normalizePath('/new/path')]).toBe(
        TrustLevel.TRUST_FOLDER,
      );

      const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
      const config = JSON.parse(content);
      expect(config[normalizePath('/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);
    });
  });

  describe('isWorkspaceTrusted Integration', () => {
    const mockSettings: Settings = {
      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: Settings = {
        security: { folderTrust: { enabled: false } },
      };
      expect(isWorkspaceTrusted(disabledSettings, '/any')).toEqual({
        isTrusted: true,
        source: undefined,
      });
    });
  });

  describe('isWorkspaceTrusted headless mode', () => {
    const mockSettings: Settings = {
      security: {
        folderTrust: {
          enabled: true,
        },
      },
    };

    it('should NOT 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: undefined,
        source: undefined,
      });
    });

    it('should return true when GEMINI_CLI_TRUST_WORKSPACE is true', async () => {
      process.env['GEMINI_CLI_TRUST_WORKSPACE'] = 'true';
      try {
        expect(isWorkspaceTrusted(mockSettings)).toEqual({
          isTrusted: true,
          source: 'env',
        });
      } finally {
        delete process.env['GEMINI_CLI_TRUST_WORKSPACE'];
      }
    });

    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,
      );
    });

    it('should return undefined for isPathTrusted when isHeadlessMode is true', async () => {
      const geminiCore = await import('@google/gemini-cli-core');
      vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);

      const folders = loadTrustedFolders();
      expect(folders.isPathTrusted('/any-untrusted-path')).toBe(undefined);
    });
  });

  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}.`,
      );
    });
  });

  const itif = (condition: boolean) => (condition ? it : it.skip);

  describe('Symlinks Support', () => {
    const mockSettings: Settings = {
      security: { folderTrust: { enabled: true } },
    };

    // TODO: issue 19387 - Enable symlink tests on Windows
    itif(process.platform !== 'win32')(
      '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, 'dir');

        // 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();
    });
  });
});
