import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
import { createMockLogger } from '../../__test-utils__/mockFactories.js';

const { default: ModelsService } = await import('../modelsService.js');

describe('ModelsService', () => {
  let service;
  let logger;
  let originalFetch;

  beforeEach(() => {
    jest.useFakeTimers();
    logger = createMockLogger();
    service = new ModelsService({ backend: { baseUrl: 'https://test-api.example.com' } }, logger);
    originalFetch = global.fetch;
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.useRealTimers();
    global.fetch = originalFetch;
    if (service.retryTimer) clearTimeout(service.retryTimer);
  });

  test('constructor sets backend URL from config', () => {
    expect(service.backendUrl).toBe('https://test-api.example.com');
  });

  test('constructor uses fallback URL', () => {
    const s = new ModelsService({}, logger);
    expect(s.backendUrl).toBe('http://localhost:8080');
  });

  test('constructor uses backendUrl config fallback', () => {
    const s = new ModelsService({ backendUrl: 'http://custom:9090' }, logger);
    expect(s.backendUrl).toBe('http://custom:9090');
  });

  test('setApiKeyManager stores the manager', () => {
    const mockManager = { getKeysForRequest: jest.fn() };
    service.setApiKeyManager(mockManager);
    expect(service.apiKeyManager).toBe(mockManager);
  });

  test('getAvailableModelNames returns empty when no models', () => {
    const names = service.getAvailableModelNames();
    expect(names).toEqual([]);
    expect(logger.error).toHaveBeenCalled();
  });

  test('getAvailableModelNames returns model names', () => {
    service.models = [{ name: 'model-a' }, { name: 'model-b' }];
    const names = service.getAvailableModelNames();
    expect(names).toEqual(['model-a', 'model-b']);
  });

  test('getModels returns empty when no models', () => {
    expect(service.getModels()).toEqual([]);
  });

  test('getModels returns models array', () => {
    service.models = [{ name: 'model-a' }];
    expect(service.getModels()).toHaveLength(1);
  });

  test('fetchModels skips when already loading', async () => {
    service.isLoading = true;
    await service.fetchModels();
    expect(global.fetch).not.toHaveBeenCalled();
  });

  test('fetchModels fetches and stores models', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({
        models: [{ name: 'claude-3' }, { name: 'gpt-4' }]
      })
    });

    await service.fetchModels();
    expect(service.models).toHaveLength(2);
    expect(service.lastFetched).toBeInstanceOf(Date);
    expect(service.isLoading).toBe(false);
  });

  test('fetchModels uses /api prefix for localhost', async () => {
    service.backendUrl = 'http://localhost:8080';
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({ models: [] })
    });

    await service.fetchModels();
    expect(global.fetch.mock.calls[0][0]).toContain('/api/llm/models');
  });

  test('fetchModels uses no /api prefix for non-localhost', async () => {
    service.backendUrl = 'https://remote.example.com';
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({ models: [] })
    });

    await service.fetchModels();
    expect(global.fetch.mock.calls[0][0]).toBe('https://remote.example.com/llm/models');
  });

  test('fetchModels includes auth header from apiKeyManager', async () => {
    service.apiKeyManager = {
      getKeysForRequest: jest.fn().mockReturnValue({ loxiaApiKey: 'mgr-key' })
    };
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({ models: [] })
    });

    await service.fetchModels();
    const headers = global.fetch.mock.calls[0][1].headers;
    expect(headers.Authorization).toBe('Bearer mgr-key');
  });

  test('fetchModels falls back to context apiKey', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({ models: [] })
    });

    await service.fetchModels({ apiKey: 'ctx-key' });
    const headers = global.fetch.mock.calls[0][1].headers;
    expect(headers.Authorization).toBe('Bearer ctx-key');
  });

  test('fetchModels throws on invalid response format', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({ something: 'else' })
    });

    await expect(service.fetchModels()).rejects.toThrow('Invalid response format');
  });

  test('fetchModels handles HTTP error', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: false,
      status: 500,
      statusText: 'Server Error'
    });

    await expect(service.fetchModels()).rejects.toThrow('HTTP 500');
  });

  test('fetchModels handles AbortError timeout', async () => {
    const abortError = new Error('Aborted');
    abortError.name = 'AbortError';
    global.fetch.mockRejectedValueOnce(abortError);

    await expect(service.fetchModels()).rejects.toThrow('timeout');
  });

  test('fetchModels does not rethrow when models already exist', async () => {
    service.models = [{ name: 'existing' }];
    global.fetch.mockResolvedValueOnce({
      ok: false,
      status: 503,
      statusText: 'Unavailable'
    });

    // Should not throw because models already exist
    await service.fetchModels();
    expect(service.lastError).toContain('503');
  });

  test('needsRefresh returns true when never fetched', () => {
    expect(service.needsRefresh()).toBe(true);
  });

  test('needsRefresh returns false shortly after fetch', () => {
    service.lastFetched = new Date();
    expect(service.needsRefresh()).toBe(false);
  });

  test('refresh calls fetchModels', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({ models: [] })
    });
    await service.refresh();
    expect(global.fetch).toHaveBeenCalled();
  });

  test('refreshWithContext fetches when needed', async () => {
    service.needsRefresh = true; // truthy
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({ models: [] })
    });
    await service.refreshWithContext({ apiKey: 'key' });
    expect(global.fetch).toHaveBeenCalled();
  });

  test('_scheduleRetry does nothing after max retries', () => {
    service.retryAttempts = 3;
    service._scheduleRetry();
    expect(service.retryTimer).toBeNull();
    expect(logger.error).toHaveBeenCalled();
  });

  test('_scheduleRetry sets timer with exponential backoff', () => {
    service.retryAttempts = 0;
    service._scheduleRetry();
    expect(service.retryTimer).not.toBeNull();
    expect(service.retryAttempts).toBe(1);
  });

  test('_cancelRetry clears timer', () => {
    service.retryTimer = setTimeout(() => {}, 10000);
    service._cancelRetry();
    expect(service.retryTimer).toBeNull();
  });

  test('_cancelRetry is no-op with no timer', () => {
    service._cancelRetry();
    // No error
  });

  test('getStatus returns full status object', () => {
    service.models = [{ name: 'a' }];
    service.lastFetched = new Date('2024-01-01');
    service.lastError = 'some error';
    const status = service.getStatus();
    expect(status.initialized).toBe(true);
    expect(status.modelCount).toBe(1);
    expect(status.hasError).toBe(true);
    expect(status.error).toBe('some error');
  });

  test('getStatus when no models', () => {
    const status = service.getStatus();
    expect(status.initialized).toBe(false);
    expect(status.modelCount).toBe(0);
    expect(status.lastFetched).toBeNull();
  });

  test('initialize handles fetch failure and schedules retry', async () => {
    global.fetch.mockRejectedValueOnce(new Error('network fail'));
    await service.initialize();
    expect(service.lastError).toBe('network fail');
    expect(logger.error).toHaveBeenCalled();
  });

  test('initialize succeeds with valid data', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: jest.fn().mockResolvedValue({
        models: [{ name: 'test-model' }]
      })
    });
    await service.initialize();
    expect(service.models).toHaveLength(1);
  });
});
