import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { detectOllama, listOllamaModels, getRecommendedModel } from "../services/ollama.js";
import type { OllamaModel } from "../services/ollama.js";

describe("detectOllama", () => {
  beforeEach(() => {
    vi.stubGlobal("fetch", vi.fn());
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it("returns installed:true + version when Ollama responds at /api/version", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ version: "0.5.1" }),
    });
    vi.stubGlobal("fetch", mockFetch);

    const result = await detectOllama();

    expect(result.installed).toBe(true);
    expect(result.version).toBe("0.5.1");
    expect(result.installUrl).toBe("https://ollama.com/download");
  });

  it("returns installed:false + installUrl when Ollama is absent (ECONNREFUSED)", async () => {
    const mockFetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
    vi.stubGlobal("fetch", mockFetch);

    const result = await detectOllama();

    expect(result.installed).toBe(false);
    expect(result.version).toBeNull();
    expect(result.installUrl).toBe("https://ollama.com/download");
  });

  it("returns installed:false when fetch times out (AbortController)", async () => {
    const mockFetch = vi.fn().mockImplementation((_url: string, opts: { signal?: AbortSignal }) => {
      return new Promise<never>((_resolve, reject) => {
        if (opts?.signal) {
          opts.signal.addEventListener("abort", () => {
            reject(new DOMException("The operation was aborted.", "AbortError"));
          });
        }
        // Never resolves — simulates timeout
      });
    });
    vi.stubGlobal("fetch", mockFetch);

    const result = await detectOllama();

    expect(result.installed).toBe(false);
    expect(result.version).toBeNull();
    expect(result.installUrl).toBe("https://ollama.com/download");
  }, 10000);

  it("returns installed:false when response is not ok", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 503,
    });
    vi.stubGlobal("fetch", mockFetch);

    const result = await detectOllama();

    expect(result.installed).toBe(false);
    expect(result.version).toBeNull();
  });
});

describe("listOllamaModels", () => {
  beforeEach(() => {
    vi.stubGlobal("fetch", vi.fn());
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it("returns OllamaModel[] mapped from /api/tags response", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        models: [
          {
            name: "qwen2.5-coder:32b",
            model: "qwen2.5-coder:32b",
            modified_at: "2026-01-01T00:00:00Z",
            size: 23123456789,
            digest: "abc123",
            details: {
              parent_model: "",
              format: "gguf",
              family: "qwen2",
              families: ["qwen2"],
              parameter_size: "32.8B",
              quantization_level: "Q4_K_M",
            },
          },
          {
            name: "llama3.1:8b",
            model: "llama3.1:8b",
            modified_at: "2026-01-01T00:00:00Z",
            size: 4500000000,
            digest: "def456",
            details: {
              parent_model: "",
              format: "gguf",
              family: "llama",
              families: ["llama"],
              parameter_size: "8.0B",
              quantization_level: "Q4_K_M",
            },
          },
        ],
      }),
    });
    vi.stubGlobal("fetch", mockFetch);

    const models = await listOllamaModels();

    expect(models).toHaveLength(2);
    expect(models[0].name).toBe("qwen2.5-coder:32b");
    expect(models[0].parameterSize).toBe("32.8B");
    expect(models[0].quantization).toBe("Q4_K_M");
    expect(models[0].sizeBytes).toBe(23123456789);
    expect(models[0].family).toBe("qwen2");
    expect(models[0].recommended).toBe(false);
    expect(models[0].recommendationReason).toBeNull();
    expect(models[1].name).toBe("llama3.1:8b");
    expect(models[1].family).toBe("llama");
  });

  it("returns empty array when Ollama is absent", async () => {
    const mockFetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
    vi.stubGlobal("fetch", mockFetch);

    const models = await listOllamaModels();

    expect(models).toEqual([]);
  });

  it("returns empty array when response is not ok", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 503,
    });
    vi.stubGlobal("fetch", mockFetch);

    const models = await listOllamaModels();

    expect(models).toEqual([]);
  });
});

describe("getRecommendedModel", () => {
  const makeModel = (name: string, family: string): OllamaModel => ({
    name,
    parameterSize: "7B",
    quantization: "Q4_K_M",
    sizeBytes: 4000000000,
    family,
    recommended: false,
    recommendationReason: null,
  });

  it("recommends a 7b-class model when system RAM is 8GB", () => {
    const models: OllamaModel[] = [
      makeModel("qwen2.5-coder:7b", "qwen2"),
      makeModel("qwen2.5-coder:32b", "qwen2"),
    ];
    const ramBytes = 8 * 1024 * 1024 * 1024; // 8GB

    const result = getRecommendedModel(models, ramBytes);

    const recommended = result.filter((m) => m.recommended);
    expect(recommended).toHaveLength(1);
    expect(recommended[0].name).toBe("qwen2.5-coder:7b");
    expect(recommended[0].recommendationReason).not.toBeNull();
  });

  it("recommends a 32b-class model when system RAM is 32GB", () => {
    const models: OllamaModel[] = [
      makeModel("qwen2.5-coder:7b", "qwen2"),
      makeModel("qwen2.5-coder:32b", "qwen2"),
    ];
    const ramBytes = 32 * 1024 * 1024 * 1024; // 32GB

    const result = getRecommendedModel(models, ramBytes);

    const recommended = result.filter((m) => m.recommended);
    expect(recommended).toHaveLength(1);
    expect(recommended[0].name).toBe("qwen2.5-coder:32b");
    expect(recommended[0].recommendationReason).not.toBeNull();
  });

  it("returns recommended=false for all models not in catalog", () => {
    const models: OllamaModel[] = [
      makeModel("unknown-model:7b", "unknown"),
      makeModel("another-unknown:13b", "mystery"),
    ];
    const ramBytes = 64 * 1024 * 1024 * 1024; // 64GB — plenty of RAM

    const result = getRecommendedModel(models, ramBytes);

    expect(result.every((m) => !m.recommended)).toBe(true);
  });

  it("returns empty array when no models provided", () => {
    const result = getRecommendedModel([], 16 * 1024 * 1024 * 1024);
    expect(result).toEqual([]);
  });

  it("does not recommend models that exceed 75% of system RAM", () => {
    // 4GB RAM — 75% = 3GB. qwen2.5-coder:7b needs 5GB, should NOT be recommended
    const models: OllamaModel[] = [
      makeModel("qwen2.5-coder:7b", "qwen2"),
    ];
    const ramBytes = 4 * 1024 * 1024 * 1024; // 4GB

    const result = getRecommendedModel(models, ramBytes);

    expect(result.every((m) => !m.recommended)).toBe(true);
  });
});
