/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as path from 'node:path';
import * as os from 'node:os';
import {
  getEnvContents,
  maybePromptForSettings,
  promptForSetting,
  type ExtensionSetting,
  updateSetting,
  ExtensionSettingScope,
  getScopedEnvContents,
} from './extensionSettings.js';
import type { ExtensionConfig } from '../extension.js';
import { ExtensionStorage } from './storage.js';
import prompts from 'prompts';
import * as fsPromises from 'node:fs/promises';
import * as fs from 'node:fs';
import { KeychainTokenStorage } from '@google/gemini-cli-core';
import { EXTENSION_SETTINGS_FILENAME } from './variables.js';

vi.mock('prompts');
vi.mock('os', async (importOriginal) => {
  const mockedOs = await importOriginal<typeof os>();
  return {
    ...mockedOs,
    homedir: vi.fn(),
  };
});

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    KeychainTokenStorage: vi.fn(),
  };
});

describe('extensionSettings', () => {
  let tempHomeDir: string;
  let tempWorkspaceDir: string;
  let extensionDir: string;
  let mockKeychainData: Record<string, Record<string, string>>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockKeychainData = {};
    vi.mocked(KeychainTokenStorage).mockImplementation(
      (serviceName: string) => {
        if (!mockKeychainData[serviceName]) {
          mockKeychainData[serviceName] = {};
        }
        const keychainData = mockKeychainData[serviceName];
        return {
          getSecret: vi
            .fn()
            .mockImplementation(
              async (key: string) => keychainData[key] || null,
            ),
          setSecret: vi
            .fn()
            .mockImplementation(async (key: string, value: string) => {
              keychainData[key] = value;
            }),
          deleteSecret: vi.fn().mockImplementation(async (key: string) => {
            delete keychainData[key];
          }),
          listSecrets: vi
            .fn()
            .mockImplementation(async () => Object.keys(keychainData)),
          isAvailable: vi.fn().mockResolvedValue(true),
        } as unknown as KeychainTokenStorage;
      },
    );
    tempHomeDir = os.tmpdir() + path.sep + `gemini-cli-test-home-${Date.now()}`;
    tempWorkspaceDir = path.join(
      os.tmpdir(),
      `gemini-cli-test-workspace-${Date.now()}`,
    );
    extensionDir = path.join(tempHomeDir, '.gemini', 'extensions', 'test-ext');
    // Spy and mock the method, but also create the directory so we can write to it.
    vi.spyOn(ExtensionStorage.prototype, 'getExtensionDir').mockReturnValue(
      extensionDir,
    );
    fs.mkdirSync(extensionDir, { recursive: true });
    fs.mkdirSync(tempWorkspaceDir, { recursive: true });
    vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
    vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
    vi.mocked(prompts).mockClear();
  });

  afterEach(() => {
    fs.rmSync(tempHomeDir, { recursive: true, force: true });
    fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
    vi.restoreAllMocks();
  });

  describe('maybePromptForSettings', () => {
    const mockRequestSetting = vi.fn(
      async (setting: ExtensionSetting) => `mock-${setting.envVar}`,
    );

    beforeEach(() => {
      mockRequestSetting.mockClear();
    });

    it('should do nothing if settings are undefined', async () => {
      const config: ExtensionConfig = { name: 'test-ext', version: '1.0.0' };
      await maybePromptForSettings(
        config,
        '12345',
        mockRequestSetting,
        undefined,
        undefined,
      );
      expect(mockRequestSetting).not.toHaveBeenCalled();
    });

    it('should do nothing if settings are empty', async () => {
      const config: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [],
      };
      await maybePromptForSettings(
        config,
        '12345',
        mockRequestSetting,
        undefined,
        undefined,
      );
      expect(mockRequestSetting).not.toHaveBeenCalled();
    });

    it('should prompt for all settings if there is no previous config', async () => {
      const config: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1' },
          { name: 's2', description: 'd2', envVar: 'VAR2' },
        ],
      };
      await maybePromptForSettings(
        config,
        '12345',
        mockRequestSetting,
        undefined,
        undefined,
      );
      expect(mockRequestSetting).toHaveBeenCalledTimes(2);
      expect(mockRequestSetting).toHaveBeenCalledWith(config.settings![0]);
      expect(mockRequestSetting).toHaveBeenCalledWith(config.settings![1]);
    });

    it('should only prompt for new settings', async () => {
      const previousConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
      };
      const newConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1' },
          { name: 's2', description: 'd2', envVar: 'VAR2' },
        ],
      };
      const previousSettings = { VAR1: 'previous-VAR1' };

      await maybePromptForSettings(
        newConfig,
        '12345',
        mockRequestSetting,
        previousConfig,
        previousSettings,
      );

      expect(mockRequestSetting).toHaveBeenCalledTimes(1);
      expect(mockRequestSetting).toHaveBeenCalledWith(newConfig.settings![1]);

      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      const expectedContent = 'VAR1=previous-VAR1\nVAR2=mock-VAR2\n';
      expect(actualContent).toBe(expectedContent);
    });

    it('should clear settings if new config has no settings', async () => {
      const previousConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1' },
          {
            name: 's2',
            description: 'd2',
            envVar: 'SENSITIVE_VAR',
            sensitive: true,
          },
        ],
      };
      const newConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [],
      };
      const previousSettings = {
        VAR1: 'previous-VAR1',
        SENSITIVE_VAR: 'secret',
      };
      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      await userKeychain.setSecret('SENSITIVE_VAR', 'secret');
      const envPath = path.join(extensionDir, '.env');
      await fsPromises.writeFile(envPath, 'VAR1=previous-VAR1');

      await maybePromptForSettings(
        newConfig,
        '12345',
        mockRequestSetting,
        previousConfig,
        previousSettings,
      );

      expect(mockRequestSetting).not.toHaveBeenCalled();
      const actualContent = await fsPromises.readFile(envPath, 'utf-8');
      expect(actualContent).toBe('');
      expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
    });

    it('should remove sensitive settings from keychain', async () => {
      const previousConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          {
            name: 's1',
            description: 'd1',
            envVar: 'SENSITIVE_VAR',
            sensitive: true,
          },
        ],
      };
      const newConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [],
      };
      const previousSettings = { SENSITIVE_VAR: 'secret' };
      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      await userKeychain.setSecret('SENSITIVE_VAR', 'secret');

      await maybePromptForSettings(
        newConfig,
        '12345',
        mockRequestSetting,
        previousConfig,
        previousSettings,
      );

      expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
    });

    it('should remove settings that are no longer in the config', async () => {
      const previousConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1' },
          { name: 's2', description: 'd2', envVar: 'VAR2' },
        ],
      };
      const newConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
      };
      const previousSettings = {
        VAR1: 'previous-VAR1',
        VAR2: 'previous-VAR2',
      };

      await maybePromptForSettings(
        newConfig,
        '12345',
        mockRequestSetting,
        previousConfig,
        previousSettings,
      );

      expect(mockRequestSetting).not.toHaveBeenCalled();

      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      const expectedContent = 'VAR1=previous-VAR1\n';
      expect(actualContent).toBe(expectedContent);
    });

    it('should reprompt if a setting changes sensitivity', async () => {
      const previousConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1', sensitive: false },
        ],
      };
      const newConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1', sensitive: true },
        ],
      };
      const previousSettings = { VAR1: 'previous-VAR1' };

      await maybePromptForSettings(
        newConfig,
        '12345',
        mockRequestSetting,
        previousConfig,
        previousSettings,
      );

      expect(mockRequestSetting).toHaveBeenCalledTimes(1);
      expect(mockRequestSetting).toHaveBeenCalledWith(newConfig.settings![0]);

      // The value should now be in keychain, not the .env file.
      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      expect(actualContent).toBe('');
    });

    it('should not prompt if settings are identical', async () => {
      const previousConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1' },
          { name: 's2', description: 'd2', envVar: 'VAR2' },
        ],
      };
      const newConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          { name: 's1', description: 'd1', envVar: 'VAR1' },
          { name: 's2', description: 'd2', envVar: 'VAR2' },
        ],
      };
      const previousSettings = {
        VAR1: 'previous-VAR1',
        VAR2: 'previous-VAR2',
      };

      await maybePromptForSettings(
        newConfig,
        '12345',
        mockRequestSetting,
        previousConfig,
        previousSettings,
      );

      expect(mockRequestSetting).not.toHaveBeenCalled();
      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      const expectedContent = 'VAR1=previous-VAR1\nVAR2=previous-VAR2\n';
      expect(actualContent).toBe(expectedContent);
    });

    it('should wrap values with spaces in quotes', async () => {
      const config: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
      };
      mockRequestSetting.mockResolvedValue('a value with spaces');

      await maybePromptForSettings(
        config,
        '12345',
        mockRequestSetting,
        undefined,
        undefined,
      );

      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      expect(actualContent).toBe('VAR1="a value with spaces"\n');
    });

    it('should not set sensitive settings if the value is empty during initial setup', async () => {
      const config: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [
          {
            name: 's1',
            description: 'd1',
            envVar: 'SENSITIVE_VAR',
            sensitive: true,
          },
        ],
      };
      mockRequestSetting.mockResolvedValue('');

      await maybePromptForSettings(
        config,
        '12345',
        mockRequestSetting,
        undefined,
        undefined,
      );

      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
    });

    it('should not attempt to clear secrets if keychain is unavailable', async () => {
      // Arrange
      const mockIsAvailable = vi.fn().mockResolvedValue(false);
      const mockListSecrets = vi.fn();

      vi.mocked(KeychainTokenStorage).mockImplementation(
        () =>
          ({
            isAvailable: mockIsAvailable,
            listSecrets: mockListSecrets,
            deleteSecret: vi.fn(),
            getSecret: vi.fn(),
            setSecret: vi.fn(),
          }) as unknown as KeychainTokenStorage,
      );

      const config: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [], // Empty settings triggers clearSettings
      };

      const previousConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
      };

      // Act
      await maybePromptForSettings(
        config,
        '12345',
        mockRequestSetting,
        previousConfig,
        undefined,
      );

      // Assert
      expect(mockIsAvailable).toHaveBeenCalled();
      expect(mockListSecrets).not.toHaveBeenCalled();
    });
  });

  describe('promptForSetting', () => {
    it.each([
      {
        description:
          'should use prompts with type "password" for sensitive settings',
        setting: {
          name: 'API Key',
          description: 'Your secret key',
          envVar: 'API_KEY',
          sensitive: true,
        },
        expectedType: 'password',
        promptValue: 'secret-key',
      },
      {
        description:
          'should use prompts with type "text" for non-sensitive settings',
        setting: {
          name: 'Username',
          description: 'Your public username',
          envVar: 'USERNAME',
          sensitive: false,
        },
        expectedType: 'text',
        promptValue: 'test-user',
      },
      {
        description: 'should default to "text" if sensitive is undefined',
        setting: {
          name: 'Username',
          description: 'Your public username',
          envVar: 'USERNAME',
        },
        expectedType: 'text',
        promptValue: 'test-user',
      },
    ])('$description', async ({ setting, expectedType, promptValue }) => {
      vi.mocked(prompts).mockResolvedValue({ value: promptValue });

      const result = await promptForSetting(setting as ExtensionSetting);

      expect(prompts).toHaveBeenCalledWith({
        type: expectedType,
        name: 'value',
        message: `${setting.name}\n${setting.description}`,
      });
      expect(result).toBe(promptValue);
    });

    it('should return undefined if the user cancels the prompt', async () => {
      vi.mocked(prompts).mockResolvedValue({ value: undefined });
      const result = await promptForSetting({
        name: 'Test',
        description: 'Test desc',
        envVar: 'TEST_VAR',
      });
      expect(result).toBeUndefined();
    });
  });

  describe('getScopedEnvContents', () => {
    const config: ExtensionConfig = {
      name: 'test-ext',
      version: '1.0.0',
      settings: [
        { name: 's1', description: 'd1', envVar: 'VAR1' },
        {
          name: 's2',
          description: 'd2',
          envVar: 'SENSITIVE_VAR',
          sensitive: true,
        },
      ],
    };
    const extensionId = '12345';

    it('should return combined contents from user .env and keychain for USER scope', async () => {
      const userEnvPath = path.join(extensionDir, EXTENSION_SETTINGS_FILENAME);
      await fsPromises.writeFile(userEnvPath, 'VAR1=user-value1');
      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      await userKeychain.setSecret('SENSITIVE_VAR', 'user-secret');

      const contents = await getScopedEnvContents(
        config,
        extensionId,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );

      expect(contents).toEqual({
        VAR1: 'user-value1',
        SENSITIVE_VAR: 'user-secret',
      });
    });

    it('should return combined contents from workspace .env and keychain for WORKSPACE scope', async () => {
      const workspaceEnvPath = path.join(
        tempWorkspaceDir,
        EXTENSION_SETTINGS_FILENAME,
      );
      await fsPromises.writeFile(workspaceEnvPath, 'VAR1=workspace-value1');
      const workspaceKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345 ${tempWorkspaceDir}`,
      );
      await workspaceKeychain.setSecret('SENSITIVE_VAR', 'workspace-secret');

      const contents = await getScopedEnvContents(
        config,
        extensionId,
        ExtensionSettingScope.WORKSPACE,
        tempWorkspaceDir,
      );

      expect(contents).toEqual({
        VAR1: 'workspace-value1',
        SENSITIVE_VAR: 'workspace-secret',
      });
    });

    it('should ignore .env if it is a directory', async () => {
      const workspaceEnvPath = path.join(
        tempWorkspaceDir,
        EXTENSION_SETTINGS_FILENAME,
      );
      fs.mkdirSync(workspaceEnvPath);
      const workspaceKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345 ${tempWorkspaceDir}`,
      );
      await workspaceKeychain.setSecret('SENSITIVE_VAR', 'workspace-secret');

      const contents = await getScopedEnvContents(
        config,
        extensionId,
        ExtensionSettingScope.WORKSPACE,
        tempWorkspaceDir,
      );

      expect(contents).toEqual({
        SENSITIVE_VAR: 'workspace-secret',
      });
    });
  });

  describe('getEnvContents (merged)', () => {
    const config: ExtensionConfig = {
      name: 'test-ext',
      version: '1.0.0',
      settings: [
        { name: 's1', description: 'd1', envVar: 'VAR1' },
        { name: 's2', description: 'd2', envVar: 'VAR2', sensitive: true },
        { name: 's3', description: 'd3', envVar: 'VAR3' },
      ],
    };
    const extensionId = '12345';

    it('should merge user and workspace settings, with workspace taking precedence', async () => {
      // User settings
      const userEnvPath = path.join(extensionDir, EXTENSION_SETTINGS_FILENAME);
      await fsPromises.writeFile(
        userEnvPath,
        'VAR1=user-value1\nVAR3=user-value3',
      );
      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext ${extensionId}`,
      );
      await userKeychain.setSecret('VAR2', 'user-secret2');

      // Workspace settings
      const workspaceEnvPath = path.join(
        tempWorkspaceDir,
        EXTENSION_SETTINGS_FILENAME,
      );
      await fsPromises.writeFile(workspaceEnvPath, 'VAR1=workspace-value1');
      const workspaceKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext ${extensionId} ${tempWorkspaceDir}`,
      );
      await workspaceKeychain.setSecret('VAR2', 'workspace-secret2');

      const contents = await getEnvContents(
        config,
        extensionId,
        tempWorkspaceDir,
      );

      expect(contents).toEqual({
        VAR1: 'workspace-value1',
        VAR2: 'workspace-secret2',
        VAR3: 'user-value3',
      });
    });
  });

  describe('updateSetting', () => {
    const config: ExtensionConfig = {
      name: 'test-ext',
      version: '1.0.0',
      settings: [
        { name: 's1', description: 'd1', envVar: 'VAR1' },
        { name: 's2', description: 'd2', envVar: 'VAR2', sensitive: true },
      ],
    };
    const mockRequestSetting = vi.fn();

    beforeEach(async () => {
      const userEnvPath = path.join(extensionDir, '.env');
      await fsPromises.writeFile(userEnvPath, 'VAR1=value1\n');
      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      await userKeychain.setSecret('VAR2', 'value2');
      mockRequestSetting.mockClear();
    });

    it('should update a non-sensitive setting in USER scope', async () => {
      mockRequestSetting.mockResolvedValue('new-value1');

      await updateSetting(
        config,
        '12345',
        'VAR1',
        mockRequestSetting,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );

      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      expect(actualContent).toContain('VAR1=new-value1');
    });

    it('should update a non-sensitive setting in WORKSPACE scope', async () => {
      mockRequestSetting.mockResolvedValue('new-workspace-value');

      await updateSetting(
        config,
        '12345',
        'VAR1',
        mockRequestSetting,
        ExtensionSettingScope.WORKSPACE,
        tempWorkspaceDir,
      );

      const expectedEnvPath = path.join(tempWorkspaceDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      expect(actualContent).toContain('VAR1=new-workspace-value');
    });

    it('should throw an error when trying to write to a workspace with a .env directory', async () => {
      const workspaceEnvPath = path.join(tempWorkspaceDir, '.env');
      fs.mkdirSync(workspaceEnvPath);

      mockRequestSetting.mockResolvedValue('new-workspace-value');

      await expect(
        updateSetting(
          config,
          '12345',
          'VAR1',
          mockRequestSetting,
          ExtensionSettingScope.WORKSPACE,
          tempWorkspaceDir,
        ),
      ).rejects.toThrow(
        /Cannot write extension settings to .* because it is a directory./,
      );
    });

    it('should update a sensitive setting in USER scope', async () => {
      mockRequestSetting.mockResolvedValue('new-value2');

      await updateSetting(
        config,
        '12345',
        'VAR2',
        mockRequestSetting,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );

      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      expect(await userKeychain.getSecret('VAR2')).toBe('new-value2');
    });

    it('should update a sensitive setting in WORKSPACE scope', async () => {
      mockRequestSetting.mockResolvedValue('new-workspace-secret');

      await updateSetting(
        config,
        '12345',
        'VAR2',
        mockRequestSetting,
        ExtensionSettingScope.WORKSPACE,
        tempWorkspaceDir,
      );

      const workspaceKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345 ${tempWorkspaceDir}`,
      );
      expect(await workspaceKeychain.getSecret('VAR2')).toBe(
        'new-workspace-secret',
      );
    });

    it('should leave existing, unmanaged .env variables intact when updating in WORKSPACE scope', async () => {
      // Setup a pre-existing .env file in the workspace with unmanaged variables
      const workspaceEnvPath = path.join(tempWorkspaceDir, '.env');
      const originalEnvContent =
        'PROJECT_VAR_1=value_1\nPROJECT_VAR_2=value_2\nVAR1=original-value'; // VAR1 is managed by extension
      await fsPromises.writeFile(workspaceEnvPath, originalEnvContent);

      // Simulate updating an extension-managed non-sensitive setting
      mockRequestSetting.mockResolvedValue('updated-value');
      await updateSetting(
        config,
        '12345',
        'VAR1',
        mockRequestSetting,
        ExtensionSettingScope.WORKSPACE,
        tempWorkspaceDir,
      );

      // Read the .env file after update
      const actualContent = await fsPromises.readFile(
        workspaceEnvPath,
        'utf-8',
      );

      // Assert that original variables are intact and extension variable is updated
      expect(actualContent).toContain('PROJECT_VAR_1=value_1');
      expect(actualContent).toContain('PROJECT_VAR_2=value_2');
      expect(actualContent).toContain('VAR1=updated-value');

      // Ensure no other unexpected changes or deletions
      const lines = actualContent.split('\n').filter((line) => line.length > 0);
      expect(lines).toHaveLength(3); // Should only have the three variables
    });

    it('should delete a sensitive setting if the new value is empty', async () => {
      mockRequestSetting.mockResolvedValue('');

      await updateSetting(
        config,
        '12345',
        'VAR2',
        mockRequestSetting,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );

      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      expect(await userKeychain.getSecret('VAR2')).toBeNull();
    });

    it('should delete a non-sensitive setting if the new value is empty', async () => {
      mockRequestSetting.mockResolvedValue('');

      await updateSetting(
        config,
        '12345',
        'VAR1',
        mockRequestSetting,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );

      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      expect(actualContent).not.toContain('VAR1=');
    });

    it('should not throw if deleting a non-existent sensitive setting with empty value', async () => {
      mockRequestSetting.mockResolvedValue('');
      // Ensure it doesn't exist first
      const userKeychain = new KeychainTokenStorage(
        `Gemini CLI Extensions test-ext 12345`,
      );
      await userKeychain.deleteSecret('VAR2');

      await updateSetting(
        config,
        '12345',
        'VAR2',
        mockRequestSetting,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );
      // Should complete without error
    });

    it('should throw error if env var name contains invalid characters', async () => {
      const securityConfig: ExtensionConfig = {
        name: 'test-ext',
        version: '1.0.0',
        settings: [{ name: 's2', description: 'd2', envVar: 'VAR-BAD' }],
      };
      mockRequestSetting.mockResolvedValue('value');

      await expect(
        updateSetting(
          securityConfig,
          '12345',
          'VAR-BAD',
          mockRequestSetting,
          ExtensionSettingScope.USER,
          tempWorkspaceDir,
        ),
      ).rejects.toThrow(/Invalid environment variable name/);
    });

    it('should throw error if env var value contains newlines', async () => {
      mockRequestSetting.mockResolvedValue('value\nwith\nnewlines');

      await expect(
        updateSetting(
          config,
          '12345',
          'VAR1',
          mockRequestSetting,
          ExtensionSettingScope.USER,
          tempWorkspaceDir,
        ),
      ).rejects.toThrow(/Invalid environment variable value/);
    });

    it('should quote values with spaces', async () => {
      mockRequestSetting.mockResolvedValue('value with spaces');

      await updateSetting(
        config,
        '12345',
        'VAR1',
        mockRequestSetting,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );

      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      expect(actualContent).toContain('VAR1="value with spaces"');
    });

    it('should escape quotes in values', async () => {
      mockRequestSetting.mockResolvedValue('value with "quotes"');

      await updateSetting(
        config,
        '12345',
        'VAR1',
        mockRequestSetting,
        ExtensionSettingScope.USER,
        tempWorkspaceDir,
      );

      const expectedEnvPath = path.join(extensionDir, '.env');
      const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
      expect(actualContent).toContain('VAR1="value with \\"quotes\\""');
    });
  });
});
