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

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as path from 'node:path';
import { loadConfig } from './config.js';
import type { Settings } from './settings.js';
import {
  type ExtensionLoader,
  getCodeAssistServer,
  Config,
  ExperimentFlags,
  fetchAdminControlsOnce,
  type FetchAdminControlsResponse,
  AuthType,
  isHeadlessMode,
  FatalAuthenticationError,
  PolicyDecision,
  ApprovalMode,
  PRIORITY_YOLO_ALLOW_ALL,
  createPolicyEngineConfig,
} from '@google/gemini-cli-core';

// Mock dependencies
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    PRIORITY_YOLO_ALLOW_ALL: 998,
    Config: vi.fn().mockImplementation((params) => {
      const mockConfig = {
        ...params,
        initialize: vi.fn(),
        waitForMcpInit: vi.fn(),
        refreshAuth: vi.fn(),
        getExperiments: vi.fn().mockReturnValue({
          flags: {
            [actual.ExperimentFlags.ENABLE_ADMIN_CONTROLS]: {
              boolValue: false,
            },
          },
        }),
        getRemoteAdminSettings: vi.fn(),
        setRemoteAdminSettings: vi.fn(),
      };
      return mockConfig;
    }),
    startupProfiler: {
      flush: vi.fn(),
    },
    isHeadlessMode: vi.fn().mockReturnValue(false),
    getCodeAssistServer: vi.fn(),
    fetchAdminControlsOnce: vi.fn(),
    createPolicyEngineConfig: vi
      .fn()
      .mockImplementation(
        (_settings, mode, _defaultPoliciesDir, _interactive) => ({
          rules:
            mode === actual.ApprovalMode.YOLO
              ? [
                  {
                    toolName: '*',
                    decision: actual.PolicyDecision.ALLOW,
                    priority: actual.PRIORITY_YOLO_ALLOW_ALL,
                    modes: [actual.ApprovalMode.YOLO],
                    allowRedirection: true,
                  },
                ]
              : [
                  {
                    toolName: 'read_file',
                    decision: actual.PolicyDecision.ALLOW,
                    priority: 1.05,
                    source: 'Default: read-only.toml',
                  },
                ],
          checkers: [],
        }),
      ),
    coreEvents: {
      emitAdminSettingsChanged: vi.fn(),
    },
  };
});

vi.mock('../utils/logger.js', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));

