import {
  OCRStrategy,
  FileSources,
  EModelEndpoint,
  EImageOutputType,
  AgentCapabilities,
  defaultSocialLogins,
  validateAzureGroups,
  defaultAgentCapabilities,
} from 'librechat-data-provider';
import type { TCustomConfig } from 'librechat-data-provider';

jest.mock('@librechat/data-schemas', () => ({
  ...jest.requireActual('@librechat/data-schemas'),
  logger: {
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
  },
}));

import { AppService } from '@librechat/data-schemas';

const azureGroups = [
  {
    group: 'librechat-westus',
    apiKey: '${WESTUS_API_KEY}',
    instanceName: 'librechat-westus',
    version: '2023-12-01-preview',
    models: {
      'gpt-4-vision-preview': {
        deploymentName: 'gpt-4-vision-preview',
        version: '2024-02-15-preview',
      },
      'gpt-3.5-turbo': {
        deploymentName: 'gpt-35-turbo',
      },
      'gpt-3.5-turbo-1106': {
        deploymentName: 'gpt-35-turbo-1106',
      },
      'gpt-4': {
        deploymentName: 'gpt-4',
      },
      'gpt-4-1106-preview': {
        deploymentName: 'gpt-4-1106-preview',
      },
    },
  },
  {
    group: 'librechat-eastus',
    apiKey: '${EASTUS_API_KEY}',
    instanceName: 'librechat-eastus',
    deploymentName: 'gpt-4-turbo',
    version: '2024-02-15-preview',
    models: {
      'gpt-4-turbo': true,
    },
  } as const,
];

