// ABOUTME: E2E tests for provider instance resolution and integration
// ABOUTME: Tests full flow from provider instance selection to actual API calls

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import * as fs from 'fs';
import * as path from 'path';
import { ProviderRegistry } from './registry';
import { ProviderInstanceManager } from './instance/manager';
import { ProviderCatalogManager } from './catalog/manager';
// import { Session } from '~/sessions/session';
// import { Project } from '~/projects/project';
// import { useTempLaceDir } from '~/test-utils/temp-lace-dir';
import { setupCoreTest } from '@lace/agent/test-utils/core-test-setup';
import { anthropicBaseMessagesTrap } from '@lace/agent/test-utils/anthropic-base-namespace-trap';
import type { ProviderInstancesConfig, CatalogProvider } from './catalog/types';

// Mock OpenAI-compatible server factory
function createMockOpenAIServer(baseUrl: string, expectedApiKey: string) {
  return setupServer(
    http.post(`${baseUrl}/chat/completions`, ({ request }: { request: Request }) => {
      const authHeader = request.headers.get('authorization');

      if (authHeader !== `Bearer ${expectedApiKey}`) {
        return HttpResponse.json(
          { error: { message: 'Invalid API key', type: 'invalid_request_error' } },
          { status: 401 }
        );
      }

      // Return successful mock response
      return HttpResponse.json({
        id: 'chatcmpl-test',
        object: 'chat.completion',
        created: Date.now(),
        model: 'gpt-4',
        choices: [
          {
            index: 0,
            message: {
              role: 'assistant',
              content: `OpenAI response from ${baseUrl} with key ${expectedApiKey}`,
            },
            finish_reason: 'stop',
          },
        ],
        usage: {
          prompt_tokens: 10,
          completion_tokens: 20,
          total_tokens: 30,
        },
      });
    })
  );
}

// Mock Anthropic-compatible server factory
function createMockAnthropicServer(baseUrl: string, expectedApiKey: string) {
  return setupServer(
    http.post(`${baseUrl}/messages`, ({ request }: { request: Request }) => {
      const authHeader = request.headers.get('x-api-key');

      if (authHeader !== expectedApiKey) {
        return HttpResponse.json(
          { type: 'error', error: { type: 'authentication_error', message: 'Invalid API key' } },
          { status: 401 }
        );
      }

      // Return successful mock response
      return HttpResponse.json({
        id: 'msg_test',
        type: 'message',
        role: 'assistant',
        content: [
          {
            type: 'text',
            text: `Anthropic response from ${baseUrl} with key ${expectedApiKey}`,
          },
        ],
        model: 'claude-3-sonnet-20241022',
        stop_reason: 'end_turn',
        stop_sequence: null,
        usage: {
          input_tokens: 10,
          output_tokens: 20,
        },
      });
    })
  );
}

// Mock LMStudio server factory (WebSocket-based)
function createMockLMStudioServer(baseUrl: string) {
  // LMStudio uses WebSocket connections, which are harder to mock
  // For now, we'll skip this in favor of testing the config passing
  return setupServer(
    http.get(`${baseUrl.replace('ws://', 'http://')}/v1/models`, () => {
      return HttpResponse.json({
        object: 'list',
        data: [
          {
            id: 'local-model',
            object: 'model',
            created: Date.now(),
            owned_by: 'lmstudio',
          },
        ],
      });
    })
  );
}

// Mock Ollama server factory
function createMockOllamaServer(baseUrl: string) {
  return setupServer(
    // Mock the model list endpoint for diagnostics
    http.get(`${baseUrl}/api/tags`, () => {
      return HttpResponse.json({
        models: [
          {
            name: 'llama2',
            model: 'llama2',
            modified_at: new Date().toISOString(),
            size: 3826793677,
            digest: 'fe938a131f40e6f6d40083c9f0f430a515233eb2edaa6d72eb85c50d64f2300e',
          },
        ],
      });
    }),
    // Mock the chat endpoint
    http.post(`${baseUrl}/api/chat`, () => {
      return HttpResponse.json({
        model: 'llama2',
        created_at: new Date().toISOString(),
        message: {
          role: 'assistant',
          content: `Ollama response from ${baseUrl}`,
        },
        done: true,
        total_duration: 1000000,
        load_duration: 500000,
        prompt_eval_count: 10,
        eval_count: 20,
      });
    })
  );
}