describe('loadConfig', () => {
  const mockSettings = {} as Settings;
  const mockExtensionLoader = {} as ExtensionLoader;
  const taskId = 'test-task-id';

  beforeEach(() => {
    vi.clearAllMocks();
    vi.stubEnv('GEMINI_API_KEY', 'test-key');
  });

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

  describe('admin settings overrides', () => {
    it('should not fetch admin controls if experiment is disabled', async () => {
      await loadConfig(mockSettings, mockExtensionLoader, taskId);
      expect(fetchAdminControlsOnce).not.toHaveBeenCalled();
    });

    it('should pass clientName as a2a-server to Config', async () => {
      await loadConfig(mockSettings, mockExtensionLoader, taskId);
      expect(Config).toHaveBeenCalledWith(
        expect.objectContaining({
          clientName: 'a2a-server',
        }),
      );
    });

    describe('when admin controls experiment is enabled', () => {
      beforeEach(() => {
        // We need to cast to any here to modify the mock implementation
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (Config as any).mockImplementation((params: unknown) => {
          const mockConfig = {
            ...(params as object),
            initialize: vi.fn(),
            waitForMcpInit: vi.fn(),
            refreshAuth: vi.fn(),
            getExperiments: vi.fn().mockReturnValue({
              flags: {
                [ExperimentFlags.ENABLE_ADMIN_CONTROLS]: {
                  boolValue: true,
                },
              },
            }),
            getRemoteAdminSettings: vi.fn().mockReturnValue({}),
            setRemoteAdminSettings: vi.fn(),
          };
          return mockConfig;
        });
      });

      it('should fetch admin controls and apply them', async () => {
        const mockAdminSettings: FetchAdminControlsResponse = {
          mcpSetting: {
            mcpEnabled: false,
          },
          cliFeatureSetting: {
            extensionsSetting: {
              extensionsEnabled: false,
            },
          },
          strictModeDisabled: false,
        };
        vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);

        await loadConfig(mockSettings, mockExtensionLoader, taskId);

        expect(Config).toHaveBeenLastCalledWith(
          expect.objectContaining({
            disableYoloMode: !mockAdminSettings.strictModeDisabled,
            mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
            extensionsEnabled:
              mockAdminSettings.cliFeatureSetting?.extensionsSetting
                ?.extensionsEnabled,
          }),
        );
      });

      it('should treat unset admin settings as false when admin settings are passed', async () => {
        const mockAdminSettings: FetchAdminControlsResponse = {
          mcpSetting: {
            mcpEnabled: true,
          },
        };
        vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);

        await loadConfig(mockSettings, mockExtensionLoader, taskId);

        expect(Config).toHaveBeenLastCalledWith(
          expect.objectContaining({
            disableYoloMode: !false,
            mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
            extensionsEnabled: undefined,
          }),
        );
      });

      it('should not pass default unset admin settings when no admin settings are present', async () => {
        const mockAdminSettings: FetchAdminControlsResponse = {};
        vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);

        await loadConfig(mockSettings, mockExtensionLoader, taskId);

        expect(Config).toHaveBeenLastCalledWith(expect.objectContaining({}));
      });

      it('should fetch admin controls using the code assist server when available', async () => {
        const mockAdminSettings: FetchAdminControlsResponse = {
          mcpSetting: {
            mcpEnabled: true,
          },
          strictModeDisabled: true,
        };
        const mockCodeAssistServer = { projectId: 'test-project' };
        vi.mocked(getCodeAssistServer).mockReturnValue(
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          mockCodeAssistServer as any,
        );
        vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);

        await loadConfig(mockSettings, mockExtensionLoader, taskId);

        expect(fetchAdminControlsOnce).toHaveBeenCalledWith(
          mockCodeAssistServer,
          true,
        );
        expect(Config).toHaveBeenLastCalledWith(
          expect.objectContaining({
            disableYoloMode: !mockAdminSettings.strictModeDisabled,
            mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
            extensionsEnabled: undefined,
          }),
        );
      });
    });
  });

  it('should set customIgnoreFilePaths when CUSTOM_IGNORE_FILE_PATHS env var is present', async () => {
    const testPath = '/tmp/ignore';
    vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', testPath);
    const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
      testPath,
    ]);
  });

  it('should set customIgnoreFilePaths when settings.fileFiltering.customIgnoreFilePaths is present', async () => {
    const testPath = '/settings/ignore';
    const settings: Settings = {
      fileFiltering: {
        customIgnoreFilePaths: [testPath],
      },
    };
    const config = await loadConfig(settings, mockExtensionLoader, taskId);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
      testPath,
    ]);
  });

  it('should merge customIgnoreFilePaths from settings and env var', async () => {
    const envPath = '/env/ignore';
    const settingsPath = '/settings/ignore';
    vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', envPath);
    const settings: Settings = {
      fileFiltering: {
        customIgnoreFilePaths: [settingsPath],
      },
    };
    const config = await loadConfig(settings, mockExtensionLoader, taskId);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
      settingsPath,
      envPath,
    ]);
  });

  it('should split CUSTOM_IGNORE_FILE_PATHS using system delimiter', async () => {
    const paths = ['/path/one', '/path/two'];
    vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', paths.join(path.delimiter));
    const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual(paths);
  });

  it('should have empty customIgnoreFilePaths when both are missing', async () => {
    const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([]);
  });

  describe('policy engine configuration', () => {
    it('should merge V1 and V2 tool settings into policySettings', async () => {
      const settings: Settings = {
        allowedTools: ['v1-allowed'],
        tools: {
          allowed: ['v2-allowed'],
          exclude: ['v2-exclude'],
          core: ['v2-core'],
        },
        mcpServers: {
          test: { command: 'test', args: [] },
        },
        policyPaths: ['/path/to/policy'],
        adminPolicyPaths: ['/path/to/admin/policy'],
      };

      await loadConfig(settings, mockExtensionLoader, taskId);

      expect(createPolicyEngineConfig).toHaveBeenCalledWith(
        expect.objectContaining({
          tools: {
            core: ['v2-core'],
            exclude: ['v2-exclude'],
            allowed: ['v1-allowed'],
          },
          mcpServers: settings.mcpServers,
          policyPaths: settings.policyPaths,
          adminPolicyPaths: settings.adminPolicyPaths,
        }),
        ApprovalMode.DEFAULT,
        undefined,
        true,
      );
    });

    it('should use V2 tool settings when V1 is missing', async () => {
      const settings: Settings = {
        tools: {
          allowed: ['v2-allowed'],
        },
      };

      await loadConfig(settings, mockExtensionLoader, taskId);

      expect(createPolicyEngineConfig).toHaveBeenCalledWith(
        expect.objectContaining({
          tools: expect.objectContaining({
            allowed: ['v2-allowed'],
          }),
        }),
        ApprovalMode.DEFAULT,
        undefined,
        true,
      );
    });

    it('should use V1 tool settings when V2 is also present', async () => {
      const settings: Settings = {
        allowedTools: ['v1-allowed'],
        tools: {
          allowed: ['v2-allowed'],
        },
      };

      await loadConfig(settings, mockExtensionLoader, taskId);

      expect(createPolicyEngineConfig).toHaveBeenCalledWith(
        expect.objectContaining({
          tools: expect.objectContaining({
            allowed: ['v1-allowed'],
          }),
        }),
        ApprovalMode.DEFAULT,
        undefined,
        true,
      );
    });
  });

  describe('tool configuration', () => {
    it('should pass V1 allowedTools to Config properly', async () => {
      const settings: Settings = {
        allowedTools: ['shell', 'edit'],
      };
      await loadConfig(settings, mockExtensionLoader, taskId);
      expect(Config).toHaveBeenCalledWith(
        expect.objectContaining({
          allowedTools: ['shell', 'edit'],
        }),
      );
    });

    it('should pass V2 tools.allowed to Config properly', async () => {
      const settings: Settings = {
        tools: {
          allowed: ['shell', 'fetch'],
        },
      };
      await loadConfig(settings, mockExtensionLoader, taskId);
      expect(Config).toHaveBeenCalledWith(
        expect.objectContaining({
          allowedTools: ['shell', 'fetch'],
        }),
      );
    });

    it('should prefer V1 allowedTools over V2 tools.allowed if both present', async () => {
      const settings: Settings = {
        allowedTools: ['v1-tool'],
        tools: {
          allowed: ['v2-tool'],
        },
      };
      await loadConfig(settings, mockExtensionLoader, taskId);
      expect(Config).toHaveBeenCalledWith(
        expect.objectContaining({
          allowedTools: ['v1-tool'],
        }),
      );
    });

    it('should pass enableAgents to Config constructor', async () => {
      const settings: Settings = {
        experimental: {
          enableAgents: false,
        },
      };
      await loadConfig(settings, mockExtensionLoader, taskId);
      expect(Config).toHaveBeenCalledWith(
        expect.objectContaining({
          enableAgents: false,
        }),
      );
    });

    it('should default enableAgents to true when not provided', async () => {
      await loadConfig(mockSettings, mockExtensionLoader, taskId);
      expect(Config).toHaveBeenCalledWith(
        expect.objectContaining({
          enableAgents: true,
        }),
      );
    });

    describe('interactivity', () => {
      it('should always set interactive true', async () => {
        vi.mocked(isHeadlessMode).mockReturnValue(true);
        await loadConfig(mockSettings, mockExtensionLoader, taskId);
        expect(Config).toHaveBeenCalledWith(
          expect.objectContaining({
            interactive: true,
          }),
        );

        vi.mocked(isHeadlessMode).mockReturnValue(false);
        await loadConfig(mockSettings, mockExtensionLoader, taskId);
        expect(Config).toHaveBeenCalledWith(
          expect.objectContaining({
            interactive: true,
          }),
        );
      });

      it('should set enableInteractiveShell based on headless mode', async () => {
        vi.mocked(isHeadlessMode).mockReturnValue(false);
        await loadConfig(mockSettings, mockExtensionLoader, taskId);
        expect(Config).toHaveBeenCalledWith(
          expect.objectContaining({
            enableInteractiveShell: true,
          }),
        );

        vi.mocked(isHeadlessMode).mockReturnValue(true);
        await loadConfig(mockSettings, mockExtensionLoader, taskId);
        expect(Config).toHaveBeenCalledWith(
          expect.objectContaining({
            enableInteractiveShell: false,
          }),
        );
      });
    });

    describe('YOLO mode', () => {
      it('should enable YOLO mode and add policy rule when GEMINI_YOLO_MODE is true', async () => {
        vi.stubEnv('GEMINI_YOLO_MODE', 'true');
        await loadConfig(mockSettings, mockExtensionLoader, taskId);
        expect(Config).toHaveBeenCalledWith(
          expect.objectContaining({
            approvalMode: 'yolo',
            policyEngineConfig: expect.objectContaining({
              rules: expect.arrayContaining([
                expect.objectContaining({
                  decision: PolicyDecision.ALLOW,
                  priority: PRIORITY_YOLO_ALLOW_ALL,
                  modes: ['yolo'],
                  allowRedirection: true,
                }),
              ]),
            }),
          }),
        );
      });

      it('should use default approval mode and load default rules when GEMINI_YOLO_MODE is not true', async () => {
        vi.stubEnv('GEMINI_YOLO_MODE', 'false');
        await loadConfig(mockSettings, mockExtensionLoader, taskId);
        expect(Config).toHaveBeenCalledWith(
          expect.objectContaining({
            approvalMode: 'default',
            policyEngineConfig: expect.objectContaining({
              rules: expect.arrayContaining([
                expect.objectContaining({
                  toolName: 'read_file',
                  decision: PolicyDecision.ALLOW,
                }),
              ]),
            }),
          }),
        );
      });
    });

    describe('authentication logic', () => {
      const setupConfigMock = (refreshAuthMock: ReturnType<typeof vi.fn>) => {
        vi.mocked(Config).mockImplementation(
          (params: unknown) =>
            ({
              ...(params as object),
              initialize: vi.fn(),
              waitForMcpInit: vi.fn(),
              refreshAuth: refreshAuthMock,
              getExperiments: vi.fn().mockReturnValue({ flags: {} }),
              getRemoteAdminSettings: vi.fn(),
              setRemoteAdminSettings: vi.fn(),
            }) as unknown as Config,
        );
      };

      beforeEach(() => {
        vi.stubEnv('USE_CCPA', 'true');
        vi.stubEnv('GEMINI_API_KEY', '');
      });

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

      it('should attempt COMPUTE_ADC by default and bypass LOGIN_WITH_GOOGLE if successful', async () => {
        const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
        setupConfigMock(refreshAuthMock);

        await loadConfig(mockSettings, mockExtensionLoader, taskId);

        expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
        expect(refreshAuthMock).not.toHaveBeenCalledWith(
          AuthType.LOGIN_WITH_GOOGLE,
        );
      });

      it('should fallback to LOGIN_WITH_GOOGLE if COMPUTE_ADC fails and interactive mode is available', async () => {
        vi.mocked(isHeadlessMode).mockReturnValue(false);
        const refreshAuthMock = vi.fn().mockImplementation((authType) => {
          if (authType === AuthType.COMPUTE_ADC) {
            return Promise.reject(new Error('ADC failed'));
          }
          return Promise.resolve();
        });
        setupConfigMock(refreshAuthMock);

        await loadConfig(mockSettings, mockExtensionLoader, taskId);

        expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
        expect(refreshAuthMock).toHaveBeenCalledWith(
          AuthType.LOGIN_WITH_GOOGLE,
        );
      });

      it('should throw FatalAuthenticationError in headless mode if COMPUTE_ADC fails', async () => {
        vi.mocked(isHeadlessMode).mockReturnValue(true);

        const refreshAuthMock = vi.fn().mockImplementation((authType) => {
          if (authType === AuthType.COMPUTE_ADC) {
            return Promise.reject(new Error('ADC not found'));
          }
          return Promise.resolve();
        });
        setupConfigMock(refreshAuthMock);

        await expect(
          loadConfig(mockSettings, mockExtensionLoader, taskId),
        ).rejects.toThrow(
          'COMPUTE_ADC failed: ADC not found. (LOGIN_WITH_GOOGLE fallback skipped due to headless mode. Run in an interactive terminal to use OAuth.)',
        );

        expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
        expect(refreshAuthMock).not.toHaveBeenCalledWith(
          AuthType.LOGIN_WITH_GOOGLE,
        );
      });

      it('should include both original and fallback error when LOGIN_WITH_GOOGLE fallback fails', async () => {
        vi.mocked(isHeadlessMode).mockReturnValue(false);

        const refreshAuthMock = vi.fn().mockImplementation((authType) => {
          if (authType === AuthType.COMPUTE_ADC) {
            throw new Error('ADC failed');
          }
          if (authType === AuthType.LOGIN_WITH_GOOGLE) {
            throw new FatalAuthenticationError('OAuth failed');
          }
          return Promise.resolve();
        });
        setupConfigMock(refreshAuthMock);

        await expect(
          loadConfig(mockSettings, mockExtensionLoader, taskId),
        ).rejects.toThrow(
          'OAuth failed. The initial COMPUTE_ADC attempt also failed: ADC failed',
        );
      });
    });
  });
});