describe('AppService', () => {
  const mockSystemTools = {
    ExampleTool: {
      type: 'function',
      function: {
        description: 'Example tool function',
        name: 'exampleFunction',
        parameters: {
          type: 'object',
          properties: {
            param1: { type: 'string', description: 'An example parameter' },
          },
          required: ['param1'],
        },
      },
    },
  };

  beforeEach(() => {
    process.env.CDN_PROVIDER = undefined;
    jest.clearAllMocks();
  });

  it('should correctly assign process.env and initialize app config based on custom config', async () => {
    const config: Partial<TCustomConfig> = {
      registration: { socialLogins: ['testLogin'] },
      fileStrategy: 'testStrategy' as FileSources,
      balance: {
        enabled: true,
      },
    };

    const result = await AppService({ config, systemTools: mockSystemTools });

    expect(process.env.CDN_PROVIDER).toEqual('testStrategy');

    expect(result).toEqual(
      expect.objectContaining({
        config: expect.objectContaining({
          fileStrategy: 'testStrategy',
        }),
        registration: expect.objectContaining({
          socialLogins: ['testLogin'],
        }),
        fileStrategy: 'testStrategy',
        interfaceConfig: expect.objectContaining({
          endpointsMenu: true,
          modelSelect: true,
          parameters: true,
          sidePanel: true,
          presets: true,
        }),
        mcpConfig: null,
        imageOutputType: expect.any(String),
        fileConfig: undefined,
        secureImageLinks: undefined,
        balance: { enabled: true },
        filteredTools: undefined,
        includedTools: undefined,
        webSearch: expect.objectContaining({
          safeSearch: 1,
          jinaApiKey: '${JINA_API_KEY}',
          jinaApiUrl: '${JINA_API_URL}',
          cohereApiKey: '${COHERE_API_KEY}',
          serperApiKey: '${SERPER_API_KEY}',
          searxngApiKey: '${SEARXNG_API_KEY}',
          firecrawlApiKey: '${FIRECRAWL_API_KEY}',
          firecrawlApiUrl: '${FIRECRAWL_API_URL}',
          searxngInstanceUrl: '${SEARXNG_INSTANCE_URL}',
        }),
        memory: undefined,
        endpoints: expect.objectContaining({
          agents: expect.objectContaining({
            disableBuilder: false,
            capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
            maxCitations: 30,
            maxCitationsPerFile: 7,
            minRelevanceScore: 0.45,
          }),
        }),
      }),
    );
  });

  it('should change the `imageOutputType` based on config value', async () => {
    const config = {
      version: '0.10.0',
      imageOutputType: EImageOutputType.WEBP,
    };

    const result = await AppService({ config });
    expect(result).toEqual(
      expect.objectContaining({
        imageOutputType: EImageOutputType.WEBP,
      }),
    );
  });

  it('should default to `PNG` `imageOutputType` with no provided type', async () => {
    const config = {
      version: '0.10.0',
    };

    const result = await AppService({ config });
    expect(result).toEqual(
      expect.objectContaining({
        imageOutputType: EImageOutputType.PNG,
      }),
    );
  });

  it('should default to `PNG` `imageOutputType` with no provided config', async () => {
    const config = {};

    const result = await AppService({ config });
    expect(result).toEqual(
      expect.objectContaining({
        imageOutputType: EImageOutputType.PNG,
      }),
    );
  });

  it('should load and format tools accurately with defined structure', async () => {
    const config = {};

    const result = await AppService({ config, systemTools: mockSystemTools });

    // Verify tools are included in the returned config
    expect(result.availableTools).toBeDefined();
    expect(result.availableTools?.ExampleTool).toEqual({
      type: 'function',
      function: {
        description: 'Example tool function',
        name: 'exampleFunction',
        parameters: {
          type: 'object',
          properties: {
            param1: { type: 'string', description: 'An example parameter' },
          },
          required: ['param1'],
        },
      },
    });
  });

  it('should correctly configure Assistants endpoint based on custom config', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        [EModelEndpoint.assistants]: {
          disableBuilder: true,
          pollIntervalMs: 5000,
          timeoutMs: 30000,
          supportedIds: ['id1', 'id2'],
          privateAssistants: false,
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.assistants]: expect.objectContaining({
            disableBuilder: true,
            pollIntervalMs: 5000,
            timeoutMs: 30000,
            supportedIds: expect.arrayContaining(['id1', 'id2']),
            privateAssistants: false,
          }),
        }),
      }),
    );
  });

  it('should correctly configure Agents endpoint based on custom config', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        [EModelEndpoint.agents]: {
          disableBuilder: true,
          recursionLimit: 10,
          maxRecursionLimit: 20,
          allowedProviders: ['openai', 'anthropic'],
          capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.agents]: expect.objectContaining({
            disableBuilder: true,
            recursionLimit: 10,
            maxRecursionLimit: 20,
            allowedProviders: expect.arrayContaining(['openai', 'anthropic']),
            capabilities: expect.arrayContaining([
              AgentCapabilities.tools,
              AgentCapabilities.actions,
            ]),
          }),
        }),
      }),
    );
  });

  it('should configure Agents endpoint with defaults when no config is provided', async () => {
    const config = {};

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.agents]: expect.objectContaining({
            disableBuilder: false,
            capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
          }),
        }),
      }),
    );
  });

  it('should configure Agents endpoint with defaults when endpoints exist but agents is not defined', async () => {
    const config = {
      endpoints: {
        [EModelEndpoint.openAI]: {
          titleConvo: true,
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.agents]: expect.objectContaining({
            disableBuilder: false,
            capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
          }),
          [EModelEndpoint.openAI]: expect.objectContaining({
            titleConvo: true,
          }),
        }),
      }),
    );
  });

  it('should correctly configure minimum Azure OpenAI Assistant values', async () => {
    const assistantGroups = [azureGroups[0], { ...azureGroups[1], assistants: true }];
    const config = {
      endpoints: {
        [EModelEndpoint.azureOpenAI]: {
          groups: assistantGroups,
          assistants: true,
        },
      },
    };

    process.env.WESTUS_API_KEY = 'westus-key';
    process.env.EASTUS_API_KEY = 'eastus-key';

    const result = await AppService({ config });
    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.azureAssistants]: expect.objectContaining({
            capabilities: expect.arrayContaining([
              expect.any(String),
              expect.any(String),
              expect.any(String),
            ]),
          }),
        }),
      }),
    );
  });

  it('should correctly configure Azure OpenAI endpoint based on custom config', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        [EModelEndpoint.azureOpenAI]: {
          groups: azureGroups,
        },
      },
    };

    process.env.WESTUS_API_KEY = 'westus-key';
    process.env.EASTUS_API_KEY = 'eastus-key';

    const result = await AppService({ config });

    const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups);
    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.azureOpenAI]: expect.objectContaining({
            modelNames,
            modelGroupMap,
            groupMap,
          }),
        }),
      }),
    );
  });

  it('should not modify FILE_UPLOAD environment variables without rate limits', async () => {
    // Setup initial environment variables
    process.env.FILE_UPLOAD_IP_MAX = '10';
    process.env.FILE_UPLOAD_IP_WINDOW = '15';
    process.env.FILE_UPLOAD_USER_MAX = '5';
    process.env.FILE_UPLOAD_USER_WINDOW = '20';

    const initialEnv = { ...process.env };
    const config = {};

    await AppService({ config });

    // Expect environment variables to remain unchanged
    expect(process.env.FILE_UPLOAD_IP_MAX).toEqual(initialEnv.FILE_UPLOAD_IP_MAX);
    expect(process.env.FILE_UPLOAD_IP_WINDOW).toEqual(initialEnv.FILE_UPLOAD_IP_WINDOW);
    expect(process.env.FILE_UPLOAD_USER_MAX).toEqual(initialEnv.FILE_UPLOAD_USER_MAX);
    expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual(initialEnv.FILE_UPLOAD_USER_WINDOW);
  });

  it('should fallback to default FILE_UPLOAD environment variables when rate limits are unspecified', async () => {
    // Setup initial environment variables to non-default values
    process.env.FILE_UPLOAD_IP_MAX = 'initialMax';
    process.env.FILE_UPLOAD_IP_WINDOW = 'initialWindow';
    process.env.FILE_UPLOAD_USER_MAX = 'initialUserMax';
    process.env.FILE_UPLOAD_USER_WINDOW = 'initialUserWindow';
    const config = {};

    await AppService({ config });

    // Verify that process.env falls back to the initial values
    expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('initialMax');
    expect(process.env.FILE_UPLOAD_IP_WINDOW).toEqual('initialWindow');
    expect(process.env.FILE_UPLOAD_USER_MAX).toEqual('initialUserMax');
    expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual('initialUserWindow');
  });

  it('should not modify IMPORT environment variables without rate limits', async () => {
    // Setup initial environment variables
    process.env.IMPORT_IP_MAX = '10';
    process.env.IMPORT_IP_WINDOW = '15';
    process.env.IMPORT_USER_MAX = '5';
    process.env.IMPORT_USER_WINDOW = '20';

    const initialEnv = { ...process.env };
    const config = {};

    await AppService({ config });

    // Expect environment variables to remain unchanged
    expect(process.env.IMPORT_IP_MAX).toEqual(initialEnv.IMPORT_IP_MAX);
    expect(process.env.IMPORT_IP_WINDOW).toEqual(initialEnv.IMPORT_IP_WINDOW);
    expect(process.env.IMPORT_USER_MAX).toEqual(initialEnv.IMPORT_USER_MAX);
    expect(process.env.IMPORT_USER_WINDOW).toEqual(initialEnv.IMPORT_USER_WINDOW);
  });

  it('should fallback to default IMPORT environment variables when rate limits are unspecified', async () => {
    // Setup initial environment variables to non-default values
    process.env.IMPORT_IP_MAX = 'initialMax';
    process.env.IMPORT_IP_WINDOW = 'initialWindow';
    process.env.IMPORT_USER_MAX = 'initialUserMax';
    process.env.IMPORT_USER_WINDOW = 'initialUserWindow';
    const config = {};

    await AppService({ config });

    // Verify that process.env falls back to the initial values
    expect(process.env.IMPORT_IP_MAX).toEqual('initialMax');
    expect(process.env.IMPORT_IP_WINDOW).toEqual('initialWindow');
    expect(process.env.IMPORT_USER_MAX).toEqual('initialUserMax');
    expect(process.env.IMPORT_USER_WINDOW).toEqual('initialUserWindow');
  });

  it('should correctly configure endpoint with titlePrompt, titleMethod, and titlePromptTemplate', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        [EModelEndpoint.openAI]: {
          titleConvo: true,
          titleModel: 'gpt-3.5-turbo',
          titleMethod: 'structured',
          titlePrompt: 'Custom title prompt for conversation',
          titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
        },
        [EModelEndpoint.assistants]: {
          titleMethod: 'functions',
          titlePrompt: 'Generate a title for this assistant conversation',
          titlePromptTemplate: 'Assistant conversation template: {{messages}}',
        },
        [EModelEndpoint.azureOpenAI]: {
          groups: azureGroups,
          titleConvo: true,
          titleMethod: 'completion',
          titleModel: 'gpt-4',
          titlePrompt: 'Azure title prompt',
          titlePromptTemplate: 'Azure conversation: {{context}}',
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          // Check OpenAI endpoint configuration
          [EModelEndpoint.openAI]: expect.objectContaining({
            titleConvo: true,
            titleModel: 'gpt-3.5-turbo',
            titleMethod: 'structured',
            titlePrompt: 'Custom title prompt for conversation',
            titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
          }),
          // Check Assistants endpoint configuration
          [EModelEndpoint.assistants]: expect.objectContaining({
            titleMethod: 'functions',
            titlePrompt: 'Generate a title for this assistant conversation',
            titlePromptTemplate: 'Assistant conversation template: {{messages}}',
          }),
          // Check Azure OpenAI endpoint configuration
          [EModelEndpoint.azureOpenAI]: expect.objectContaining({
            titleConvo: true,
            titleMethod: 'completion',
            titleModel: 'gpt-4',
            titlePrompt: 'Azure title prompt',
            titlePromptTemplate: 'Azure conversation: {{context}}',
          }),
        }),
      }),
    );
  });

  it('should configure Agent endpoint with title generation settings', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        [EModelEndpoint.agents]: {
          disableBuilder: false,
          titleConvo: true,
          titleModel: 'gpt-4',
          titleMethod: 'structured',
          titlePrompt: 'Generate a descriptive title for this agent conversation',
          titlePromptTemplate: 'Agent conversation summary: {{content}}',
          recursionLimit: 15,
          capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
          maxCitations: 30,
          maxCitationsPerFile: 7,
          minRelevanceScore: 0.45,
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.agents]: expect.objectContaining({
            disableBuilder: false,
            titleConvo: true,
            titleModel: 'gpt-4',
            titleMethod: 'structured',
            titlePrompt: 'Generate a descriptive title for this agent conversation',
            titlePromptTemplate: 'Agent conversation summary: {{content}}',
            recursionLimit: 15,
            capabilities: expect.arrayContaining([
              AgentCapabilities.tools,
              AgentCapabilities.actions,
            ]),
          }),
        }),
      }),
    );
  });

  it('should handle missing title configuration options with defaults', async () => {
    const config = {
      endpoints: {
        [EModelEndpoint.openAI]: {
          titleConvo: true,
          // titlePrompt and titlePromptTemplate are not provided
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          [EModelEndpoint.openAI]: expect.objectContaining({
            titleConvo: true,
          }),
        }),
      }),
    );

    // Verify that optional fields are not set when not provided
    expect(result.endpoints[EModelEndpoint.openAI].titlePrompt).toBeUndefined();
    expect(result.endpoints[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined();
    expect(result.endpoints[EModelEndpoint.openAI].titleMethod).toBeUndefined();
  });

  it('should correctly configure titleEndpoint when specified', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        [EModelEndpoint.openAI]: {
          titleConvo: true,
          titleModel: 'gpt-3.5-turbo',
          titleEndpoint: EModelEndpoint.anthropic,
          titlePrompt: 'Generate a concise title',
        },
        [EModelEndpoint.agents]: {
          disableBuilder: false,
          capabilities: [AgentCapabilities.tools],
          maxCitations: 30,
          maxCitationsPerFile: 7,
          minRelevanceScore: 0.45,
          titleEndpoint: 'custom-provider',
          titleMethod: 'structured',
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          // Check OpenAI endpoint has titleEndpoint
          [EModelEndpoint.openAI]: expect.objectContaining({
            titleConvo: true,
            titleModel: 'gpt-3.5-turbo',
            titleEndpoint: EModelEndpoint.anthropic,
            titlePrompt: 'Generate a concise title',
          }),
          // Check Agents endpoint has titleEndpoint
          [EModelEndpoint.agents]: expect.objectContaining({
            titleEndpoint: 'custom-provider',
            titleMethod: 'structured',
          }),
        }),
      }),
    );
  });

  it('should correctly configure all endpoint when specified', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        all: {
          titleConvo: true,
          titleModel: 'gpt-4o-mini',
          titleMethod: 'structured',
          titlePrompt: 'Default title prompt for all endpoints',
          titlePromptTemplate: 'Default template: {{conversation}}',
          titleEndpoint: EModelEndpoint.anthropic,
          streamRate: 50,
        },
        [EModelEndpoint.openAI]: {
          titleConvo: true,
          titleModel: 'gpt-3.5-turbo',
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        // Check that 'all' endpoint config is loaded
        endpoints: expect.objectContaining({
          all: expect.objectContaining({
            titleConvo: true,
            titleModel: 'gpt-4o-mini',
            titleMethod: 'structured',
            titlePrompt: 'Default title prompt for all endpoints',
            titlePromptTemplate: 'Default template: {{conversation}}',
            titleEndpoint: EModelEndpoint.anthropic,
            streamRate: 50,
          }),
          // Check that OpenAI endpoint has its own config
          [EModelEndpoint.openAI]: expect.objectContaining({
            titleConvo: true,
            titleModel: 'gpt-3.5-turbo',
          }),
        }),
      }),
    );
  });
});

