import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { describe, expect, it, vi } from 'vitest';

import { AnthropicClient } from '../models/anthropic.js';
import { OpenAIClient } from '../models/openai.js';
import { OllamaClient } from '../models/local/ollama.js';
import { LlamaCppClient } from '../models/local/llamacpp.js';
import { GeminiClient } from '../models/gemini.js';
import { BedrockClient } from '../models/bedrock.js';
import { GitHubModelsClient } from '../models/github.js';

async function loadFactory(): Promise<typeof import('./index.js')> {
  return import('./index.js');
}

describe('createClientFromConfig', () => {
  it('creates AnthropicClient for anthropic provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'anthropic',
      model: 'claude-sonnet-4-5-20250514',
      api_key: 'sk-ant-test',
    });
    expect(client.constructor.name).toBe('AnthropicClient');
  });

  it('creates OpenAIClient for openai provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'openai',
      model: 'gpt-4o',
      api_key: 'sk-test',
    });
    expect(client.constructor.name).toBe('OpenAIClient');
  });

  it('creates OllamaClient for ollama provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'ollama',
      model: 'llama3.2:1b',
      endpoint: 'http://localhost:11434',
    });
    expect(client.constructor.name).toBe('OllamaClient');
  });

  it('creates OllamaClient with num_gpu option', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'ollama',
      model: 'llama3.2:1b',
      num_gpu: 0,
    });
    expect(client.constructor.name).toBe('OllamaClient');
  });

  it('creates LlamaCppClient for llamacpp provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'llamacpp',
      model: 'ministral-reasoning',
      endpoint: 'http://localhost:8080',
    });
    expect(client.constructor.name).toBe('LlamaCppClient');
  });

  it('defaults llamacpp endpoint to localhost:8080', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'llamacpp',
      model: 'test-model',
    });
    expect(client.constructor.name).toBe('LlamaCppClient');
  });

  it('creates GeminiClient for gemini provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'gemini',
      model: 'gemini-2.5-pro',
      api_key: 'test-key',
    });
    expect(client.constructor.name).toBe('GeminiClient');
  });

  it('throws for unknown provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    expect(() => createClientFromConfig({
      provider: 'unknown' as 'anthropic',
      model: 'test',
    })).toThrow('Unknown model provider: unknown');
  });

  it('creates OpenAIClient with OpenRouter baseURL for openrouter provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'openrouter',
      model: 'meta-llama/llama-3.1-70b',
      api_key: 'test-key',
    });
    expect(client.constructor.name).toBe('OpenAIClient');
  });

  it('creates OpenAIClient for vercel provider', async () => {
    const prev = process.env.AI_GATEWAY_API_KEY;
    process.env.AI_GATEWAY_API_KEY = 'test-key';

    try {
      const { createClientFromConfig } = await loadFactory();
      const client = createClientFromConfig({
        provider: 'vercel',
        model: 'openai/gpt-4.1',
        endpoint: 'https://ai-gateway.vercel.sh/v1',
      });
      expect(client.constructor.name).toBe('OpenAIClient');
    } finally {
      if (prev === undefined) {
        delete process.env.AI_GATEWAY_API_KEY;
      } else {
        process.env.AI_GATEWAY_API_KEY = prev;
      }
    }
  });

  it('creates OpenAIClient with Zhipu AI baseURL for zhipuai provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'zhipuai',
      model: 'glm-4.5',
      api_key: 'test-key',
    });
    expect(client.constructor.name).toBe('OpenAIClient');
  });

  it('creates OpenAIClient for zhipuai when using auth_token', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'zhipuai',
      model: 'glm-4.5',
      auth_token: 'oauth-access-token',
    });
    expect(client.constructor.name).toBe('OpenAIClient');
  });

  it('creates OpenAIClient for zhipuai when use_oauth is enabled and ZAI_API_KEY is set', async () => {
    const prev = process.env.ZAI_API_KEY;
    process.env.ZAI_API_KEY = 'zai-api-key';

    try {
      const { createClientFromConfig } = await loadFactory();
      const client = createClientFromConfig({
        provider: 'zhipuai',
        model: 'glm-4.7',
        use_oauth: true,
      });
      expect(client.constructor.name).toBe('OpenAIClient');
    } finally {
      if (prev === undefined) {
        delete process.env.ZAI_API_KEY;
      } else {
        process.env.ZAI_API_KEY = prev;
      }
    }
  });

  it('creates OpenAIClient for zhipuai using ZHIPUAI_AUTH_TOKEN env var', async () => {
    const prev = process.env.ZHIPUAI_AUTH_TOKEN;
    process.env.ZHIPUAI_AUTH_TOKEN = 'oauth-access-token';

    try {
      const { createClientFromConfig } = await loadFactory();
      const client = createClientFromConfig({
        provider: 'zhipuai',
        model: 'glm-4.5',
      });
      expect(client.constructor.name).toBe('OpenAIClient');
    } finally {
      if (prev === undefined) {
        delete process.env.ZHIPUAI_AUTH_TOKEN;
      } else {
        process.env.ZHIPUAI_AUTH_TOKEN = prev;
      }
    }
  });

  it('creates BedrockClient for bedrock provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'bedrock',
      model: 'anthropic.claude-3-sonnet',
    });
    expect(client.constructor.name).toBe('BedrockClient');
  });

  it('creates GitHubModelsClient for github provider', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'github',
      model: 'claude-sonnet-4.5',
    });
    expect(client.constructor.name).toBe('GitHubModelsClient');
  });

  it('auto-maps Anthropic model names to GitHub equivalents for github provider', async () => {
    // User might copy-paste the Anthropic model name into a github fallback block
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'github',
      model: 'claude-sonnet-4-5-20250929',
    });
    expect(client.constructor.name).toBe('GitHubModelsClient');
  });

  it('auth_mode overrides use_oauth for OpenAI', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'openai',
      model: 'gpt-4o',
      auth_mode: 'api_key',
      use_oauth: true,
      api_key: 'sk-test',
    });
    expect(client.constructor.name).toBe('OpenAIClient');
    expect((client as any).useOAuth).toBe(false);
  });

  it('auth_mode oauth selects Anthropic auth_token without requiring api_key', async () => {
    const { createClientFromConfig } = await loadFactory();
    const client = createClientFromConfig({
      provider: 'anthropic',
      model: 'claude-sonnet-4-5-20250514',
      auth_mode: 'oauth',
      auth_token: 'tok-test',
    });
    expect(client.constructor.name).toBe('AnthropicClient');
  });

  it('auth_mode api_key throws for OpenAI when no key is available', async () => {
    const prev = process.env.OPENAI_API_KEY;
    delete process.env.OPENAI_API_KEY;
    try {
      const { createClientFromConfig } = await loadFactory();
      expect(() => createClientFromConfig({
        provider: 'openai',
        model: 'gpt-4o',
        auth_mode: 'api_key',
      })).toThrow(/OpenAI API key not configured/i);
    } finally {
      if (prev === undefined) {
        delete process.env.OPENAI_API_KEY;
      } else {
        process.env.OPENAI_API_KEY = prev;
      }
    }
  });

  it('auth_mode oauth uses stored OpenAI OAuth tokens', async () => {
    const originalHome = process.env.HOME;
    const homeDir = mkdtempSync(join(tmpdir(), 'flynn-clientfactory-openai-'));
    process.env.HOME = homeDir;

    try {
      vi.resetModules();
      const { storeOpenAIAuth } = await import('../auth/openai.js');
      storeOpenAIAuth({
        access_token: 'at',
        refresh_token: 'rt',
        expires_at: Date.now() + 60_000,
        created_at: new Date().toISOString(),
      });

      const { createClientFromConfig } = await loadFactory();
      const client = createClientFromConfig({
        provider: 'openai',
        model: 'gpt-4o',
        auth_mode: 'oauth',
      });
      expect(client.constructor.name).toBe('OpenAIClient');
      expect((client as any).useOAuth).toBe(true);
    } finally {
      process.env.HOME = originalHome;
    }
  });
});