describe('Provider Instance E2E Tests', () => {
  const _tempLaceDir = setupCoreTest();
  let registry: ProviderRegistry;
  let instanceManager: ProviderInstanceManager;
  let _catalogManager: ProviderCatalogManager;

  // Mock servers for different provider endpoints
  let openaiServer1: ReturnType<typeof createMockOpenAIServer>;
  let openaiServer2: ReturnType<typeof createMockOpenAIServer>;
  let anthropicServer: ReturnType<typeof createMockAnthropicServer>;
  let lmstudioServer: ReturnType<typeof createMockLMStudioServer>;
  let ollamaServer: ReturnType<typeof createMockOllamaServer>;

  beforeEach(async () => {
    // Temp directory and persistence setup handled by setupCoreTest()

    // Create mock servers
    openaiServer1 = createMockOpenAIServer('http://mock-openai-1.test', 'test-key-1');
    openaiServer2 = createMockOpenAIServer('http://mock-openai-2.test', 'test-key-2');
    anthropicServer = createMockAnthropicServer('http://mock-anthropic.test', 'test-anthropic-key');
    lmstudioServer = createMockLMStudioServer('ws://mock-lmstudio.test:1234');
    ollamaServer = createMockOllamaServer('http://mock-ollama.test:11434');

    // Configure servers to be quiet during tests
    openaiServer1.listen({ onUnhandledRequest: 'bypass' });
    openaiServer2.listen({ onUnhandledRequest: 'bypass' });
    anthropicServer.listen({ onUnhandledRequest: 'bypass' });
    lmstudioServer.listen({ onUnhandledRequest: 'bypass' });
    ollamaServer.listen({ onUnhandledRequest: 'bypass' });

    // Setup provider catalog with test data
    _catalogManager = new ProviderCatalogManager();

    // Create test catalog data in the user-catalog directory
    const testCatalogDir = path.join(process.env.LACE_DIR!, 'user-catalog');
    fs.mkdirSync(testCatalogDir, { recursive: true });

    // OpenAI catalog
    const openaiCatalog: CatalogProvider = {
      id: 'openai',
      name: 'OpenAI',
      type: 'openai',
      default_large_model_id: 'gpt-4',
      default_small_model_id: 'gpt-3.5-turbo',
      models: [
        {
          id: 'gpt-4',
          name: 'GPT-4',
          cost_per_1m_in: 30.0,
          cost_per_1m_out: 60.0,
          context_window: 8192,
          default_max_tokens: 4096,
        },
        {
          id: 'gpt-3.5-turbo',
          name: 'GPT-3.5 Turbo',
          cost_per_1m_in: 1.5,
          cost_per_1m_out: 2.0,
          context_window: 4096,
          default_max_tokens: 4096,
        },
      ],
    };

    // Anthropic catalog
    const anthropicCatalog: CatalogProvider = {
      id: 'anthropic',
      name: 'Anthropic',
      type: 'anthropic',
      default_large_model_id: 'claude-3-opus-20240229',
      default_small_model_id: 'claude-3-haiku-20240307',
      models: [
        {
          id: 'claude-3-opus-20240229',
          name: 'Claude 3 Opus',
          cost_per_1m_in: 15.0,
          cost_per_1m_out: 75.0,
          context_window: 200000,
          default_max_tokens: 4096,
        },
        {
          id: 'claude-3-sonnet-20241022',
          name: 'Claude 3.5 Sonnet',
          cost_per_1m_in: 3.0,
          cost_per_1m_out: 15.0,
          context_window: 200000,
          default_max_tokens: 8192,
        },
      ],
    };

    // LMStudio catalog
    const lmstudioCatalog: CatalogProvider = {
      id: 'lmstudio',
      name: 'LM Studio',
      type: 'lmstudio',
      default_large_model_id: 'local-large-model',
      default_small_model_id: 'local-small-model',
      models: [
        {
          id: 'local-large-model',
          name: 'Local Large Model',
          cost_per_1m_in: 0.0,
          cost_per_1m_out: 0.0,
          context_window: 4096,
          default_max_tokens: 2048,
        },
      ],
    };

    // Ollama catalog
    const ollamaCatalog: CatalogProvider = {
      id: 'ollama',
      name: 'Ollama',
      type: 'ollama',
      default_large_model_id: 'llama2',
      default_small_model_id: 'llama2',
      models: [
        {
          id: 'llama2',
          name: 'Llama 2',
          cost_per_1m_in: 0.0,
          cost_per_1m_out: 0.0,
          context_window: 4096,
          default_max_tokens: 2048,
        },
      ],
    };

    // Write all catalogs
    fs.writeFileSync(
      path.join(testCatalogDir, 'openai.json'),
      JSON.stringify(openaiCatalog, null, 2)
    );
    fs.writeFileSync(
      path.join(testCatalogDir, 'anthropic.json'),
      JSON.stringify(anthropicCatalog, null, 2)
    );
    fs.writeFileSync(
      path.join(testCatalogDir, 'lmstudio.json'),
      JSON.stringify(lmstudioCatalog, null, 2)
    );
    fs.writeFileSync(
      path.join(testCatalogDir, 'ollama.json'),
      JSON.stringify(ollamaCatalog, null, 2)
    );

    // Setup provider instances configuration
    instanceManager = new ProviderInstanceManager();

    const testInstanceConfig: ProviderInstancesConfig = {
      version: '1.0',
      instances: {
        'openai-prod': {
          displayName: 'OpenAI Production',
          catalogProviderId: 'openai',
          endpoint: 'http://mock-openai-1.test',
          timeout: 30000,
        },
        'openai-dev': {
          displayName: 'OpenAI Development',
          catalogProviderId: 'openai',
          endpoint: 'http://mock-openai-2.test',
          timeout: 30000,
        },
        'anthropic-test': {
          displayName: 'Anthropic Test Instance',
          catalogProviderId: 'anthropic',
          endpoint: 'http://mock-anthropic.test',
          timeout: 30000,
        },
        'lmstudio-local': {
          displayName: 'Local LM Studio',
          catalogProviderId: 'lmstudio',
          endpoint: 'ws://mock-lmstudio.test:1234',
          timeout: 30000,
        },
        'ollama-local': {
          displayName: 'Local Ollama',
          catalogProviderId: 'ollama',
          endpoint: 'http://mock-ollama.test:11434',
          timeout: 30000,
        },
      },
    };

    // Save instance configuration
    const instanceConfigPath = path.join(process.env.LACE_DIR!, 'provider-instances.json');
    fs.writeFileSync(instanceConfigPath, JSON.stringify(testInstanceConfig, null, 2));

    // Save test credentials for all providers
    await instanceManager.saveCredential('openai-prod', { apiKey: 'test-key-1' });
    await instanceManager.saveCredential('openai-dev', { apiKey: 'test-key-2' });
    await instanceManager.saveCredential('anthropic-test', { apiKey: 'test-anthropic-key' });
    await instanceManager.saveCredential('lmstudio-local', { apiKey: 'not-needed-for-lmstudio' });
    await instanceManager.saveCredential('ollama-local', { apiKey: 'not-needed-for-ollama' });

    // Initialize registry
    ProviderRegistry.clearInstance();
    registry = ProviderRegistry.getInstance();
  });

  afterEach(() => {
    // Stop mock servers
    openaiServer1.close();
    openaiServer2.close();
    anthropicServer.close();
    lmstudioServer.close();
    ollamaServer.close();

    // Clear singleton after test
    ProviderRegistry.clearInstance();

    // Temp directory cleanup handled by setupCoreTest()
  });

  describe('Provider Instance Resolution', () => {
    it('should resolve provider instance to correct provider configuration', async () => {
      // Test resolving first instance
      const provider1 = await registry.createProviderFromInstance('openai-prod');
      expect(provider1).toBeDefined();
      expect(provider1.providerName).toBe('openai');

      // Test resolving second instance
      const provider2 = await registry.createProviderFromInstance('openai-dev');
      expect(provider2).toBeDefined();
      expect(provider2.providerName).toBe('openai');

      // Providers should be different instances
      expect(provider1).not.toBe(provider2);
    });

    it('should create provider with correct model when specified', async () => {
      const provider = await registry.createProviderFromInstanceAndModel('openai-prod', 'gpt-4');
      expect(provider).toBeDefined();
      expect(provider.providerName).toBe('openai');
      // Note: We'll need to verify the model is set correctly in the provider config
    });

    it('should throw error for non-existent provider instance', async () => {
      await expect(registry.createProviderFromInstance('non-existent')).rejects.toThrow(
        'Provider instance not found: non-existent'
      );
    });

    it('should throw error for instance without credentials', async () => {
      // Create instance config without credentials
      const testConfig: ProviderInstancesConfig = {
        version: '1.0',
        instances: {
          'no-creds': {
            displayName: 'No Credentials',
            catalogProviderId: 'openai',
          },
        },
      };

      const configPath = path.join(process.env.LACE_DIR!, 'provider-instances.json');
      fs.writeFileSync(configPath, JSON.stringify(testConfig, null, 2));

      // Reinitialize registry to pick up new config
      // Registry will auto-initialize when needed

      await expect(registry.createProviderFromInstance('no-creds')).rejects.toThrow(
        'No credentials found for instance: no-creds'
      );
    });
  });

  describe('Multiple OpenAI-Compatible Endpoints', () => {
    it('should route requests to correct endpoints based on instance', async () => {
      // This test verifies that different provider instances actually
      // make requests to different endpoints with different credentials

      // Create providers from different instances
      const provider1 = await registry.createProviderFromInstanceAndModel('openai-prod', 'gpt-4');
      const provider2 = await registry.createProviderFromInstanceAndModel('openai-dev', 'gpt-4');

      // Make requests with both providers
      const response1 = await provider1.createResponse(
        [{ role: 'user', content: 'Hello from prod' }],
        [],
        'claude-3-5-haiku-20241022'
      );

      const response2 = await provider2.createResponse(
        [{ role: 'user', content: 'Hello from dev' }],
        [],
        'claude-3-5-haiku-20241022'
      );

      // Verify responses contain endpoint-specific information
      expect(response1.content).toContain('http://mock-openai-1.test');
      expect(response1.content).toContain('test-key-1');

      expect(response2.content).toContain('http://mock-openai-2.test');
      expect(response2.content).toContain('test-key-2');
    });

    it('should handle authentication failures correctly', async () => {
      // Create instance with wrong credentials
      await instanceManager.saveCredential('openai-prod', { apiKey: 'wrong-key' });

      // Reinitialize registry
      // Registry will auto-initialize when needed

      const provider = await registry.createProviderFromInstanceAndModel('openai-prod', 'gpt-4');

      // This should fail with authentication error
      await expect(
        provider.createResponse([{ role: 'user', content: 'Hello' }], [], 'gpt-4')
      ).rejects.toThrow(); // Should throw due to 401 response
    });
  });

  describe('Anthropic Provider Instance Integration', () => {
    it('should route Anthropic requests to custom endpoint', async () => {
      const provider = await registry.createProviderFromInstanceAndModel(
        'anthropic-test',
        'claude-3-sonnet-20241022'
      );

      // Mock the Anthropic provider's getAnthropicClient method to verify baseURL configuration
      const anthropicProvider = provider as unknown as {
        getAnthropicClient: () => { baseURL: string; beta: unknown };
      };
      const originalGetClient = anthropicProvider.getAnthropicClient.bind(
        anthropicProvider
      ) as () => { baseURL: string; beta: unknown };
      let capturedBaseURL: string | undefined;

      anthropicProvider.getAnthropicClient = vi.fn().mockImplementation(() => {
        const client = originalGetClient();
        capturedBaseURL = client.baseURL;

        // Provider now routes through beta.messages.* — see chunk I migration.
        const stubResponse = {
          id: 'msg_test',
          type: 'message',
          role: 'assistant',
          content: [
            {
              type: 'text',
              text: `Anthropic response from ${capturedBaseURL} with key test-anthropic-key`,
            },
          ],
          model: 'claude-3-sonnet-20241022',
          stop_reason: 'end_turn',
          stop_sequence: null,
          usage: {
            input_tokens: 10,
            output_tokens: 20,
          },
        };
        // Throwing trap on base namespace — provider must call beta.messages.*
        (client as { messages: unknown }).messages = anthropicBaseMessagesTrap();
        (client as { beta: unknown }).beta = {
          messages: {
            create: vi.fn().mockResolvedValue(stubResponse),
            stream: vi.fn(),
            countTokens: vi.fn().mockResolvedValue({ input_tokens: 100 }),
          },
        };

        return client as { baseURL: string; beta: unknown };
      }) as () => { baseURL: string; beta: unknown };

      const response = await provider.createResponse(
        [{ role: 'user', content: 'Hello Anthropic' }],
        [],
        'claude-3-5-haiku-20241022'
      );

      // Verify the provider was configured with the correct baseURL
      expect(capturedBaseURL).toBe('http://mock-anthropic.test');

      // Verify response contains endpoint-specific information
      expect(response.content).toContain('Anthropic response from http://mock-anthropic.test');
      expect(response.content).toContain('test-anthropic-key');
    });

    it('should handle Anthropic authentication failures', async () => {
      // Create instance with wrong credentials
      await instanceManager.saveCredential('anthropic-test', { apiKey: 'wrong-anthropic-key' });

      // Reinitialize registry
      // Registry will auto-initialize when needed

      const provider = await registry.createProviderFromInstanceAndModel(
        'anthropic-test',
        'claude-3-sonnet-20241022'
      );

      // Mock the Anthropic provider to simulate auth failure
      const anthropicProvider = provider as unknown as {
        getAnthropicClient: () => { messages: unknown; beta: unknown };
      };
      anthropicProvider.getAnthropicClient = vi.fn().mockImplementation(() => ({
        // Throwing trap on base namespace — provider must call beta.messages.*
        messages: anthropicBaseMessagesTrap(),
        beta: {
          messages: {
            create: vi.fn().mockRejectedValue(new Error('Authentication failed: Invalid API key')),
            stream: vi.fn(),
            countTokens: vi.fn().mockResolvedValue({ input_tokens: 100 }),
          },
        },
      }));

      // This should fail with authentication error
      await expect(
        provider.createResponse([{ role: 'user', content: 'Hello' }], [], 'gpt-4')
      ).rejects.toThrow('Authentication failed: Invalid API key');
    });
  });

  describe('Local Provider Instance Integration', () => {
    it('should configure LMStudio with custom endpoint', async () => {
      const provider = await registry.createProviderFromInstanceAndModel(
        'lmstudio-local',
        'local-large-model'
      );

      // Verify provider is configured correctly
      expect(provider.providerName).toBe('lmstudio');
      // LMStudio uses WebSocket connections which are harder to test end-to-end
      // But we can verify the provider was created successfully
    });

    it('should configure Ollama with custom endpoint', async () => {
      const provider = await registry.createProviderFromInstanceAndModel('ollama-local', 'llama2');

      const response = await provider.createResponse(
        [{ role: 'user', content: 'Hello Ollama' }],
        [],
        'llama2'
      );

      // Verify response contains endpoint-specific information
      expect(response.content).toContain('Ollama response from http://mock-ollama.test:11434');
    });
  });

  describe('Cross-Provider Instance Testing', () => {
    it('should handle multiple different provider types simultaneously', async () => {
      // Create providers from different provider types
      const openaiProvider = await registry.createProviderFromInstanceAndModel(
        'openai-prod',
        'gpt-4'
      );
      const anthropicProvider = await registry.createProviderFromInstanceAndModel(
        'anthropic-test',
        'claude-3-sonnet-20241022'
      );
      const ollamaProvider = await registry.createProviderFromInstanceAndModel(
        'ollama-local',
        'llama2'
      );

      // Mock the Anthropic provider to avoid MSW/AbortSignal issues
      const anthropicProviderAny = anthropicProvider as unknown as {
        getAnthropicClient: () => { beta: unknown };
      };
      const originalGetClient = anthropicProviderAny.getAnthropicClient.bind(
        anthropicProviderAny
      ) as () => { beta: unknown };
      anthropicProviderAny.getAnthropicClient = vi.fn().mockImplementation(() => {
        const client = originalGetClient();
        // Throwing trap on base namespace — provider must call beta.messages.*
        (client as { messages: unknown }).messages = anthropicBaseMessagesTrap();
        (client as { beta: unknown }).beta = {
          messages: {
            create: vi.fn().mockResolvedValue({
              id: 'msg_test',
              type: 'message',
              role: 'assistant',
              content: [
                {
                  type: 'text',
                  text: 'Anthropic response from http://mock-anthropic.test with key test-anthropic-key',
                },
              ],
              model: 'claude-3-sonnet-20241022',
              stop_reason: 'end_turn',
              stop_sequence: null,
              usage: {
                input_tokens: 10,
                output_tokens: 20,
              },
            }),
            stream: vi.fn(),
            countTokens: vi.fn().mockResolvedValue({ input_tokens: 100 }),
          },
        };
        return client as { beta: unknown };
      }) as () => { beta: unknown };

      // Make concurrent requests
      const [openaiResponse, anthropicResponse, ollamaResponse] = await Promise.all([
        openaiProvider.createResponse([{ role: 'user', content: 'Hello OpenAI' }], [], 'gpt-4o'),
        anthropicProvider.createResponse(
          [{ role: 'user', content: 'Hello Anthropic' }],
          [],
          'claude-3-5-haiku-20241022'
        ),
        ollamaProvider.createResponse([{ role: 'user', content: 'Hello Ollama' }], [], 'llama2'),
      ]);

      // Verify each provider used the correct endpoint
      expect(openaiResponse.content).toContain('OpenAI response from http://mock-openai-1.test');
      expect(anthropicResponse.content).toContain(
        'Anthropic response from http://mock-anthropic.test'
      );
      expect(ollamaResponse.content).toContain(
        'Ollama response from http://mock-ollama.test:11434'
      );
    });

    it('should validate models exist in respective catalogs', async () => {
      // Try to create provider with wrong model for provider type
      await expect(
        registry.createProviderFromInstanceAndModel('openai-prod', 'claude-3-sonnet-20241022')
      ).rejects.toThrow(
        'Model not found in catalog: claude-3-sonnet-20241022 for instance openai-prod (provider openai)'
      );

      await expect(
        registry.createProviderFromInstanceAndModel('anthropic-test', 'gpt-4')
      ).rejects.toThrow(
        'Model not found in catalog: gpt-4 for instance anthropic-test (provider anthropic)'
      );
    });
  });

  describe('Error Scenario Tests', () => {
    describe('Network Failures', () => {
      it('should handle connection timeouts gracefully', async () => {
        // Use a simple mock approach instead of real network timeouts to make tests faster
        const provider = await registry.createProviderFromInstanceAndModel('openai-prod', 'gpt-4');

        // Mock the OpenAI provider to simulate connection timeout
        const openaiProvider = provider as { createResponse: unknown };
        openaiProvider.createResponse = vi
          .fn()
          .mockRejectedValue(new Error('Connection timeout: fetch failed'));

        await expect(
          provider.createResponse([{ role: 'user', content: 'Hello' }], [], 'gpt-4')
        ).rejects.toThrow(/timeout|fetch failed/i);
      });

      it('should handle DNS resolution failures', async () => {
        const testConfig: ProviderInstancesConfig = {
          version: '1.0',
          instances: {
            'dns-fail-test': {
              displayName: 'DNS Fail Test',
              catalogProviderId: 'openai',
              endpoint: 'http://definitely-not-a-real-domain-12345.example',
              timeout: 2000, // Shorter timeout for faster test
            },
          },
        };

        const configPath = path.join(process.env.LACE_DIR!, 'provider-instances.json');
        fs.writeFileSync(configPath, JSON.stringify(testConfig, null, 2));
        await instanceManager.saveCredential('dns-fail-test', { apiKey: 'test-key' });
        // Registry will auto-initialize when needed

        const provider = await registry.createProviderFromInstanceAndModel(
          'dns-fail-test',
          'gpt-4'
        );

        // As above: the SDK wraps the DNS failure in a retryable
        // APIConnectionError, so cap retries — this asserts the surfaced error,
        // not the retry schedule.
        provider.RETRY_CONFIG = { maxRetries: 1 };

        await expect(
          provider.createResponse([{ role: 'user', content: 'Hello' }], [], 'gpt-4')
        ).rejects.toThrow(/getaddrinfo ENOTFOUND|fetch failed|Connection error/i);
      }, 8000); // Increase timeout for network tests
    });

    describe('Authentication Edge Cases', () => {
      it('should handle authentication failures with mock', async () => {
        // Use the existing OpenAI mock approach to avoid MSW conflicts
        const provider = await registry.createProviderFromInstanceAndModel('openai-prod', 'gpt-4');

        // Mock the OpenAI provider to simulate authentication failure
        const openaiProvider = provider as { createResponse: unknown };
        const _originalCreateResponse = openaiProvider.createResponse;
        openaiProvider.createResponse = vi
          .fn()
          .mockRejectedValue(new Error('Authentication failed: Invalid API key'));

        await expect(
          provider.createResponse([{ role: 'user', content: 'Hello' }], [], 'gpt-4')
        ).rejects.toThrow(/Authentication failed: Invalid API key/i);
      });
    });

    describe('Configuration Errors', () => {
      it('should handle invalid endpoint URLs', async () => {
        // Use a valid URL format but non-existent domain to avoid URL validation issues
        const testConfig: ProviderInstancesConfig = {
          version: '1.0',
          instances: {
            'invalid-url-test': {
              displayName: 'Invalid URL Test',
              catalogProviderId: 'openai',
              endpoint: 'http://invalid-domain-does-not-exist.test',
              timeout: 2000, // Short timeout for faster test
            },
          },
        };

        const configPath = path.join(process.env.LACE_DIR!, 'provider-instances.json');
        fs.writeFileSync(configPath, JSON.stringify(testConfig, null, 2));
        await instanceManager.saveCredential('invalid-url-test', { apiKey: 'test-key' });
        // Registry will auto-initialize when needed

        const provider = await registry.createProviderFromInstanceAndModel(
          'invalid-url-test',
          'gpt-4'
        );

        // A connection failure is retryable — the SDK wraps ENOTFOUND in an
        // APIConnectionError, and treating that class as permanent is what let a
        // transient blip fail a turn outright. This test is about the error the
        // caller finally sees, not about retry policy, so cap the retries rather
        // than let the default schedule (10 attempts, exponential backoff) decide
        // how long the assertion takes.
        provider.RETRY_CONFIG = { maxRetries: 1 };

        await expect(
          provider.createResponse([{ role: 'user', content: 'Hello' }], [], 'gpt-4')
        ).rejects.toThrow(/fetch failed|ENOTFOUND|Connection error/i);
      }, 6000);

      it('should handle missing catalog provider', async () => {
        const testConfig: ProviderInstancesConfig = {
          version: '1.0',
          instances: {
            'missing-catalog-test': {
              displayName: 'Missing Catalog Test',
              catalogProviderId: 'non-existent-provider',
              endpoint: 'http://test.example',
              timeout: 5000,
            },
          },
        };

        const configPath = path.join(process.env.LACE_DIR!, 'provider-instances.json');
        fs.writeFileSync(configPath, JSON.stringify(testConfig, null, 2));
        await instanceManager.saveCredential('missing-catalog-test', { apiKey: 'test-key' });
        // Registry will auto-initialize when needed

        await expect(
          registry.createProviderFromInstanceAndModel('missing-catalog-test', 'gpt-4')
        ).rejects.toThrow(/catalog.*provider.*not.*found|non-existent-provider/i);
      });
    });
  });

  describe.skip('Real Agent Integration', () => {
    // TODO: These tests require updating Session.spawnAgent to support provider instances
    // Will be implemented after fixing the spawnAgent method

    it.skip('should spawn agent with provider instance via API', () => {
      // Test the full flow: UI selection -> API -> Agent creation -> Provider usage
      expect(true).toBe(true); // Placeholder for when implementation is complete
    });

    it.skip('should send messages through correct provider endpoint', () => {
      // Test that agent messages actually go through the provider instance endpoint
      expect(true).toBe(true); // Placeholder for when implementation is complete
    });
  });
});
