import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProviderRegistry } from '../src/providers/registry.js';
import { AnthropicProvider } from '../src/providers/anthropic.js';
import type { LlmProvider, CompletionOptions, CompletionResult } from '../src/providers/types.js';

function mockProvider(name: string): LlmProvider {
  return {
    name,
    complete: vi.fn(async (): Promise<CompletionResult> => ({
      content: `Response from ${name}`,
      toolCalls: [],
      usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
      finishReason: 'stop',
    })),
    listModels: vi.fn(async () => [`${name}-model-1`, `${name}-model-2`]),
    isAvailable: vi.fn(async () => true),
  };
}

describe('ProviderRegistry', () => {
  let registry: ProviderRegistry;

  beforeEach(() => {
    registry = new ProviderRegistry();
  });

  it('starts with no providers', () => {
    expect(registry.list()).toEqual([]);
    expect(registry.getActive()).toBeNull();
    expect(registry.getActiveName()).toBeNull();
  });

  it('registers a provider and sets it as active', () => {
    const openai = mockProvider('openai');
    registry.register(openai);

    expect(registry.list()).toEqual(['openai']);
    expect(registry.getActive()).toBe(openai);
    expect(registry.getActiveName()).toBe('openai');
  });

  it('first registered provider becomes active', () => {
    registry.register(mockProvider('openai'));
    registry.register(mockProvider('anthropic'));

    expect(registry.getActiveName()).toBe('openai');
    expect(registry.list()).toEqual(['openai', 'anthropic']);
  });

  it('switches active provider', () => {
    registry.register(mockProvider('openai'));
    registry.register(mockProvider('anthropic'));

    registry.setActive('anthropic');
    expect(registry.getActiveName()).toBe('anthropic');
  });

  it('throws when setting unknown provider as active', () => {
    expect(() => registry.setActive('unknown')).toThrow("Provider 'unknown' is not registered");
  });

  it('gets provider by name', () => {
    const openai = mockProvider('openai');
    registry.register(openai);

    expect(registry.get('openai')).toBe(openai);
    expect(registry.get('unknown')).toBeUndefined();
  });

  it('unregisters a provider', () => {
    registry.register(mockProvider('openai'));
    registry.register(mockProvider('anthropic'));

    registry.unregister('openai');
    expect(registry.list()).toEqual(['anthropic']);
    // Active should switch to remaining provider
    expect(registry.getActiveName()).toBe('anthropic');
  });

  it('unregistering active provider switches to next available', () => {
    registry.register(mockProvider('openai'));
    registry.register(mockProvider('anthropic'));
    registry.setActive('openai');

    registry.unregister('openai');
    expect(registry.getActiveName()).toBe('anthropic');
  });

  it('unregistering last provider clears active', () => {
    registry.register(mockProvider('openai'));
    registry.unregister('openai');

    expect(registry.getActive()).toBeNull();
    expect(registry.getActiveName()).toBeNull();
  });

  it('active provider can complete', async () => {
    const provider = mockProvider('openai');
    registry.register(provider);

    const active = registry.getActive()!;
    const result = await active.complete({
      messages: [{ role: 'user', content: 'Hello' }],
    });

    expect(result.content).toBe('Response from openai');
    expect(result.finishReason).toBe('stop');
    expect(provider.complete).toHaveBeenCalled();
  });

  it('active provider can list models', async () => {
    registry.register(mockProvider('anthropic'));

    const active = registry.getActive()!;
    const models = await active.listModels();

    expect(models).toEqual(['anthropic-model-1', 'anthropic-model-2']);
  });

  describe('tier management', () => {
    it('assigns providers to tiers', () => {
      registry.register(mockProvider('vllm'));
      registry.register(mockProvider('gemini'));

      registry.assignTier('vllm', 'fast');
      registry.assignTier('gemini', 'heavy');

      expect(registry.getTierProviders('fast')).toEqual(['vllm']);
      expect(registry.getTierProviders('heavy')).toEqual(['gemini']);
      expect(registry.hasTierConfig()).toBe(true);
    });

    it('getProvider returns tier-specific provider', () => {
      const vllm = mockProvider('vllm');
      const gemini = mockProvider('gemini');
      registry.register(vllm);
      registry.register(gemini);

      registry.assignTier('vllm', 'fast');
      registry.assignTier('gemini', 'heavy');

      expect(registry.getProvider('fast')).toBe(vllm);
      expect(registry.getProvider('heavy')).toBe(gemini);
    });

    it('getProvider falls back to other tier', () => {
      const vllm = mockProvider('vllm');
      registry.register(vllm);

      registry.assignTier('vllm', 'fast');

      // Requesting heavy but only fast exists → falls back to fast
      expect(registry.getProvider('heavy')).toBe(vllm);
    });

    it('getProvider falls back to getActive when no tiers', () => {
      const openai = mockProvider('openai');
      registry.register(openai);

      // No tier assignments → falls back to legacy getActive()
      expect(registry.getProvider('fast')).toBe(openai);
      expect(registry.getProvider('heavy')).toBe(openai);
      expect(registry.hasTierConfig()).toBe(false);
    });

    it('unregister removes from tier assignments', () => {
      registry.register(mockProvider('vllm'));
      registry.register(mockProvider('gemini'));

      registry.assignTier('vllm', 'fast');
      registry.assignTier('gemini', 'heavy');

      registry.unregister('vllm');

      expect(registry.getTierProviders('fast')).toEqual([]);
      expect(registry.getTierProviders('heavy')).toEqual(['gemini']);
    });

    it('assignTier throws for unregistered provider', () => {
      expect(() => registry.assignTier('unknown', 'fast')).toThrow("Provider 'unknown' is not registered");
    });

    it('multiple providers in same tier uses first', () => {
      const vllm = mockProvider('vllm');
      const ollama = mockProvider('ollama');
      registry.register(vllm);
      registry.register(ollama);

      registry.assignTier('vllm', 'fast');
      registry.assignTier('ollama', 'fast');

      expect(registry.getProvider('fast')).toBe(vllm);
      expect(registry.getTierProviders('fast')).toEqual(['vllm', 'ollama']);
    });

    it('listProviders includes tier info', () => {
      registry.register(mockProvider('vllm'));
      registry.register(mockProvider('gemini'));

      registry.assignTier('vllm', 'fast');
      registry.assignTier('gemini', 'heavy');

      const providers = registry.listProviders();
      expect(providers).toEqual([
        { name: 'vllm', tiers: ['fast'] },
        { name: 'gemini', tiers: ['heavy'] },
      ]);
    });

    it('disposeAll calls dispose on all providers', () => {
      const disposeFn = vi.fn();
      const provider = { ...mockProvider('test'), dispose: disposeFn };
      registry.register(provider);

      registry.disposeAll();

      expect(disposeFn).toHaveBeenCalledOnce();
    });
  });
});

