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

import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { handleCreditsFlow } from './creditsFlowHandler.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import {
  type Config,
  type GeminiUserTier,
  makeFakeConfig,
  getG1CreditBalance,
  shouldAutoUseCredits,
  shouldShowOverageMenu,
  shouldShowEmptyWalletMenu,
  shouldLaunchBrowser,
  logBillingEvent,
  G1_CREDIT_TYPE,
  UserTierId,
} from '@google/gemini-cli-core';
import { MessageType } from '../types.js';

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    getG1CreditBalance: vi.fn(),
    shouldAutoUseCredits: vi.fn(),
    shouldShowOverageMenu: vi.fn(),
    shouldShowEmptyWalletMenu: vi.fn(),
    logBillingEvent: vi.fn(),
    openBrowserSecurely: vi.fn(),
    shouldLaunchBrowser: vi.fn().mockReturnValue(true),
  };
});

describe('handleCreditsFlow', () => {
  let mockConfig: Config;
  let mockHistoryManager: UseHistoryManagerReturn;
  let isDialogPending: React.MutableRefObject<boolean>;
  let mockSetOverageMenuRequest: ReturnType<typeof vi.fn>;
  let mockSetEmptyWalletRequest: ReturnType<typeof vi.fn>;
  let mockSetModelSwitchedFromQuotaError: ReturnType<typeof vi.fn>;
  const mockPaidTier: GeminiUserTier = {
    id: UserTierId.STANDARD,
    availableCredits: [{ creditType: G1_CREDIT_TYPE, creditAmount: '100' }],
  };

  beforeEach(() => {
    mockConfig = makeFakeConfig();
    mockHistoryManager = {
      addItem: vi.fn(),
      history: [],
      updateItem: vi.fn(),
      clearItems: vi.fn(),
      loadHistory: vi.fn(),
    };
    isDialogPending = { current: false };
    mockSetOverageMenuRequest = vi.fn();
    mockSetEmptyWalletRequest = vi.fn();
    mockSetModelSwitchedFromQuotaError = vi.fn();

    vi.spyOn(mockConfig, 'setQuotaErrorOccurred');
    vi.spyOn(mockConfig, 'setOverageStrategy');

    vi.mocked(getG1CreditBalance).mockReturnValue(100);
    vi.mocked(shouldAutoUseCredits).mockReturnValue(false);
    vi.mocked(shouldShowOverageMenu).mockReturnValue(false);
    vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(false);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  function makeArgs(
    overrides?: Partial<Parameters<typeof handleCreditsFlow>[0]>,
  ) {
    return {
      config: mockConfig,
      paidTier: mockPaidTier,
      overageStrategy: 'ask' as const,
      failedModel: 'gemini-3-pro-preview',
      fallbackModel: 'gemini-3-flash-preview',
      usageLimitReachedModel: 'all Pro models',
      resetTime: '3:45 PM',
      historyManager: mockHistoryManager,
      setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
      isDialogPending,
      setOverageMenuRequest: mockSetOverageMenuRequest,
      setEmptyWalletRequest: mockSetEmptyWalletRequest,
      ...overrides,
    };
  }

  it('should return null if credit balance is null (non-G1 user)', async () => {
    vi.mocked(getG1CreditBalance).mockReturnValue(null);
    const result = await handleCreditsFlow(makeArgs());
    expect(result).toBeNull();
  });

  it('should return null if credits are already auto-used (strategy=always)', async () => {
    vi.mocked(shouldAutoUseCredits).mockReturnValue(true);
    const result = await handleCreditsFlow(makeArgs());
    expect(result).toBeNull();
  });

  it('should show overage menu and return retry_with_credits when use_credits selected', async () => {
    vi.mocked(shouldShowOverageMenu).mockReturnValue(true);

    const flowPromise = handleCreditsFlow(makeArgs());

    // Extract the resolve callback from the setOverageMenuRequest call
    expect(mockSetOverageMenuRequest).toHaveBeenCalledOnce();
    const request = mockSetOverageMenuRequest.mock.calls[0][0];
    expect(request.failedModel).toBe('all Pro models');
    expect(request.creditBalance).toBe(100);

    // Simulate user choosing 'use_credits'
    request.resolve('use_credits');
    const result = await flowPromise;

    expect(result).toBe('retry_with_credits');
    expect(mockConfig.setOverageStrategy).toHaveBeenCalledWith('always');
    expect(logBillingEvent).toHaveBeenCalled();
  });

  it('should show overage menu and return retry_always when use_fallback selected', async () => {
    vi.mocked(shouldShowOverageMenu).mockReturnValue(true);

    const flowPromise = handleCreditsFlow(makeArgs());
    const request = mockSetOverageMenuRequest.mock.calls[0][0];
    request.resolve('use_fallback');
    const result = await flowPromise;

    expect(result).toBe('retry_always');
  });

  it('should show overage menu and return stop when stop selected', async () => {
    vi.mocked(shouldShowOverageMenu).mockReturnValue(true);

    const flowPromise = handleCreditsFlow(makeArgs());
    const request = mockSetOverageMenuRequest.mock.calls[0][0];
    request.resolve('stop');
    const result = await flowPromise;

    expect(result).toBe('stop');
  });

  it('should return stop immediately if dialog is already pending (overage)', async () => {
    vi.mocked(shouldShowOverageMenu).mockReturnValue(true);
    isDialogPending.current = true;

    const result = await handleCreditsFlow(makeArgs());
    expect(result).toBe('stop');
    expect(mockSetOverageMenuRequest).not.toHaveBeenCalled();
  });

  it('should show empty wallet menu and return stop when get_credits selected', async () => {
    vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true);

    const flowPromise = handleCreditsFlow(makeArgs());

    expect(mockSetEmptyWalletRequest).toHaveBeenCalledOnce();
    const request = mockSetEmptyWalletRequest.mock.calls[0][0];
    expect(request.failedModel).toBe('all Pro models');

    request.resolve('get_credits');
    const result = await flowPromise;

    expect(result).toBe('stop');
    expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
      expect.objectContaining({
        type: MessageType.INFO,
        text: expect.stringContaining('few minutes'),
      }),
      expect.any(Number),
    );
  });

  it('should show empty wallet menu and return retry_always when use_fallback selected', async () => {
    vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true);

    const flowPromise = handleCreditsFlow(makeArgs());
    const request = mockSetEmptyWalletRequest.mock.calls[0][0];
    request.resolve('use_fallback');
    const result = await flowPromise;

    expect(result).toBe('retry_always');
  });

  it('should return stop immediately if dialog is already pending (empty wallet)', async () => {
    vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true);
    isDialogPending.current = true;

    const result = await handleCreditsFlow(makeArgs());
    expect(result).toBe('stop');
    expect(mockSetEmptyWalletRequest).not.toHaveBeenCalled();
  });

  it('should return null if no flow conditions are met', async () => {
    vi.mocked(getG1CreditBalance).mockReturnValue(100);
    vi.mocked(shouldAutoUseCredits).mockReturnValue(false);
    vi.mocked(shouldShowOverageMenu).mockReturnValue(false);
    vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(false);

    const result = await handleCreditsFlow(makeArgs());
    expect(result).toBeNull();
  });

  it('should clear dialog state after overage menu resolves', async () => {
    vi.mocked(shouldShowOverageMenu).mockReturnValue(true);

    const flowPromise = handleCreditsFlow(makeArgs());
    expect(isDialogPending.current).toBe(true);

    const request = mockSetOverageMenuRequest.mock.calls[0][0];
    request.resolve('stop');
    await flowPromise;

    expect(isDialogPending.current).toBe(false);
    // Verify null was set to clear the request
    expect(mockSetOverageMenuRequest).toHaveBeenCalledWith(null);
  });

  it('should clear dialog state after empty wallet menu resolves', async () => {
    vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true);

    const flowPromise = handleCreditsFlow(makeArgs());
    expect(isDialogPending.current).toBe(true);

    const request = mockSetEmptyWalletRequest.mock.calls[0][0];
    request.resolve('stop');
    await flowPromise;

    expect(isDialogPending.current).toBe(false);
    expect(mockSetEmptyWalletRequest).toHaveBeenCalledWith(null);
  });

  describe('headless mode (shouldLaunchBrowser=false)', () => {
    beforeEach(() => {
      vi.mocked(shouldLaunchBrowser).mockReturnValue(false);
    });

    it('should show manage URL in history when manage selected in headless mode', async () => {
      vi.mocked(shouldShowOverageMenu).mockReturnValue(true);

      const flowPromise = handleCreditsFlow(makeArgs());
      const request = mockSetOverageMenuRequest.mock.calls[0][0];
      request.resolve('manage');
      const result = await flowPromise;

      expect(result).toBe('stop');
      expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: expect.stringContaining('Please open this URL in a browser:'),
        }),
        expect.any(Number),
      );
    });

    it('should show credits URL in history when get_credits selected in headless mode', async () => {
      vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true);

      const flowPromise = handleCreditsFlow(makeArgs());
      const request = mockSetEmptyWalletRequest.mock.calls[0][0];

      // Trigger onGetCredits callback and wait for it
      await request.onGetCredits();

      expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: expect.stringContaining('Please open this URL in a browser:'),
        }),
        expect.any(Number),
      );

      request.resolve('get_credits');
      await flowPromise;
    });
  });
});
