import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OllamaClient } from './client.js';

const BASE_URL = 'http://localhost:11434';

function mockFetch(response: unknown, options?: { ok?: boolean; status?: number }) {
  return vi.fn().mockResolvedValue({
    ok: options?.ok ?? true,
    status: options?.status ?? 200,
    json: () => Promise.resolve(response),
    text: () => Promise.resolve(JSON.stringify(response)),
  });
}

describe('OllamaClient', () => {
  let client: OllamaClient;

  beforeEach(() => {
    client = new OllamaClient({ baseUrl: BASE_URL });
    vi.restoreAllMocks();
  });

  describe('constructor', () => {
    it('strips trailing slashes from baseUrl', () => {
      const c = new OllamaClient({ baseUrl: 'http://localhost:11434///' });
      expect(c.baseUrl).toBe('http://localhost:11434');
    });
  });

  describe('tags', () => {
    it('returns list of models', async () => {
      const models = [{ name: 'llama3', size: 1000, digest: 'abc', modified_at: '2024-01-01' }];
      globalThis.fetch = mockFetch({ models });

      const result = await client.tags();
      expect(result).toEqual(models);
      expect(globalThis.fetch).toHaveBeenCalledWith(
        `${BASE_URL}/api/tags`,
        expect.objectContaining({
          headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
        })
      );
    });

    it('returns empty array when models is null', async () => {
      globalThis.fetch = mockFetch({ models: null });
      const result = await client.tags();
      expect(result).toEqual([]);
    });
  });

  describe('ps', () => {
    it('returns running models', async () => {
      const models = [
        { name: 'llama3', size: 1000, digest: 'abc', expires_at: '2024-01-01', size_vram: 500 },
      ];
      globalThis.fetch = mockFetch({ models });

      const result = await client.ps();
      expect(result).toEqual(models);
    });
  });

  describe('show', () => {
    it('sends POST with model name', async () => {
      const showData = { modelfile: '', parameters: '', template: '', details: {} };
      globalThis.fetch = mockFetch(showData);

      const result = await client.show('llama3');
      expect(result).toEqual(showData);
      expect(globalThis.fetch).toHaveBeenCalledWith(
        `${BASE_URL}/api/show`,
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ name: 'llama3' }),
        })
      );
    });
  });

  describe('pull (non-streaming)', () => {
    it('pulls a model', async () => {
      globalThis.fetch = mockFetch({ status: 'success' });

      const result = await client.pull('llama3');
      expect(result).toEqual({ status: 'success' });
    });

    it('throws on failure', async () => {
      globalThis.fetch = mockFetch('Pull failed', { ok: false, status: 500 });
      await expect(client.pull('bad-model')).rejects.toThrow('Ollama pull failed (500)');
    });
  });

  describe('load', () => {
    it('sends generate with keep_alive', async () => {
      globalThis.fetch = mockFetch({});

      await client.load('llama3', '15m');
      expect(globalThis.fetch).toHaveBeenCalledWith(
        `${BASE_URL}/api/generate`,
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ model: 'llama3', prompt: '', keep_alive: '15m' }),
        })
      );
    });
  });

  describe('unload', () => {
    it('sends generate with keep_alive: 0', async () => {
      globalThis.fetch = mockFetch({});

      await client.unload('llama3');
      expect(globalThis.fetch).toHaveBeenCalledWith(
        `${BASE_URL}/api/generate`,
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ model: 'llama3', prompt: '', keep_alive: '0' }),
        })
      );
    });
  });

  describe('delete', () => {
    it('sends DELETE request', async () => {
      globalThis.fetch = mockFetch({});

      await client.delete('llama3');
      expect(globalThis.fetch).toHaveBeenCalledWith(
        `${BASE_URL}/api/delete`,
        expect.objectContaining({
          method: 'DELETE',
          body: JSON.stringify({ name: 'llama3' }),
        })
      );
    });

    it('throws on failure', async () => {
      globalThis.fetch = mockFetch('model not found', { ok: false, status: 404 });
      await expect(client.delete('nope')).rejects.toThrow('Ollama /api/delete failed (404)');
    });
  });

  describe('version', () => {
    it('returns version string', async () => {
      globalThis.fetch = mockFetch({ version: '0.5.4' });

      const result = await client.version();
      expect(result).toBe('0.5.4');
    });
  });
});