vi.mock('node:https', () => {
  let capturedOpts: Record<string, unknown> = {};
  const mockRequest = vi.fn((opts: unknown, cb?: unknown) => {
    capturedOpts = opts as Record<string, unknown>;
    const mockRes = {
      on: (event: string, handler: (data?: unknown) => void) => {
        if (event === 'data') {
          handler(Buffer.from(JSON.stringify({
            content: [{ type: 'text', text: 'ok' }],
            usage: { input_tokens: 1, output_tokens: 1 },
            stop_reason: 'end_turn',
          })));
        }
        if (event === 'end') handler();
        return mockRes;
      },
    };
    if (typeof cb === 'function') cb(mockRes);
    return {
      on: vi.fn().mockReturnThis(),
      write: vi.fn(),
      end: vi.fn(),
    };
  });
  return {
    default: { request: mockRequest },
    __capturedOpts: () => capturedOpts,
    __mockRequest: mockRequest,
  };
});

describe('AnthropicProvider auth headers', () => {
  it('uses Authorization: Bearer for OAuth tokens', async () => {
    const { __capturedOpts } = await import('node:https') as unknown as { __capturedOpts: () => Record<string, unknown> };
    const provider = new AnthropicProvider({ apiKey: 'sk-ant-oat01-test-token' });
    await provider.complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 1 });

    const headers = __capturedOpts().headers as Record<string, string>;
    expect(headers['Authorization']).toBe('Bearer sk-ant-oat01-test-token');
    expect(headers['x-api-key']).toBeUndefined();
  });

  it('uses x-api-key for standard API keys', async () => {
    const { __capturedOpts } = await import('node:https') as unknown as { __capturedOpts: () => Record<string, unknown> };
    const provider = new AnthropicProvider({ apiKey: 'sk-ant-api03-standard-key' });
    await provider.complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 1 });

    const headers = __capturedOpts().headers as Record<string, string>;
    expect(headers['x-api-key']).toBe('sk-ant-api03-standard-key');
    expect(headers['Authorization']).toBeUndefined();
  });

  it('includes claude-sonnet-4-5 in model list', async () => {
    const provider = new AnthropicProvider({ apiKey: 'test' });
    const models = await provider.listModels();
    expect(models).toContain('claude-sonnet-4-5-20250514');
    expect(models).toContain('claude-opus-4-20250514');
    expect(models).toContain('claude-haiku-3-5-20241022');
  });
});