describe('anthropicToGitHubModel', () => {
  it('maps claude-sonnet-4-20250514 to claude-sonnet-4', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-sonnet-4-20250514')).toBe('claude-sonnet-4');
  });

  it('maps claude-sonnet-4-5-20250929 to claude-sonnet-4.5', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-sonnet-4-5-20250929')).toBe('claude-sonnet-4.5');
  });

  it('maps claude-opus-4-20250514 to claude-opus-4', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-opus-4-20250514')).toBe('claude-opus-4');
  });

  it('maps claude-opus-4-5-20250918 to claude-opus-4.5', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-opus-4-5-20250918')).toBe('claude-opus-4.5');
  });

  it('maps claude-opus-4-6-20250715 to claude-opus-4.6', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-opus-4-6-20250715')).toBe('claude-opus-4.6');
  });

  it('maps claude-3-5-haiku-20241022 to claude-haiku-4.5', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-3-5-haiku-20241022')).toBe('claude-haiku-4.5');
  });

  it('maps claude-haiku-4-5-20251001 to claude-haiku-4.5', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-haiku-4-5-20251001')).toBe('claude-haiku-4.5');
  });

  it('strips date suffix and converts trailing version number with dot for unknown models', async () => {
    // "claude-sonnet-5-7-20260101" → strip date → "claude-sonnet-5-7" → dot → "claude-sonnet-5.7"
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-sonnet-5-7-20260101')).toBe('claude-sonnet-5.7');
  });

  it('strips date suffix for models without sub-version', async () => {
    // "claude-sonnet-5-20260101" → strip date → "claude-sonnet-5" (no trailing -N to dot-convert)
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('claude-sonnet-5-20260101')).toBe('claude-sonnet-5');
  });

  it('returns undefined for models without date suffix', async () => {
    const { anthropicToGitHubModel } = await loadFactory();
    expect(anthropicToGitHubModel('llama3.2:1b')).toBeUndefined();
  });
});

describe('createAutoFallbackClient', () => {
  it('creates a GitHubModelsClient for anthropic provider', async () => {
    const { createAutoFallbackClient } = await loadFactory();
    const client = createAutoFallbackClient({
      provider: 'anthropic',
      model: 'claude-sonnet-4-20250514',
    });
    expect(client).toBeDefined();
    expect((client as any).constructor.name).toBe('GitHubModelsClient');
  });

  it('returns undefined for non-anthropic providers', async () => {
    const { createAutoFallbackClient } = await loadFactory();
    expect(createAutoFallbackClient({ provider: 'openai', model: 'gpt-4o' })).toBeUndefined();
    expect(createAutoFallbackClient({ provider: 'ollama', model: 'llama3.2:1b' })).toBeUndefined();
    expect(createAutoFallbackClient({ provider: 'gemini', model: 'gemini-2.5-pro' })).toBeUndefined();
  });

  it('returns undefined for unmappable anthropic models', async () => {
    const { createAutoFallbackClient } = await loadFactory();
    expect(createAutoFallbackClient({ provider: 'anthropic', model: 'custom-model' })).toBeUndefined();
  });
});