describe('AppService updating app config and issuing warnings', () => {
  let initialEnv: NodeJS.ProcessEnv;

  beforeEach(() => {
    // Store initial environment variables to restore them after each test
    initialEnv = { ...process.env };

    process.env.CDN_PROVIDER = undefined;
    jest.clearAllMocks();
  });

  afterEach(() => {
    // Restore initial environment variables
    process.env = { ...initialEnv };
  });

  it('should initialize app config with default values if config is empty', async () => {
    const config = {};

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        config: {},
        fileStrategy: FileSources.local,
        registration: expect.objectContaining({
          socialLogins: defaultSocialLogins,
        }),
        balance: expect.objectContaining({
          enabled: false,
          startBalance: undefined,
        }),
      }),
    );
  });

  it('should initialize app config with values from config', async () => {
    // Mock loadCustomConfig to return a specific config object with a complete balance config
    const config: Partial<TCustomConfig> = {
      fileStrategy: FileSources.firebase,
      registration: { socialLogins: ['testLogin'] },
      balance: {
        enabled: false,
        startBalance: 5000,
        autoRefillEnabled: true,
        refillIntervalValue: 15,
        refillIntervalUnit: 'hours',
        refillAmount: 5000,
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        config,
        fileStrategy: config.fileStrategy,
        registration: expect.objectContaining({
          socialLogins: config.registration?.socialLogins,
        }),
        balance: config.balance,
      }),
    );
  });

  it('should apply the assistants endpoint configuration correctly to app config', async () => {
    const config: Partial<TCustomConfig> = {
      endpoints: {
        assistants: {
          version: 'v2',
          retrievalModels: ['gpt-4', 'gpt-3.5-turbo'],
          capabilities: [],
          disableBuilder: true,
          pollIntervalMs: 5000,
          timeoutMs: 30000,
          supportedIds: ['id1', 'id2'],
        },
      },
    };

    const result = await AppService({ config });

    expect(result).toEqual(
      expect.objectContaining({
        endpoints: expect.objectContaining({
          assistants: expect.objectContaining({
            disableBuilder: true,
            pollIntervalMs: 5000,
            timeoutMs: 30000,
            supportedIds: ['id1', 'id2'],
          }),
        }),
      }),
    );

    // Verify excludedIds is undefined when not provided
    expect(result.endpoints.assistants.excludedIds).toBeUndefined();
  });

  it('should not parse environment variable references in OCR config', async () => {
    // Mock custom configuration with env variable references in OCR config
    const config: Partial<TCustomConfig> = {
      ocr: {
        apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
        baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
        strategy: OCRStrategy.MISTRAL_OCR,
        mistralModel: 'mistral-medium',
      },
    };

    // Set actual environment variables with different values
    process.env.OCR_API_KEY_CUSTOM_VAR_NAME = 'actual-api-key';
    process.env.OCR_BASEURL_CUSTOM_VAR_NAME = 'https://actual-ocr-url.com';

    const result = await AppService({ config });

    // Verify that the raw string references were preserved and not interpolated
    expect(result).toEqual(
      expect.objectContaining({
        ocr: expect.objectContaining({
          apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
          baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
          strategy: 'mistral_ocr',
          mistralModel: 'mistral-medium',
        }),
      }),
    );
  });

  it('should correctly configure peoplePicker permissions when specified', async () => {
    const config = {
      interface: {
        peoplePicker: {
          users: true,
          groups: true,
          roles: true,
        },
      },
    };

    const result = await AppService({ config });

    // Check that interface config includes the permissions
    expect(result).toEqual(
      expect.objectContaining({
        interfaceConfig: expect.objectContaining({
          peoplePicker: expect.objectContaining({
            users: true,
            groups: true,
            roles: true,
          }),
        }),
      }),
    );
  });
});
