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

import { describe, it, expect, vi, afterEach } from 'vitest';
import {
  classifyGoogleError,
  RetryableQuotaError,
  TerminalQuotaError,
  ValidationRequiredError,
} from './googleQuotaErrors.js';
import * as errorParser from './googleErrors.js';
import type { GoogleApiError } from './googleErrors.js';
import { ModelNotFoundError } from './httpErrors.js';

describe('classifyGoogleError', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('should return original error if not a Google API error', () => {
    const regularError = new Error('Something went wrong');
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
    const result = classifyGoogleError(regularError);
    expect(result).toBe(regularError);
  });

  it('should return RetryableQuotaError when message contains "Please retry in Xs"', () => {
    const complexError = {
      error: {
        message:
          '{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s."}]}}',
        code: 429,
        status: 'Too Many Requests',
      },
    };
    const rawError = new Error(JSON.stringify(complexError));
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);

    const result = classifyGoogleError(rawError);

    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBe(44097.740004);
    expect((result as RetryableQuotaError).message).toBe(rawError.message);
  });

  it('should return RetryableQuotaError when error is a string and message contains "Please retry in Xms"', () => {
    const complexErrorString = JSON.stringify({
      error: {
        message:
          '{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms."}]}}',
        code: 429,
        status: 'Too Many Requests',
      },
    });
    const rawError = new Error(complexErrorString);
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);

    const result = classifyGoogleError(rawError);

    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(900.2);
    expect((result as RetryableQuotaError).message).toBe(rawError.message);
  });

  it('should return RetryableQuotaError for 503 Service Unavailable', () => {
    const apiError: GoogleApiError = {
      code: 503,
      message: 'Service Unavailable',
      details: [],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const originalError = new Error('Service Unavailable');
    const result = classifyGoogleError(originalError);
    expect(result).toBeInstanceOf(RetryableQuotaError);
    if (result instanceof RetryableQuotaError) {
      expect(result.cause).toBe(apiError);
      expect(result.message).toBe('Service Unavailable');
    }
  });

  it('should return RetryableQuotaError with delay for 503 Service Unavailable with RetryInfo', () => {
    const apiError: GoogleApiError = {
      code: 503,
      message:
        'No capacity available for model gemini-3.1-pro-preview on the server',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'MODEL_CAPACITY_EXHAUSTED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {
            model: 'gemini-3.1-pro-preview',
          },
        },
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '9s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBe(9000);
  });

  it('should return original error if code is not 429, 499 or 503', () => {
    const apiError: GoogleApiError = {
      code: 500,
      message: 'Server error',
      details: [],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const originalError = new Error();
    const result = classifyGoogleError(originalError);
    expect(result).toBe(originalError);
    expect(result).not.toBeInstanceOf(TerminalQuotaError);
    expect(result).not.toBeInstanceOf(RetryableQuotaError);
  });

  it('should return RetryableQuotaError for 499 Client Closed Request', () => {
    const apiError: GoogleApiError = {
      code: 499,
      message: 'Client Closed Request',
      details: [],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const originalError = new Error('Client Closed Request');
    const result = classifyGoogleError(originalError);
    expect(result).toBeInstanceOf(RetryableQuotaError);
    if (result instanceof RetryableQuotaError) {
      expect(result.cause).toBe(apiError);
      expect(result.message).toBe('Client Closed Request');
    }
  });

  it('should return TerminalQuotaError for daily quota violations in QuotaFailure', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Quota exceeded',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.QuotaFailure',
          violations: [
            {
              subject: 'user',
              description: 'daily limit',
              quotaId: 'RequestsPerDay-limit',
            },
          ],
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(TerminalQuotaError);
    expect((result as TerminalQuotaError).cause).toBe(apiError);
  });

  it('should return TerminalQuotaError for retry delays over 5 minutes', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Too many requests',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '301s', // Over 5 min threshold => terminal
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(TerminalQuotaError);
    expect((result as TerminalQuotaError).retryDelayMs).toBe(301000);
  });

  it('should return RetryableQuotaError for short retry delays', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Too many requests',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '45.123s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBe(45123);
  });

  it('should return RetryableQuotaError for per-minute quota violations in QuotaFailure', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Quota exceeded',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.QuotaFailure',
          violations: [
            {
              subject: 'user',
              description: 'per minute limit',
              quotaId: 'RequestsPerMinute-limit',
            },
          ],
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBe(60000);
  });

  it('should return RetryableQuotaError for per-minute quota violations in ErrorInfo', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Quota exceeded',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'QUOTA_EXCEEDED',
          domain: 'googleapis.com',
          metadata: {
            quota_limit: 'RequestsPerMinute_PerProject_PerUser',
          },
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBe(60000);
  });

  it('should return RetryableQuotaError for another short retry delay', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message:
        'You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits.\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 56.185908122s.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.QuotaFailure',
          violations: [
            {
              quotaMetric:
                'generativelanguage.googleapis.com/generate_content_free_tier_requests',
              quotaId: 'GenerateRequestsPerMinutePerProjectPerModel-FreeTier',
              quotaDimensions: {
                location: 'global',
                model: 'gemini-2.5-pro',
              },
              quotaValue: '2',
            },
          ],
        },
        {
          '@type': 'type.googleapis.com/google.rpc.Help',
          links: [
            {
              description: 'Learn more about Gemini API quotas',
              url: 'https://ai.google.dev/gemini-api/docs/rate-limits',
            },
          ],
        },
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '56s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBe(56000);
  });

  it('should return RetryableQuotaError for Cloud Code RATE_LIMIT_EXCEEDED with retry delay', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message:
        'You have exhausted your capacity on this model. Your quota will reset after 0s.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'RATE_LIMIT_EXCEEDED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {
            uiMessage: 'true',
            model: 'gemini-2.5-pro',
            quotaResetDelay: '539.477544ms',
            quotaResetTimeStamp: '2025-10-20T19:14:08Z',
          },
        },
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '0.539477544s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(
      539.477544,
    );
  });

  it('should return TerminalQuotaError for Cloud Code RATE_LIMIT_EXCEEDED with retry delay over 5 minutes', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message:
        'You have exhausted your capacity on this model. Your quota will reset after 10m.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'RATE_LIMIT_EXCEEDED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {
            uiMessage: 'true',
            model: 'gemini-2.5-pro',
          },
        },
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '600s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(TerminalQuotaError);
    expect((result as TerminalQuotaError).retryDelayMs).toBe(600000);
    expect((result as TerminalQuotaError).reason).toBe('RATE_LIMIT_EXCEEDED');
  });

  it('should return TerminalQuotaError for Cloud Code QUOTA_EXHAUSTED', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message:
        'You have exhausted your capacity on this model. Your quota will reset after 0s.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'QUOTA_EXHAUSTED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {
            uiMessage: 'true',
            model: 'gemini-2.5-pro',
            quotaResetDelay: '539.477544ms',
            quotaResetTimeStamp: '2025-10-20T19:14:08Z',
          },
        },
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '0.539477544s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should return TerminalQuotaError for INSUFFICIENT_G1_CREDITS_BALANCE without domain', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Resource has been exhausted (e.g. check quota).',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'INSUFFICIENT_G1_CREDITS_BALANCE',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(TerminalQuotaError);
    expect((result as TerminalQuotaError).isInsufficientCredits).toBe(true);
    expect((result as TerminalQuotaError).reason).toBe(
      'INSUFFICIENT_G1_CREDITS_BALANCE',
    );
  });

  it('should prioritize daily limit over retry info', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Quota exceeded',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.QuotaFailure',
          violations: [
            {
              subject: 'user',
              description: 'daily limit',
              quotaId: 'RequestsPerDay-limit',
            },
          ],
        },
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '10s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should return RetryableQuotaError for any 429', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Too many requests',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.DebugInfo',
          detail: 'some debug info',
          stackEntries: [],
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const originalError = new Error();
    const result = classifyGoogleError(originalError);
    expect(result).toBeInstanceOf(RetryableQuotaError);
    if (result instanceof RetryableQuotaError) {
      expect(result.retryDelayMs).toBeUndefined();
    }
  });

  it('should classify nested JSON string 404 error as ModelNotFoundError', () => {
    // Mimic the double-wrapped JSON structure seen in the user report
    const innerError = {
      error: {
        code: 404,
        message:
          'models/NOT_FOUND is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.',
        status: 'NOT_FOUND',
      },
    };
    const errorString = JSON.stringify(innerError);

    const outerErrorString = JSON.stringify({
      error: {
        message: errorString,
      },
    });
    const error = new Error(`[API Error: ${outerErrorString}]`);

    const classified = classifyGoogleError(error);
    expect(classified).toBeInstanceOf(ModelNotFoundError);
    expect((classified as ModelNotFoundError).code).toBe(404);
  });

  it('should fallback to string parsing for retry delays when details array is empty', () => {
    const errorWithEmptyDetails = {
      error: {
        code: 429,
        message: 'Resource exhausted. Please retry in 5s',
        details: [],
      },
    };

    const result = classifyGoogleError(errorWithEmptyDetails);

    expect(result).toBeInstanceOf(RetryableQuotaError);
    if (result instanceof RetryableQuotaError) {
      expect(result.retryDelayMs).toBe(5000);
      // The cause should be the parsed GoogleApiError
      expect(result.cause).toEqual({
        code: 429,
        message: 'Resource exhausted. Please retry in 5s',
        details: [],
      });
    }
  });

  it('should return TerminalQuotaError when fallback "Please retry in" delay exceeds 5 minutes', () => {
    const errorWithEmptyDetails = {
      error: {
        code: 429,
        message: 'Resource exhausted. Please retry in 400s',
        details: [],
      },
    };

    const result = classifyGoogleError(errorWithEmptyDetails);

    expect(result).toBeInstanceOf(TerminalQuotaError);
    if (result instanceof TerminalQuotaError) {
      expect(result.retryDelayMs).toBe(400000);
    }
  });

  it('should return RetryableQuotaError when retry delay is exactly 5 minutes', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Too many requests',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '300s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(RetryableQuotaError);
    expect((result as RetryableQuotaError).retryDelayMs).toBe(300000);
  });

  it('should return RetryableQuotaError without delay time for generic 429 without specific message', () => {
    const generic429 = {
      status: 429,
      message: 'Resource exhausted. No specific retry info.',
    };

    const result = classifyGoogleError(generic429);

    expect(result).toBeInstanceOf(RetryableQuotaError);
    if (result instanceof RetryableQuotaError) {
      expect(result.retryDelayMs).toBeUndefined();
    }
  });

  it('should return RetryableQuotaError without delay time for 429 with empty details and no regex match', () => {
    const errorWithEmptyDetails = {
      error: {
        code: 429,
        message: 'A generic 429 error with no retry message.',
        details: [],
      },
    };

    const result = classifyGoogleError(errorWithEmptyDetails);

    expect(result).toBeInstanceOf(RetryableQuotaError);
    if (result instanceof RetryableQuotaError) {
      expect(result.retryDelayMs).toBeUndefined();
    }
  });

  it('should return RetryableQuotaError without delay time for 429 with some detail', () => {
    const errorWithEmptyDetails = {
      error: {
        code: 429,
        message: 'A generic 429 error with no retry message.',
        details: [
          {
            '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
            reason: 'QUOTA_EXCEEDED',
            domain: 'googleapis.com',
            metadata: {
              quota_limit: '',
            },
          },
        ],
      },
    };

    const result = classifyGoogleError(errorWithEmptyDetails);

    expect(result).toBeInstanceOf(RetryableQuotaError);
    if (result instanceof RetryableQuotaError) {
      expect(result.retryDelayMs).toBeUndefined();
    }
  });

  it('should return ValidationRequiredError for 403 with VALIDATION_REQUIRED from cloudcode-pa domain', () => {
    const apiError: GoogleApiError = {
      code: 403,
      message: 'Validation required to continue.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'VALIDATION_REQUIRED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {
            validation_link: 'https://fallback.example.com/validate',
          },
        },
        {
          '@type': 'type.googleapis.com/google.rpc.Help',
          links: [
            {
              description: 'Complete validation to continue',
              url: 'https://example.com/validate',
            },
            {
              description: 'Learn more',
              url: 'https://support.google.com/accounts?p=al_alert',
            },
          ],
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(ValidationRequiredError);
    expect((result as ValidationRequiredError).validationLink).toBe(
      'https://example.com/validate',
    );
    expect((result as ValidationRequiredError).validationDescription).toBe(
      'Complete validation to continue',
    );
    expect((result as ValidationRequiredError).learnMoreUrl).toBe(
      'https://support.google.com/accounts?p=al_alert',
    );
    expect((result as ValidationRequiredError).cause).toBe(apiError);
  });

  it('should correctly parse Learn more URL when first link description contains "Learn more" text', () => {
    // This tests the real API response format where the description of the first
    // link contains "Learn more:" text, but we should use the second link's URL
    const apiError: GoogleApiError = {
      code: 403,
      message: 'Validation required to continue.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'VALIDATION_REQUIRED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {},
        },
        {
          '@type': 'type.googleapis.com/google.rpc.Help',
          links: [
            {
              description:
                'Further action is required to use this service. Navigate to the following URL to complete verification:\n\nhttps://accounts.sandbox.google.com/signin/continue?...\n\nLearn more:\n\nhttps://support.google.com/accounts?p=al_alert\n',
              url: 'https://accounts.sandbox.google.com/signin/continue?sarp=1&scc=1&continue=...',
            },
            {
              description: 'Learn more',
              url: 'https://support.google.com/accounts?p=al_alert',
            },
          ],
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(ValidationRequiredError);
    // Should get the validation link from the first link
    expect((result as ValidationRequiredError).validationLink).toBe(
      'https://accounts.sandbox.google.com/signin/continue?sarp=1&scc=1&continue=...',
    );
    // Should get the Learn more URL from the SECOND link, not the first
    expect((result as ValidationRequiredError).learnMoreUrl).toBe(
      'https://support.google.com/accounts?p=al_alert',
    );
  });

  it('should fallback to ErrorInfo metadata when Help detail is not present', () => {
    const apiError: GoogleApiError = {
      code: 403,
      message: 'Validation required.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'VALIDATION_REQUIRED',
          domain: 'staging-cloudcode-pa.googleapis.com',
          metadata: {
            validation_link: 'https://staging.example.com/validate',
          },
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(ValidationRequiredError);
    expect((result as ValidationRequiredError).validationLink).toBe(
      'https://staging.example.com/validate',
    );
    expect(
      (result as ValidationRequiredError).validationDescription,
    ).toBeUndefined();
    expect((result as ValidationRequiredError).learnMoreUrl).toBeUndefined();
  });

  it('should return original error for 403 with different reason', () => {
    const apiError: GoogleApiError = {
      code: 403,
      message: 'Access denied.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'ACCESS_DENIED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {},
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const originalError = new Error();
    const result = classifyGoogleError(originalError);
    expect(result).toBe(originalError);
    expect(result).not.toBeInstanceOf(ValidationRequiredError);
  });

  it('should find learn more link by hostname when description is different', () => {
    const apiError: GoogleApiError = {
      code: 403,
      message: 'Validation required.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'VALIDATION_REQUIRED',
          domain: 'cloudcode-pa.googleapis.com',
          metadata: {},
        },
        {
          '@type': 'type.googleapis.com/google.rpc.Help',
          links: [
            {
              description: 'Complete validation',
              url: 'https://accounts.google.com/validate',
            },
            {
              description: 'More information', // Not exactly "Learn more"
              url: 'https://support.google.com/accounts?p=al_alert',
            },
          ],
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(ValidationRequiredError);
    expect((result as ValidationRequiredError).learnMoreUrl).toBe(
      'https://support.google.com/accounts?p=al_alert',
    );
  });

  it('should return original error for 403 from non-cloudcode domain', () => {
    const apiError: GoogleApiError = {
      code: 403,
      message: 'Forbidden.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'VALIDATION_REQUIRED',
          domain: 'other.googleapis.com',
          metadata: {},
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const originalError = new Error();
    const result = classifyGoogleError(originalError);
    expect(result).toBe(originalError);
    expect(result).not.toBeInstanceOf(ValidationRequiredError);
  });

  it('should return TerminalQuotaError for Cloud Code QUOTA_EXHAUSTED with SSE-corrupted domain', () => {
    // SSE serialization can inject a trailing comma into the domain string.
    // This test verifies that the domain sanitization handles this case.
    const apiError: GoogleApiError = {
      code: 429,
      message:
        'You have exhausted your capacity on this model. Your quota will reset after 19h14m47s.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'QUOTA_EXHAUSTED',
          domain: 'cloudcode-pa.googleapis.com,',
          metadata: {
            uiMessage: 'true',
            model: 'gemini-3-flash-preview',
          },
        },
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '68940s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should return ValidationRequiredError with SSE-corrupted domain', () => {
    const apiError: GoogleApiError = {
      code: 403,
      message: 'Forbidden.',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.ErrorInfo',
          reason: 'VALIDATION_REQUIRED',
          domain: 'cloudcode-pa.googleapis.com,',
          metadata: {
            validationUrl: 'https://example.com/validate',
            validationDescription: 'Please validate',
          },
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(new Error());
    expect(result).toBeInstanceOf(ValidationRequiredError);
  });

  it('should return TerminalQuotaError when limit is 0 even if message contains "Please retry in Xs"', () => {
    const complexError = {
      error: {
        message:
          '{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0\\nPlease retry in 59.906331105s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0\\nPlease retry in 59.906331105s."}]}}',
        code: 429,
        status: 'Too Many Requests',
      },
    };
    const rawError = new Error(JSON.stringify(complexError)) as Error & {
      status?: number;
    };
    rawError.status = 429;
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);

    const result = classifyGoogleError(rawError);

    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should return TerminalQuotaError when limit is 0 even if structured RetryInfo is present', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Quota exceeded for limit: 0',
      details: [
        {
          '@type': 'type.googleapis.com/google.rpc.RetryInfo',
          retryDelay: '59s',
        },
      ],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(
      new Error('Quota exceeded for limit: 0'),
    );
    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should return TerminalQuotaError when limit is 0 and message contains actual newlines', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Quota exceeded for metric: ...\nlimit: 0, model: gemini-3-pro',
      details: [],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(
      new Error(
        'Quota exceeded for metric: ...\nlimit: 0, model: gemini-3-pro',
      ),
    );
    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should return TerminalQuotaError when limit is 0 followed by a period', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message: 'Quota exceeded for metric: ...\nlimit: 0. Please retry in 59s.',
      details: [],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(
      new Error(
        'Quota exceeded for metric: ...\nlimit: 0. Please retry in 59s.',
      ),
    );
    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should return RetryableQuotaError when limit is fractional (e.g., 0.5)', () => {
    const apiError: GoogleApiError = {
      code: 429,
      message:
        'Quota exceeded for metric: ...\nlimit: 0.5. Please retry in 59s.',
      details: [],
    };
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
    const result = classifyGoogleError(
      new Error(
        'Quota exceeded for metric: ...\nlimit: 0.5. Please retry in 59s.',
      ),
    );
    expect(result).toBeInstanceOf(RetryableQuotaError);
  });

  it('should fall back to "Model not found" for 404 error with plain object', () => {
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
    const result = classifyGoogleError({ status: 404 });
    expect(result).toBeInstanceOf(ModelNotFoundError);
    expect((result as ModelNotFoundError).message).toBe('Model not found');
  });

  it('should parse custom 404 message from plain object correctly', () => {
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
    const result = classifyGoogleError({
      status: 404,
      message: 'Custom 404 message',
    });
    expect(result).toBeInstanceOf(ModelNotFoundError);
    expect((result as ModelNotFoundError).message).toBe('Custom 404 message');
  });

  it('should classify plain object with limit: 0 message as TerminalQuotaError correctly', () => {
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
    const result = classifyGoogleError({
      status: 429,
      message: 'Quota exceeded, limit: 0',
    });
    expect(result).toBeInstanceOf(TerminalQuotaError);
  });

  it('should handle Error instances with undefined message gracefully', () => {
    const malformedError = new Error();
    delete (malformedError as { message?: string }).message;
    vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);

    const result = classifyGoogleError(malformedError);
    expect(result).toBe(malformedError); // Should return the original error without crashing
  });
});
