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

import {
  OAuth2Client,
  Compute,
  GoogleAuth,
  type Credentials,
} from 'google-auth-library';
import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
  afterEach,
  type Mock,
} from 'vitest';
import {
  getOauthClient,
  resetOauthClientForTesting,
  clearCachedCredentialFile,
  clearOauthClientCache,
  authEvents,
} from './oauth2.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import http from 'node:http';
import open from 'open';
import crypto from 'node:crypto';
import * as os from 'node:os';
import { AuthType } from '../core/contentGenerator.js';
import type { Config } from '../config/config.js';
import readline from 'node:readline';
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
import { GEMINI_DIR, homedir as pathsHomedir } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js';
import { writeToStdout } from '../utils/stdio.js';
import {
  FatalCancellationError,
  FatalAuthenticationError,
} from '../utils/errors.js';
import process from 'node:process';
import { coreEvents } from '../utils/events.js';
import { isHeadlessMode } from '../utils/headless.js';

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

vi.mock('../utils/paths.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../utils/paths.js')>();
  return {
    ...actual,
    homedir: vi.fn(),
  };
});

vi.mock('google-auth-library');
vi.mock('http');
vi.mock('open');
vi.mock('crypto');
vi.mock('node:readline');
vi.mock('../utils/headless.js', () => ({
  isHeadlessMode: vi.fn(),
}));
vi.mock('../utils/browser.js', () => ({
  shouldAttemptBrowserLaunch: () => true,
}));
vi.mock('../utils/stdio.js', () => ({
  writeToStdout: vi.fn(),
  writeToStderr: vi.fn(),
  createWorkingStdio: vi.fn(() => ({
    stdout: process.stdout,
    stderr: process.stderr,
  })),
  enterAlternateScreen: vi.fn(),
  exitAlternateScreen: vi.fn(),
  enableLineWrapping: vi.fn(),
  disableMouseEvents: vi.fn(),
  disableKittyKeyboardProtocol: vi.fn(),
}));

vi.mock('./oauth-credential-storage.js', () => ({
  OAuthCredentialStorage: {
    saveCredentials: vi.fn(),
    loadCredentials: vi.fn(),
    clearCredentials: vi.fn(),
  },
}));

vi.mock('../mcp/token-storage/hybrid-token-storage.js', () => ({
  HybridTokenStorage: vi.fn(() => ({
    getCredentials: vi.fn(),
    setCredentials: vi.fn(),
    deleteCredentials: vi.fn(),
  })),
}));

const mockConfig = {
  getNoBrowser: () => false,
  getProxy: () => 'http://test.proxy.com:8080',
  isBrowserLaunchSuppressed: () => false,
  getAcpMode: () => false,
  isInteractive: () => true,
} as unknown as Config;

// Mock fetch globally
global.fetch = vi.fn();

describe('oauth2', () => {
  beforeEach(() => {
    vi.mocked(isHeadlessMode).mockReturnValue(false);
    (readline.createInterface as Mock).mockReturnValue({
      question: vi.fn((_query, callback) => callback('')),
      close: vi.fn(),
      on: vi.fn(),
    });
    vi.spyOn(coreEvents, 'listenerCount').mockReturnValue(1);
    vi.spyOn(coreEvents, 'emitConsentRequest').mockImplementation((payload) => {
      payload.onConfirm(true);
    });
  });

  describe('with encrypted flag false', () => {
    let tempHomeDir: string;

    beforeEach(() => {
      process.env[FORCE_ENCRYPTED_FILE_ENV_VAR] = 'false';
      tempHomeDir = fs.mkdtempSync(
        path.join(os.tmpdir(), 'gemini-cli-test-home-'),
      );
      vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
      vi.mocked(pathsHomedir).mockReturnValue(tempHomeDir);
    });
    afterEach(() => {
      fs.rmSync(tempHomeDir, { recursive: true, force: true });
      vi.clearAllMocks();
      resetOauthClientForTesting();
      vi.unstubAllEnvs();
    });

    it('should perform a web login', async () => {
      const mockAuthUrl = 'https://example.com/auth';
      const mockCode = 'test-code';
      const mockState = 'test-state';
      const mockTokens = {
        access_token: 'test-access-token',
        refresh_token: 'test-refresh-token',
      };

      const mockGenerateAuthUrl = vi.fn().mockReturnValue(mockAuthUrl);
      const mockGetToken = vi.fn().mockResolvedValue({ tokens: mockTokens });
      const mockSetCredentials = vi.fn();
      const mockGetAccessToken = vi
        .fn()
        .mockResolvedValue({ token: 'mock-access-token' });
      let tokensListener: ((tokens: Credentials) => void) | undefined;
      const mockOAuth2Client = {
        generateAuthUrl: mockGenerateAuthUrl,
        getToken: mockGetToken,
        setCredentials: mockSetCredentials,
        getAccessToken: mockGetAccessToken,
        credentials: mockTokens,
        on: vi.fn((event, listener) => {
          if (event === 'tokens') {
            tokensListener = listener;
          }
        }),
      } as unknown as OAuth2Client;
      vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

      vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
      vi.mocked(open).mockImplementation(
        async () => ({ on: vi.fn() }) as never,
      );

      // Mock the UserInfo API response
      vi.mocked(global.fetch).mockResolvedValue({
        ok: true,
        json: vi
          .fn()
          .mockResolvedValue({ email: 'test-google-account@gmail.com' }),
      } as unknown as Response);

      let requestCallback!: http.RequestListener<
        typeof http.IncomingMessage,
        typeof http.ServerResponse
      >;

      let serverListeningCallback: (value: unknown) => void;
      const serverListeningPromise = new Promise(
        (resolve) => (serverListeningCallback = resolve),
      );

      let capturedPort = 0;
      const mockHttpServer = {
        listen: vi.fn((port: number, _host: string, callback?: () => void) => {
          capturedPort = port;
          if (callback) {
            callback();
          }
          serverListeningCallback(undefined);
        }),
        close: vi.fn((callback?: () => void) => {
          if (callback) {
            callback();
          }
        }),
        on: vi.fn(),
        address: () => ({ port: capturedPort }),
      };
      (http.createServer as Mock).mockImplementation((cb) => {
        requestCallback = cb as http.RequestListener<
          typeof http.IncomingMessage,
          typeof http.ServerResponse
        >;
        return mockHttpServer as unknown as http.Server;
      });

      const clientPromise = getOauthClient(
        AuthType.LOGIN_WITH_GOOGLE,
        mockConfig,
      );

      // wait for server to start listening.
      await serverListeningPromise;

      const mockReq = {
        url: `/oauth2callback?code=${mockCode}&state=${mockState}`,
      } as http.IncomingMessage;
      const mockRes = {
        writeHead: vi.fn(),
        end: vi.fn(),
      } as unknown as http.ServerResponse;

      requestCallback(mockReq, mockRes);

      const client = await clientPromise;
      expect(client).toBe(mockOAuth2Client);

      expect(open).toHaveBeenCalledWith(mockAuthUrl);
      expect(mockGetToken).toHaveBeenCalledWith({
        code: mockCode,
        redirect_uri: `http://127.0.0.1:${capturedPort}/oauth2callback`,
      });
      expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);

      // Manually trigger the 'tokens' event listener
      if (tokensListener) {
        await (
          tokensListener as unknown as (tokens: Credentials) => Promise<void>
        )(mockTokens);
      }

      // Verify Google Account was cached
      const googleAccountPath = path.join(
        tempHomeDir,
        GEMINI_DIR,
        'google_accounts.json',
      );
      expect(fs.existsSync(googleAccountPath)).toBe(true);
      const cachedGoogleAccount = fs.readFileSync(googleAccountPath, 'utf-8');
      expect(JSON.parse(cachedGoogleAccount)).toEqual({
        active: 'test-google-account@gmail.com',
        old: [],
      });

      // Verify the getCachedGoogleAccount function works
      const userAccountManager = new UserAccountManager();
      expect(userAccountManager.getCachedGoogleAccount()).toBe(
        'test-google-account@gmail.com',
      );
    });

    it('should clear credentials file', async () => {
      // Setup initial state with files
      const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');

      await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
      await fs.promises.writeFile(credsPath, '{}');

      await clearCachedCredentialFile();

      expect(fs.existsSync(credsPath)).toBe(false);
    });

    it('should emit post_auth event when loading cached credentials', async () => {
      const cachedCreds = { refresh_token: 'cached-token' };
      const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
      await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
      await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));

      const mockClient = {
        setCredentials: vi.fn(),
        getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
        getTokenInfo: vi.fn().mockResolvedValue({}),
        on: vi.fn(),
      };
      vi.mocked(OAuth2Client).mockImplementation(
        () => mockClient as unknown as OAuth2Client,
      );

      const eventPromise = new Promise<void>((resolve) => {
        authEvents.once('post_auth', (creds) => {
          expect(creds.refresh_token).toBe('cached-token');
          resolve();
        });
      });

      await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);
      await eventPromise;
    });

    it('should throw FatalAuthenticationError in non-interactive session when manual auth is required', async () => {
      const mockConfigNonInteractive = {
        getNoBrowser: () => true,
        getProxy: () => 'http://test.proxy.com:8080',
        isBrowserLaunchSuppressed: () => true,
        isInteractive: () => false,
      } as unknown as Config;

      await expect(
        getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfigNonInteractive),
      ).rejects.toThrow(FatalAuthenticationError);

      await expect(
        getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfigNonInteractive),
      ).rejects.toThrow(
        'Manual authorization is required but the current session is non-interactive.',
      );
    });

    it('should perform login with user code', async () => {
      const mockConfigWithNoBrowser = {
        getNoBrowser: () => true,
        getProxy: () => 'http://test.proxy.com:8080',
        isBrowserLaunchSuppressed: () => true,
        isInteractive: () => true,
      } as unknown as Config;

      const mockCodeVerifier = {
        codeChallenge: 'test-challenge',
        codeVerifier: 'test-verifier',
      };
      const mockAuthUrl = 'https://example.com/auth-user-code';
      const mockCode = 'test-user-code';

      const mockTokens = {
        access_token: 'test-access-token-user-code',
        refresh_token: 'test-refresh-token-user-code',
      };

      const mockGenerateAuthUrl = vi.fn().mockReturnValue(mockAuthUrl);
      const mockGetToken = vi.fn().mockResolvedValue({ tokens: mockTokens });
      const mockGenerateCodeVerifierAsync = vi
        .fn()
        .mockResolvedValue(mockCodeVerifier);

      const mockOAuth2Client = {
        generateAuthUrl: mockGenerateAuthUrl,
        getToken: mockGetToken,
        generateCodeVerifierAsync: mockGenerateCodeVerifierAsync,
        getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
        on: vi.fn(),
        credentials: {},
      } as unknown as OAuth2Client;
      mockOAuth2Client.setCredentials = vi.fn().mockImplementation((creds) => {
        mockOAuth2Client.credentials = creds;
      });
      vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

      const mockReadline = {
        question: vi.fn((_query, callback) => callback(mockCode)),
        close: vi.fn(),
        on: vi.fn(),
      };
      (readline.createInterface as Mock).mockReturnValue(mockReadline);

      const client = await getOauthClient(
        AuthType.LOGIN_WITH_GOOGLE,
        mockConfigWithNoBrowser,
      );

      expect(client).toBe(mockOAuth2Client);

      // Verify the auth flow
      expect(mockGenerateCodeVerifierAsync).toHaveBeenCalled();
      expect(mockGenerateAuthUrl).toHaveBeenCalled();
      expect(vi.mocked(writeToStdout)).toHaveBeenCalledWith(
        expect.stringContaining(mockAuthUrl),
      );
      expect(mockReadline.question).toHaveBeenCalledWith(
        'Enter the authorization code: ',
        expect.any(Function),
      );
      expect(mockGetToken).toHaveBeenCalledWith({
        code: mockCode,
        codeVerifier: mockCodeVerifier.codeVerifier,
        redirect_uri: 'https://codeassist.google.com/authcode',
      });
      expect(mockOAuth2Client.setCredentials).toHaveBeenCalledWith(mockTokens);
    });

    it('should cache Google Account when logging in with user code', async () => {
      const mockConfigWithNoBrowser = {
        getNoBrowser: () => true,
        getProxy: () => 'http://test.proxy.com:8080',
        isBrowserLaunchSuppressed: () => true,
        isInteractive: () => true,
      } as unknown as Config;

      const mockCodeVerifier = {
        codeChallenge: 'test-challenge',
        codeVerifier: 'test-verifier',
      };
      const mockAuthUrl = 'https://example.com/auth-user-code';
      const mockCode = 'test-user-code';
      const mockTokens = {
        access_token: 'test-access-token-user-code',
        refresh_token: 'test-refresh-token-user-code',
      };

      const mockGenerateAuthUrl = vi.fn().mockReturnValue(mockAuthUrl);
      const mockGetToken = vi.fn().mockResolvedValue({ tokens: mockTokens });
      const mockGenerateCodeVerifierAsync = vi
        .fn()
        .mockResolvedValue(mockCodeVerifier);
      const mockGetAccessToken = vi
        .fn()
        .mockResolvedValue({ token: 'test-access-token-user-code' });

      const mockOAuth2Client = {
        generateAuthUrl: mockGenerateAuthUrl,
        getToken: mockGetToken,
        generateCodeVerifierAsync: mockGenerateCodeVerifierAsync,
        getAccessToken: mockGetAccessToken,
        on: vi.fn(),
        credentials: {},
      } as unknown as OAuth2Client;
      mockOAuth2Client.setCredentials = vi.fn().mockImplementation((creds) => {
        mockOAuth2Client.credentials = creds;
      });
      vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

      vi.spyOn(crypto, 'randomBytes').mockReturnValue('test-state' as never);

      const mockReadline = {
        question: vi.fn((_query, callback) => callback(mockCode)),
        close: vi.fn(),
        on: vi.fn(),
      };
      (readline.createInterface as Mock).mockReturnValue(mockReadline);

      // Mock User Info API
      vi.mocked(global.fetch).mockResolvedValue({
        ok: true,
        json: vi
          .fn()
          .mockResolvedValue({ email: 'test-user-code-account@gmail.com' }),
      } as unknown as Response);

      await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfigWithNoBrowser);

      // Verify Google Account was cached
      const googleAccountPath = path.join(
        tempHomeDir,
        GEMINI_DIR,
        'google_accounts.json',
      );

      expect(fs.existsSync(googleAccountPath)).toBe(true);
      if (fs.existsSync(googleAccountPath)) {
        const cachedGoogleAccount = fs.readFileSync(googleAccountPath, 'utf-8');

        expect(JSON.parse(cachedGoogleAccount)).toEqual({
          active: 'test-user-code-account@gmail.com',
          old: [],
        });
      }
    });

    describe('in Cloud Shell', () => {
      const mockGetAccessToken = vi.fn();
      let mockComputeClient: Compute;

      beforeEach(() => {
        mockGetAccessToken.mockResolvedValue({ token: 'test-access-token' });
        mockComputeClient = {
          credentials: { refresh_token: 'test-refresh-token' },
          getAccessToken: mockGetAccessToken,
        } as unknown as Compute;

        (Compute as unknown as Mock).mockImplementation(
          () => mockComputeClient,
        );
      });

      it('should attempt to load cached credentials first', async () => {
        const cachedCreds = { refresh_token: 'cached-token' };
        const credsPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'oauth_creds.json',
        );
        await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
        await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));

        const mockClient = {
          setCredentials: vi.fn(),
          getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
          getTokenInfo: vi.fn().mockResolvedValue({}),
          on: vi.fn(),
        };

        // To mock the new OAuth2Client() inside the function
        vi.mocked(OAuth2Client).mockImplementation(
          () => mockClient as unknown as OAuth2Client,
        );

        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);

        expect(mockClient.setCredentials).toHaveBeenCalledWith(cachedCreds);
        expect(mockClient.getAccessToken).toHaveBeenCalled();
        expect(mockClient.getTokenInfo).toHaveBeenCalled();
        expect(Compute).not.toHaveBeenCalled(); // Should not fetch new client if cache is valid
      });

      it('should use Compute to get a client if no cached credentials exist', async () => {
        await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);

        expect(Compute).toHaveBeenCalledWith({});
        expect(mockGetAccessToken).toHaveBeenCalled();
      });

      it('should not cache the credentials after fetching them via ADC', async () => {
        const newCredentials = { refresh_token: 'new-adc-token' };
        mockComputeClient.credentials = newCredentials;
        mockGetAccessToken.mockResolvedValue({ token: 'new-adc-token' });

        await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);

        const credsPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'oauth_creds.json',
        );
        expect(fs.existsSync(credsPath)).toBe(false);
      });

      it('should return the Compute client on successful ADC authentication', async () => {
        const client = await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);
        expect(client).toBe(mockComputeClient);
      });

      it('should throw an error if ADC fails', async () => {
        const testError = new Error('ADC Failed');
        mockGetAccessToken.mockRejectedValue(testError);

        await expect(
          getOauthClient(AuthType.COMPUTE_ADC, mockConfig),
        ).rejects.toThrow(
          'Could not authenticate using metadata server application default credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed',
        );
      });
    });

    describe('credential loading order', () => {
      it('should prioritize default cached credentials over GOOGLE_APPLICATION_CREDENTIALS', async () => {
        // Setup default cached credentials
        const defaultCreds = { refresh_token: 'default-cached-token' };
        const defaultCredsPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'oauth_creds.json',
        );
        await fs.promises.mkdir(path.dirname(defaultCredsPath), {
          recursive: true,
        });
        await fs.promises.writeFile(
          defaultCredsPath,
          JSON.stringify(defaultCreds),
        );

        // Setup credentials via environment variable
        const envCreds = { refresh_token: 'env-var-token' };
        const envCredsPath = path.join(tempHomeDir, 'env_creds.json');
        await fs.promises.writeFile(envCredsPath, JSON.stringify(envCreds));
        vi.stubEnv('GOOGLE_APPLICATION_CREDENTIALS', envCredsPath);

        const mockClient = {
          setCredentials: vi.fn(),
          getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
          getTokenInfo: vi.fn().mockResolvedValue({}),
          on: vi.fn(),
        };
        vi.mocked(OAuth2Client).mockImplementation(
          () => mockClient as unknown as OAuth2Client,
        );

        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);

        // Assert the correct credentials were used
        expect(mockClient.setCredentials).toHaveBeenCalledWith(defaultCreds);
        expect(mockClient.setCredentials).not.toHaveBeenCalledWith(envCreds);
      });

      it('should fall back to GOOGLE_APPLICATION_CREDENTIALS if default cache is missing', async () => {
        // Setup credentials via environment variable
        const envCreds = { refresh_token: 'env-var-token' };
        const envCredsPath = path.join(tempHomeDir, 'env_creds.json');
        await fs.promises.writeFile(envCredsPath, JSON.stringify(envCreds));
        vi.stubEnv('GOOGLE_APPLICATION_CREDENTIALS', envCredsPath);

        const mockClient = {
          setCredentials: vi.fn(),
          getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
          getTokenInfo: vi.fn().mockResolvedValue({}),
          on: vi.fn(),
        };
        vi.mocked(OAuth2Client).mockImplementation(
          () => mockClient as unknown as OAuth2Client,
        );

        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);

        // Assert the correct credentials were used
        expect(mockClient.setCredentials).toHaveBeenCalledWith(envCreds);
      });

      it('should use GoogleAuth for BYOID credentials from GOOGLE_APPLICATION_CREDENTIALS', async () => {
        // Setup BYOID credentials via environment variable
        const byoidCredentials = {
          type: 'external_account_authorized_user',
          client_id: 'mock-client-id',
        };
        const envCredsPath = path.join(tempHomeDir, 'byoid_creds.json');
        await fs.promises.writeFile(
          envCredsPath,
          JSON.stringify(byoidCredentials),
        );
        vi.stubEnv('GOOGLE_APPLICATION_CREDENTIALS', envCredsPath);

        // Mock GoogleAuth and its chain of calls
        const mockExternalAccountClient = {
          getAccessToken: vi.fn().mockResolvedValue({ token: 'byoid-token' }),
        };
        const mockFromJSON = vi.fn().mockReturnValue(mockExternalAccountClient);
        const mockGoogleAuthInstance = {
          fromJSON: mockFromJSON,
        };
        (GoogleAuth as unknown as Mock).mockImplementation(
          () => mockGoogleAuthInstance,
        );

        const mockOAuth2Client = {
          on: vi.fn(),
        };
        (OAuth2Client as unknown as Mock).mockImplementation(
          () => mockOAuth2Client,
        );

        const client = await getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );

        // Assert that GoogleAuth was used and the correct client was returned
        expect(GoogleAuth).toHaveBeenCalledWith({
          scopes: expect.any(Array),
        });
        expect(mockFromJSON).toHaveBeenCalledWith(byoidCredentials);
        expect(client).toBe(mockExternalAccountClient);
      });
    });

    describe('with GCP environment variables', () => {
      it('should use GOOGLE_CLOUD_ACCESS_TOKEN when GOOGLE_GENAI_USE_GCA is true', async () => {
        vi.stubEnv('GOOGLE_GENAI_USE_GCA', 'true');
        vi.stubEnv('GOOGLE_CLOUD_ACCESS_TOKEN', 'gcp-access-token');

        const mockSetCredentials = vi.fn();
        const mockGetAccessToken = vi
          .fn()
          .mockResolvedValue({ token: 'gcp-access-token' });
        const mockOAuth2Client = {
          setCredentials: mockSetCredentials,
          getAccessToken: mockGetAccessToken,
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        // Mock the UserInfo API response for fetchAndCacheUserInfo
        (global.fetch as Mock).mockResolvedValue({
          ok: true,
          json: vi
            .fn()
            .mockResolvedValue({ email: 'test-gcp-account@gmail.com' }),
        } as unknown as Response);

        const client = await getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );

        expect(client).toBe(mockOAuth2Client);
        expect(mockSetCredentials).toHaveBeenCalledWith({
          access_token: 'gcp-access-token',
        });

        // Verify fetchAndCacheUserInfo was effectively called
        expect(mockGetAccessToken).toHaveBeenCalled();
        expect(global.fetch).toHaveBeenCalledWith(
          'https://www.googleapis.com/oauth2/v2/userinfo',
          {
            headers: {
              Authorization: 'Bearer gcp-access-token',
            },
          },
        );

        // Verify Google Account was cached
        const googleAccountPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'google_accounts.json',
        );
        const cachedContent = fs.readFileSync(googleAccountPath, 'utf-8');
        expect(JSON.parse(cachedContent)).toEqual({
          active: 'test-gcp-account@gmail.com',
          old: [],
        });
      });

      it('should not use GCP token if GOOGLE_CLOUD_ACCESS_TOKEN is not set', async () => {
        vi.stubEnv('GOOGLE_GENAI_USE_GCA', 'true');

        const mockSetCredentials = vi.fn();
        const mockGetAccessToken = vi
          .fn()
          .mockResolvedValue({ token: 'cached-access-token' });
        const mockGetTokenInfo = vi.fn().mockResolvedValue({});
        const mockOAuth2Client = {
          setCredentials: mockSetCredentials,
          getAccessToken: mockGetAccessToken,
          getTokenInfo: mockGetTokenInfo,
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        // Make it fall through to cached credentials path
        const cachedCreds = { refresh_token: 'cached-token' };
        const credsPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'oauth_creds.json',
        );
        await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
        await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));

        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);

        // It should be called with the cached credentials, not the GCP access token.
        expect(mockSetCredentials).toHaveBeenCalledTimes(1);
        expect(mockSetCredentials).toHaveBeenCalledWith(cachedCreds);
      });

      it('should not use GCP token if GOOGLE_GENAI_USE_GCA is not set', async () => {
        vi.stubEnv('GOOGLE_CLOUD_ACCESS_TOKEN', 'gcp-access-token');

        const mockSetCredentials = vi.fn();
        const mockGetAccessToken = vi
          .fn()
          .mockResolvedValue({ token: 'cached-access-token' });
        const mockGetTokenInfo = vi.fn().mockResolvedValue({});
        const mockOAuth2Client = {
          setCredentials: mockSetCredentials,
          getAccessToken: mockGetAccessToken,
          getTokenInfo: mockGetTokenInfo,
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        // Make it fall through to cached credentials path
        const cachedCreds = { refresh_token: 'cached-token' };
        const credsPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'oauth_creds.json',
        );
        await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
        await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));

        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);

        // It should be called with the cached credentials, not the GCP access token.
        expect(mockSetCredentials).toHaveBeenCalledTimes(1);
        expect(mockSetCredentials).toHaveBeenCalledWith(cachedCreds);
      });
    });

    describe('error handling', () => {
      it('should handle browser launch failure with FatalAuthenticationError', async () => {
        const mockError = new Error('Browser launch failed');
        (open as Mock).mockRejectedValue(mockError);

        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue('https://example.com/auth'),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        await expect(
          getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig),
        ).rejects.toThrow('Failed to open browser: Browser launch failed');
      });

      it('should handle authentication timeout with proper error message', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        const mockHttpServer = {
          listen: vi.fn(),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation(
          () => mockHttpServer as unknown as http.Server,
        );

        // Mock setTimeout to trigger timeout immediately
        const originalSetTimeout = global.setTimeout;
        global.setTimeout = vi.fn(
          (callback) => (callback(), {} as unknown as NodeJS.Timeout),
        ) as unknown as typeof setTimeout;

        await expect(
          getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig),
        ).rejects.toThrow(
          'Authentication timed out after 5 minutes. The browser tab may have gotten stuck in a loading state. Please try again or use NO_BROWSER=true for manual authentication.',
        );

        global.setTimeout = originalSetTimeout;
      });

      it('should clear the authorization timeout immediately upon successful web login to prevent memory leaks', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockCode = 'test-code';
        const mockState = 'test-state';

        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          getToken: vi.fn().mockResolvedValue({
            tokens: {
              access_token: 'test-token',
              refresh_token: 'test-refresh',
            },
          }),
          setCredentials: vi.fn().mockImplementation(function (
            this: { credentials?: unknown },
            creds: unknown,
          ) {
            this.credentials = creds;
          }),
          getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
          on: vi.fn(),
          credentials: {},
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        let requestCallback!: http.RequestListener;
        let serverListeningCallback: (value: unknown) => void;
        const serverListeningPromise = new Promise(
          (resolve) => (serverListeningCallback = resolve),
        );

        const mockHttpServer = {
          listen: vi.fn(
            (_port: number, _host: string, callback?: () => void) => {
              if (callback) callback();
              serverListeningCallback(undefined);
            },
          ),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation((cb) => {
          requestCallback = cb;
          return mockHttpServer as unknown as http.Server;
        });

        const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );
        await serverListeningPromise;

        const mockReq = {
          url: `/oauth2callback?code=${mockCode}&state=${mockState}`,
        } as http.IncomingMessage;
        const mockRes = {
          writeHead: vi.fn(),
          end: vi.fn(),
          on: vi.fn(),
        } as unknown as http.ServerResponse;

        // Trigger the successful server response
        requestCallback(mockReq, mockRes);
        await clientPromise;

        // Verify that the watchdog timer was cleared correctly
        expect(clearTimeoutSpy).toHaveBeenCalled();

        clearTimeoutSpy.mockRestore();
      });

      it('should handle OAuth callback errors with descriptive messages', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        let requestCallback!: http.RequestListener;
        let serverListeningCallback: (value: unknown) => void;
        const serverListeningPromise = new Promise(
          (resolve) => (serverListeningCallback = resolve),
        );

        const mockHttpServer = {
          listen: vi.fn(
            (_port: number, _host: string, callback?: () => void) => {
              if (callback) callback();
              serverListeningCallback(undefined);
            },
          ),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation((cb) => {
          requestCallback = cb;
          return mockHttpServer as unknown as http.Server;
        });

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );
        await serverListeningPromise;

        // Test OAuth error with description
        const mockReq = {
          url: '/oauth2callback?error=access_denied&error_description=User+denied+access',
        } as http.IncomingMessage;
        const mockRes = {
          writeHead: vi.fn(),
          end: vi.fn(),
        } as unknown as http.ServerResponse;

        await expect(async () => {
          requestCallback(mockReq, mockRes);
          await clientPromise;
        }).rejects.toThrow(
          'Google OAuth error: access_denied. User denied access',
        );
      });

      it('should handle OAuth error without description', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        let requestCallback!: http.RequestListener;
        let serverListeningCallback: (value: unknown) => void;
        const serverListeningPromise = new Promise(
          (resolve) => (serverListeningCallback = resolve),
        );

        const mockHttpServer = {
          listen: vi.fn(
            (_port: number, _host: string, callback?: () => void) => {
              if (callback) callback();
              serverListeningCallback(undefined);
            },
          ),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation((cb) => {
          requestCallback = cb;
          return mockHttpServer as unknown as http.Server;
        });

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );
        await serverListeningPromise;

        // Test OAuth error without description
        const mockReq = {
          url: '/oauth2callback?error=server_error',
        } as http.IncomingMessage;
        const mockRes = {
          writeHead: vi.fn(),
          end: vi.fn(),
        } as unknown as http.ServerResponse;

        await expect(async () => {
          requestCallback(mockReq, mockRes);
          await clientPromise;
        }).rejects.toThrow(
          'Google OAuth error: server_error. No additional details provided',
        );
      });

      it('should handle unexpected requests (like /favicon.ico) without crashing', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        let requestCallback!: http.RequestListener;
        let serverListeningCallback: (value: unknown) => void;
        const serverListeningPromise = new Promise(
          (resolve) => (serverListeningCallback = resolve),
        );

        const mockHttpServer = {
          listen: vi.fn(
            (_port: number, _host: string, callback?: () => void) => {
              if (callback) callback();
              serverListeningCallback(undefined);
            },
          ),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation((cb) => {
          requestCallback = cb;
          return mockHttpServer as unknown as http.Server;
        });

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );
        await serverListeningPromise;

        // Simulate an unexpected request, like a browser requesting a favicon
        const mockReq = {
          url: '/favicon.ico',
        } as http.IncomingMessage;
        const mockRes = {
          writeHead: vi.fn(),
          end: vi.fn(),
        } as unknown as http.ServerResponse;

        await expect(async () => {
          requestCallback(mockReq, mockRes);
          await clientPromise;
        }).rejects.toThrow(
          'OAuth callback not received. Unexpected request: /favicon.ico',
        );

        // Assert that we correctly redirected to the failure page
        expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
          Location:
            'https://developers.google.com/gemini-code-assist/auth_failure_gemini',
        });
        expect(mockRes.end).toHaveBeenCalled();
      });

      it('should handle token exchange failure with descriptive error', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockCode = 'test-code';
        const mockState = 'test-state';

        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          getToken: vi
            .fn()
            .mockRejectedValue(new Error('Token exchange failed')),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        let requestCallback!: http.RequestListener;
        let serverListeningCallback: (value: unknown) => void;
        const serverListeningPromise = new Promise(
          (resolve) => (serverListeningCallback = resolve),
        );

        const mockHttpServer = {
          listen: vi.fn(
            (_port: number, _host: string, callback?: () => void) => {
              if (callback) callback();
              serverListeningCallback(undefined);
            },
          ),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation((cb) => {
          requestCallback = cb;
          return mockHttpServer as unknown as http.Server;
        });

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );
        await serverListeningPromise;

        const mockReq = {
          url: `/oauth2callback?code=${mockCode}&state=${mockState}`,
        } as http.IncomingMessage;
        const mockRes = {
          writeHead: vi.fn(),
          end: vi.fn(),
        } as unknown as http.ServerResponse;

        await expect(async () => {
          requestCallback(mockReq, mockRes);
          await clientPromise;
        }).rejects.toThrow(
          'Failed to exchange authorization code for tokens: Token exchange failed',
        );
      });

      it('should handle fetchAndCacheUserInfo failure gracefully', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockCode = 'test-code';
        const mockState = 'test-state';
        const mockTokens = {
          access_token: 'test-access-token',
          refresh_token: 'test-refresh-token',
        };

        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          getToken: vi.fn().mockResolvedValue({ tokens: mockTokens }),
          getAccessToken: vi
            .fn()
            .mockResolvedValue({ token: 'test-access-token' }),
          on: vi.fn(),
          credentials: {},
        } as unknown as OAuth2Client;
        mockOAuth2Client.setCredentials = vi
          .fn()
          .mockImplementation((creds) => {
            mockOAuth2Client.credentials = creds;
          });
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        // Mock fetch to fail
        vi.mocked(global.fetch).mockResolvedValue({
          ok: false,
          status: 500,
          statusText: 'Internal Server Error',
        } as unknown as Response);

        const consoleLogSpy = vi
          .spyOn(debugLogger, 'log')
          .mockImplementation(() => {});

        let requestCallback!: http.RequestListener;
        let serverListeningCallback: (value: unknown) => void;
        const serverListeningPromise = new Promise(
          (resolve) => (serverListeningCallback = resolve),
        );

        const mockHttpServer = {
          listen: vi.fn(
            (_port: number, _host: string, callback?: () => void) => {
              if (callback) callback();
              serverListeningCallback(undefined);
            },
          ),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        } as unknown as http.Server;
        (http.createServer as Mock).mockImplementation((cb) => {
          requestCallback = cb;
          return mockHttpServer;
        });

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );
        await serverListeningPromise;

        const mockReq = {
          url: `/oauth2callback?code=${mockCode}&state=${mockState}`,
        } as http.IncomingMessage;
        const mockRes = {
          writeHead: vi.fn(),
          end: vi.fn(),
        } as unknown as http.ServerResponse;

        requestCallback(mockReq, mockRes);
        const client = await clientPromise;

        // Authentication should succeed even if fetchAndCacheUserInfo fails
        expect(client).toBe(mockOAuth2Client);
        expect(consoleLogSpy).toHaveBeenCalledWith(
          'Failed to fetch user info:',
          500,
          'Internal Server Error',
        );

        consoleLogSpy.mockRestore();
      });

      it('should handle user code authentication failure with descriptive error', async () => {
        const mockConfigWithNoBrowser = {
          getNoBrowser: () => true,
          getProxy: () => 'http://test.proxy.com:8080',
          isBrowserLaunchSuppressed: () => true,
          isInteractive: () => true,
        } as unknown as Config;

        const mockOAuth2Client = {
          generateCodeVerifierAsync: vi.fn().mockResolvedValue({
            codeChallenge: 'test-challenge',
            codeVerifier: 'test-verifier',
          }),
          generateAuthUrl: vi.fn().mockReturnValue('https://example.com/auth'),
          getToken: vi
            .fn()
            .mockRejectedValue(new Error('Invalid authorization code')),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        const mockReadline = {
          question: vi.fn((_query, callback) => callback('invalid-code')),
          close: vi.fn(),
          on: vi.fn(),
        };
        (readline.createInterface as Mock).mockReturnValue(mockReadline);

        const consoleLogSpy = vi
          .spyOn(debugLogger, 'log')
          .mockImplementation(() => {});
        const consoleErrorSpy = vi
          .spyOn(debugLogger, 'error')
          .mockImplementation(() => {});

        await expect(
          getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfigWithNoBrowser),
        ).rejects.toThrow('Failed to authenticate with user code.');

        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Failed to authenticate with authorization code:',
          'Invalid authorization code',
        );

        consoleLogSpy.mockRestore();
        consoleErrorSpy.mockRestore();
      });
    });

    describe('cancellation', () => {
      it('should cancel when SIGINT is received', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockState = 'test-state';
        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        // Mock createServer to return a server that doesn't do anything (keeps promise pending)
        const mockHttpServer = {
          listen: vi.fn(),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation(
          () => mockHttpServer as unknown as http.Server,
        );

        // Mock process.on to capture SIGINT handler
        const processOnSpy = vi
          .spyOn(process, 'on')
          .mockImplementation(() => process);

        const processRemoveListenerSpy = vi.spyOn(process, 'removeListener');

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );

        // Wait for the SIGINT handler to be registered
        let sigIntHandler: (() => void) | undefined;
        await vi.waitFor(() => {
          const sigintCall = processOnSpy.mock.calls.find(
            (call) => call[0] === 'SIGINT',
          );
          sigIntHandler = sigintCall?.[1] as (() => void) | undefined;
          if (!sigIntHandler)
            throw new Error('SIGINT handler not registered yet');
        });

        expect(sigIntHandler).toBeDefined();

        // Trigger SIGINT
        if (sigIntHandler) {
          sigIntHandler();
        }

        await expect(clientPromise).rejects.toThrow(FatalCancellationError);
        expect(processRemoveListenerSpy).toHaveBeenCalledWith(
          'SIGINT',
          expect.any(Function),
        );

        processOnSpy.mockRestore();
        processRemoveListenerSpy.mockRestore();
      });

      it('should cancel when Ctrl+C (0x03) is received on stdin', async () => {
        const mockAuthUrl = 'https://example.com/auth';
        const mockState = 'test-state';
        const mockOAuth2Client = {
          generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        const mockHttpServer = {
          listen: vi.fn(),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation(
          () => mockHttpServer as unknown as http.Server,
        );

        // Spy on process.stdin.on to capture data handler
        const stdinOnSpy = vi
          .spyOn(process.stdin, 'on')
          .mockImplementation(() => process.stdin);

        const stdinRemoveListenerSpy = vi.spyOn(
          process.stdin,
          'removeListener',
        );

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );

        // Wait for the stdin handler to be registered
        let dataHandler: ((data: Buffer) => void) | undefined;
        await vi.waitFor(() => {
          const dataCall = stdinOnSpy.mock.calls.find(
            (call: [string | symbol, ...unknown[]]) => call[0] === 'data',
          );
          dataHandler = dataCall?.[1] as ((data: Buffer) => void) | undefined;
          if (!dataHandler) throw new Error('stdin handler not registered yet');
        });

        expect(dataHandler).toBeDefined();

        // Trigger Ctrl+C
        if (dataHandler) {
          dataHandler(Buffer.from([0x03]));
        }

        await expect(clientPromise).rejects.toThrow(FatalCancellationError);
        expect(stdinRemoveListenerSpy).toHaveBeenCalledWith(
          'data',
          expect.any(Function),
        );

        stdinOnSpy.mockRestore();
        stdinRemoveListenerSpy.mockRestore();
      });

      it('should NOT cancel when 0x03 is embedded in a multi-byte escape sequence (Ghostty/VS Code WSL false-positive)', async () => {
        // Only a lone 0x03 byte is Ctrl+C; a multi-byte escape sequence that
        // merely contains 0x03 (e.g. from Ghostty on init/resize) must not cancel.
        const stdinOnSpy = vi
          .spyOn(process.stdin, 'on')
          .mockImplementation(() => process.stdin);
        vi.spyOn(process.stdin, 'removeListener').mockImplementation(
          () => process.stdin,
        );

        const mockHttpServer = {
          listen: vi.fn(),
          close: vi.fn(),
          on: vi.fn(),
          address: () => ({ port: 3000 }),
        };
        (http.createServer as Mock).mockImplementation(
          () => mockHttpServer as unknown as http.Server,
        );
        vi.mocked(OAuth2Client).mockImplementation(
          () =>
            ({
              generateAuthUrl: vi.fn().mockReturnValue('https://example.com'),
              on: vi.fn(),
            }) as unknown as OAuth2Client,
        );
        vi.mocked(open).mockImplementation(
          async () => ({ on: vi.fn() }) as never,
        );

        const clientPromise = getOauthClient(
          AuthType.LOGIN_WITH_GOOGLE,
          mockConfig,
        );

        // Grab the registered stdin data handler
        let dataHandler: ((data: Buffer) => void) | undefined;
        await vi.waitFor(() => {
          dataHandler = stdinOnSpy.mock.calls.find(
            (c: [string | symbol, ...unknown[]]) => c[0] === 'data',
          )?.[1] as (data: Buffer) => void;
          if (!dataHandler) throw new Error('handler not registered');
        });

        // Fire an escape sequence embedding 0x03 — must NOT cancel.
        dataHandler!(Buffer.from([0x1b, 0x5b, 0x03, 0x4d])); // ESC [ 0x03 M

        // Promise must still be pending (not rejected).
        const result = await Promise.race([
          clientPromise.then(
            () => 'resolved',
            () => 'rejected',
          ),
          new Promise<string>((r) => setTimeout(() => r('pending'), 50)),
        ]);
        expect(result).toBe('pending');

        stdinOnSpy.mockRestore();
        vi.spyOn(process.stdin, 'removeListener').mockRestore();
      });

      it('should throw FatalCancellationError when consent is denied', async () => {
        vi.spyOn(coreEvents, 'emitConsentRequest').mockImplementation(
          (payload) => {
            payload.onConfirm(false);
          },
        );

        await expect(
          getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig),
        ).rejects.toThrow(FatalCancellationError);
      });
    });

    describe('clearCachedCredentialFile', () => {
      it('should clear cached credentials and Google account', async () => {
        const cachedCreds = { refresh_token: 'test-token' };
        const credsPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'oauth_creds.json',
        );
        await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
        await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));

        const googleAccountPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'google_accounts.json',
        );
        const accountData = { active: 'test@example.com', old: [] };
        await fs.promises.writeFile(
          googleAccountPath,
          JSON.stringify(accountData),
        );
        const userAccountManager = new UserAccountManager();

        expect(fs.existsSync(credsPath)).toBe(true);
        expect(fs.existsSync(googleAccountPath)).toBe(true);
        expect(userAccountManager.getCachedGoogleAccount()).toBe(
          'test@example.com',
        );

        await clearCachedCredentialFile();
        expect(fs.existsSync(credsPath)).toBe(false);
        expect(userAccountManager.getCachedGoogleAccount()).toBeNull();
        const updatedAccountData = JSON.parse(
          fs.readFileSync(googleAccountPath, 'utf-8'),
        );
        expect(updatedAccountData.active).toBeNull();
        expect(updatedAccountData.old).toContain('test@example.com');
      });

      it('should clear the in-memory OAuth client cache', async () => {
        const mockSetCredentials = vi.fn();
        const mockGetAccessToken = vi
          .fn()
          .mockResolvedValue({ token: 'test-token' });
        const mockGetTokenInfo = vi.fn().mockResolvedValue({});
        const mockOAuth2Client = {
          setCredentials: mockSetCredentials,
          getAccessToken: mockGetAccessToken,
          getTokenInfo: mockGetTokenInfo,
          on: vi.fn(),
        } as unknown as OAuth2Client;
        vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

        // Pre-populate credentials to make getOauthClient resolve quickly
        const credsPath = path.join(
          tempHomeDir,
          GEMINI_DIR,
          'oauth_creds.json',
        );
        await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
        await fs.promises.writeFile(
          credsPath,
          JSON.stringify({ refresh_token: 'token' }),
        );

        // First call, should create a client
        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);
        expect(OAuth2Client).toHaveBeenCalledTimes(1);

        // Second call, should use cached client
        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);
        expect(OAuth2Client).toHaveBeenCalledTimes(1);

        clearOauthClientCache();

        // Third call, after clearing cache, should create a new client
        await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);
        expect(OAuth2Client).toHaveBeenCalledTimes(2);
      });
    });
  });

  describe('with encrypted flag true', () => {
    let tempHomeDir: string;
    beforeEach(() => {
      process.env[FORCE_ENCRYPTED_FILE_ENV_VAR] = 'true';
      tempHomeDir = fs.mkdtempSync(
        path.join(os.tmpdir(), 'gemini-cli-test-home-'),
      );
      vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
      vi.mocked(pathsHomedir).mockReturnValue(tempHomeDir);
    });

    afterEach(() => {
      fs.rmSync(tempHomeDir, { recursive: true, force: true });
      vi.clearAllMocks();
      resetOauthClientForTesting();
      vi.unstubAllEnvs();
    });

    it('should save credentials using OAuthCredentialStorage during web login', async () => {
      const { OAuthCredentialStorage } = await import(
        './oauth-credential-storage.js'
      );
      const mockAuthUrl = 'https://example.com/auth';
      const mockCode = 'test-code';
      const mockState = 'test-state';
      const mockTokens = {
        access_token: 'test-access-token',
        refresh_token: 'test-refresh-token',
      };

      let onTokensCallback: (tokens: Credentials) => void = () => {};
      const mockOn = vi.fn((event, callback) => {
        if (event === 'tokens') {
          onTokensCallback = callback;
        }
      });

      const mockGetToken = vi.fn().mockImplementation(async () => {
        onTokensCallback(mockTokens);
        return { tokens: mockTokens };
      });

      const mockOAuth2Client = {
        generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
        getToken: mockGetToken,
        setCredentials: vi.fn(),
        getAccessToken: vi
          .fn()
          .mockResolvedValue({ token: 'mock-access-token' }),
        on: mockOn,
        credentials: mockTokens,
      } as unknown as OAuth2Client;
      vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);

      vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
      vi.mocked(open).mockImplementation(
        async () => ({ on: vi.fn() }) as never,
      );

      (global.fetch as Mock).mockResolvedValue({
        ok: true,
        json: vi
          .fn()
          .mockResolvedValue({ email: 'test-google-account@gmail.com' }),
      } as unknown as Response);

      let requestCallback!: http.RequestListener;
      let serverListeningCallback: (value: unknown) => void;
      const serverListeningPromise = new Promise(
        (resolve) => (serverListeningCallback = resolve),
      );

      let capturedPort = 0;
      const mockHttpServer = {
        listen: vi.fn((port: number, _host: string, callback?: () => void) => {
          capturedPort = port;
          if (callback) {
            callback();
          }
          serverListeningCallback(undefined);
        }),
        close: vi.fn((callback?: () => void) => {
          if (callback) {
            callback();
          }
        }),
        on: vi.fn(),
        address: () => ({ port: capturedPort }),
      };
      (http.createServer as Mock).mockImplementation((cb) => {
        requestCallback = cb as http.RequestListener;
        return mockHttpServer as unknown as http.Server;
      });

      const clientPromise = getOauthClient(
        AuthType.LOGIN_WITH_GOOGLE,
        mockConfig,
      );

      await serverListeningPromise;

      const mockReq = {
        url: `/oauth2callback?code=${mockCode}&state=${mockState}`,
      } as http.IncomingMessage;
      const mockRes = {
        writeHead: vi.fn(),
        end: vi.fn(),
      } as unknown as http.ServerResponse;

      requestCallback(mockReq, mockRes);

      await clientPromise;

      expect(
        vi.mocked(OAuthCredentialStorage.saveCredentials),
      ).toHaveBeenCalledWith(mockTokens);
      const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
      expect(fs.existsSync(credsPath)).toBe(false);
    });

    it('should load credentials using OAuthCredentialStorage and not from file', async () => {
      const { OAuthCredentialStorage } = await import(
        './oauth-credential-storage.js'
      );
      const cachedCreds = { refresh_token: 'cached-encrypted-token' };
      vi.mocked(OAuthCredentialStorage.loadCredentials).mockResolvedValue(
        cachedCreds,
      );

      // Create a dummy unencrypted credential file.
      // If the logic is correct, this file should be ignored.
      const unencryptedCreds = { refresh_token: 'unencrypted-token' };
      const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
      await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
      await fs.promises.writeFile(credsPath, JSON.stringify(unencryptedCreds));

      const mockClient = {
        setCredentials: vi.fn(),
        getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
        getTokenInfo: vi.fn().mockResolvedValue({}),
        on: vi.fn(),
      };

      vi.mocked(OAuth2Client).mockImplementation(
        () => mockClient as unknown as OAuth2Client,
      );

      await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);

      expect(
        vi.mocked(OAuthCredentialStorage.loadCredentials),
      ).toHaveBeenCalled();
      expect(mockClient.setCredentials).toHaveBeenCalledWith(cachedCreds);
      expect(mockClient.setCredentials).not.toHaveBeenCalledWith(
        unencryptedCreds,
      );
    });

    it('should clear credentials using OAuthCredentialStorage', async () => {
      const { OAuthCredentialStorage } = await import(
        './oauth-credential-storage.js'
      );

      // Create a dummy unencrypted credential file. It should not be deleted.
      const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
      await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
      await fs.promises.writeFile(credsPath, '{}');

      await clearCachedCredentialFile();

      expect(
        OAuthCredentialStorage.clearCredentials as Mock,
      ).toHaveBeenCalled();
      expect(fs.existsSync(credsPath)).toBe(true); // The unencrypted file should remain
    });
  });
});
