import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import path from "node:path";
import os from "node:os";
import fs from "node:fs";

// ---------------------------------------------------------------------------
// Mock systeminformation before importing hardware service
// Must use vi.hoisted() so the reference is available when vi.mock() factory runs
// ---------------------------------------------------------------------------
const { mockGraphicsFn } = vi.hoisted(() => ({
  mockGraphicsFn: vi.fn(),
}));
vi.mock("systeminformation", () => ({
  default: {
    graphics: mockGraphicsFn,
  },
  graphics: mockGraphicsFn,
}));

// ---------------------------------------------------------------------------
// Mock home-paths for nexus-settings tests
// ---------------------------------------------------------------------------
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-test-"));
vi.mock("../home-paths.js", () => ({
  resolvePaperclipInstanceRoot: () => tmpDir,
}));

// ---------------------------------------------------------------------------
// Imports after mocks
// ---------------------------------------------------------------------------
import { hardwareService, _resetHardwareCache, type HardwareInfo, type HardwareTier } from "../services/hardware.js";
import { nexusSettingsService, NEXUS_MODES, type NexusMode } from "../services/nexus-settings.js";
import { getRecommendedModel, type OllamaModel } from "../services/ollama.js";

// ---------------------------------------------------------------------------
// hardwareService tests
// ---------------------------------------------------------------------------
describe("hardwareService", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    _resetHardwareCache();
    // Reset platform for each test
    Object.defineProperty(process, "platform", { value: "linux", writable: true });
  });

  it("detect() returns all required HardwareInfo fields", async () => {
    mockGraphicsFn.mockResolvedValue({ controllers: [], displays: [] });

    Object.defineProperty(process, "platform", { value: "linux", writable: true });

    const info = await hardwareService().detect();
    expect(info).toMatchObject({
      totalGb: expect.any(Number),
      freeGb: expect.any(Number),
      usableGb: expect.any(Number),
      platform: expect.any(String),
      hardwareTier: expect.any(String),
    });
    expect("gpuName" in info).toBe(true);
    expect("gpuVramGb" in info).toBe(true);
    expect("unifiedMemory" in info).toBe(true);
    expect("cpuModel" in info).toBe(true);
  });

  it("returns apple_silicon tier when platform is darwin and CPU starts with Apple", async () => {
    Object.defineProperty(process, "platform", { value: "darwin", writable: true });
    // We need to mock os.cpus() to return Apple CPU
    const origCpus = os.cpus;
    vi.spyOn(os, "cpus").mockReturnValue([
      { model: "Apple M4 Pro", speed: 3000, times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 } },
    ]);

    const info = await hardwareService().detect();
    expect(info.unifiedMemory).toBe(true);
    expect(info.hardwareTier).toBe("apple_silicon");
    expect(info.gpuVramGb).toBe(null);
    // si.graphics should NOT be called for Apple Silicon
    expect(mockGraphicsFn).not.toHaveBeenCalled();

    vi.spyOn(os, "cpus").mockRestore();
    Object.defineProperty(process, "platform", { value: "linux", writable: true });
  });

  it("returns gpu tier when GPU VRAM >= 4096 MB", async () => {
    Object.defineProperty(process, "platform", { value: "linux", writable: true });
    vi.spyOn(os, "cpus").mockReturnValue([
      { model: "Intel Core i9", speed: 3000, times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 } },
    ]);

    mockGraphicsFn.mockResolvedValue({
      controllers: [{ model: "NVIDIA RTX 4090", vram: 24576 }],
      displays: [],
    });

    const info = await hardwareService().detect();
    expect(info.hardwareTier).toBe("gpu");
    expect(info.gpuName).toBe("NVIDIA RTX 4090");
    expect(info.gpuVramGb).toBeCloseTo(24, 0);

    vi.spyOn(os, "cpus").mockRestore();
  });

  it("returns cpu_only tier when no GPU controllers", async () => {
    Object.defineProperty(process, "platform", { value: "linux", writable: true });
    vi.spyOn(os, "cpus").mockReturnValue([
      { model: "Intel Core i5", speed: 2000, times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 } },
    ]);

    mockGraphicsFn.mockResolvedValue({ controllers: [], displays: [] });

    const info = await hardwareService().detect();
    expect(info.hardwareTier).toBe("cpu_only");
    expect(info.gpuName).toBe(null);
    expect(info.gpuVramGb).toBe(null);

    vi.spyOn(os, "cpus").mockRestore();
  });

  it("returns cpu_only tier when si.graphics() times out (3 second timeout)", async () => {
    Object.defineProperty(process, "platform", { value: "linux", writable: true });
    vi.spyOn(os, "cpus").mockReturnValue([
      { model: "AMD Ryzen 5", speed: 2000, times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 } },
    ]);

    // Simulate a slow call that will never resolve in test (we fake timers)
    mockGraphicsFn.mockImplementation(
      () => new Promise((_resolve) => setTimeout(_resolve, 60000)),
    );

    vi.useFakeTimers();
    const detectPromise = hardwareService().detect();
    vi.advanceTimersByTime(4000); // Advance past 3000ms timeout
    const info = await detectPromise;

    expect(info.hardwareTier).toBe("cpu_only");
    expect(info.gpuName).toBe(null);

    vi.useRealTimers();
    vi.spyOn(os, "cpus").mockRestore();
  });
});

// ---------------------------------------------------------------------------
// nexusSettingsService tests
// ---------------------------------------------------------------------------
describe("nexusSettingsService", () => {
  afterEach(() => {
    // Clean up settings file between tests
    const settingsFile = path.join(tmpDir, "data", "nexus-settings.json");
    try { fs.unlinkSync(settingsFile); } catch { /* ignore */ }
  });

  it("get() returns { mode: 'both' } when no file exists (default)", async () => {
    const svc = nexusSettingsService();
    const settings = await svc.get();
    expect(settings).toEqual({ mode: "both" });
  });

  it("set() persists mode and get() reads it back", async () => {
    const svc = nexusSettingsService();
    await svc.set({ mode: "personal_ai" });
    const settings = await svc.get();
    expect(settings.mode).toBe("personal_ai");
  });

  it("set() with invalid mode throws Zod validation error", async () => {
    const svc = nexusSettingsService();
    await expect(svc.set({ mode: "invalid" as NexusMode })).rejects.toThrow();
  });

  it("NEXUS_MODES contains expected values", () => {
    expect(NEXUS_MODES).toContain("personal_ai");
    expect(NEXUS_MODES).toContain("project_builder");
    expect(NEXUS_MODES).toContain("both");
  });
});

// ---------------------------------------------------------------------------
// Model catalog tests
// ---------------------------------------------------------------------------
describe("model catalog", () => {
  it("every variant in catalog has a tier array", () => {
    // Load catalog directly from file
    const catalogPath = path.resolve(
      new URL(import.meta.url).pathname,
      "../../data/ollama-model-catalog.json",
    );
    const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf-8"));
    const allVariants = catalog.models.flatMap((f: any) => f.variants);
    const missingTier = allVariants.filter((v: any) => !v.tier);
    expect(missingTier).toHaveLength(0);
  });

  it("catalog contains qwen3 family with qwen3:8b variant", () => {
    const catalogPath = path.resolve(
      new URL(import.meta.url).pathname,
      "../../data/ollama-model-catalog.json",
    );
    const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf-8"));
    const qwen3Family = catalog.models.find((f: any) => f.family === "qwen3");
    expect(qwen3Family).toBeDefined();
    const qwen3_8b = qwen3Family?.variants.find((v: any) => v.name === "qwen3:8b");
    expect(qwen3_8b).toBeDefined();
    expect(qwen3_8b?.tier).toContain("gpu");
    expect(qwen3_8b?.tier).toContain("apple_silicon");
    expect(qwen3_8b?.tier).toContain("cpu_only");
  });
});

// ---------------------------------------------------------------------------
// getRecommendedModel with tier tests
// ---------------------------------------------------------------------------
describe("getRecommendedModel with tier", () => {
  const baseModels: OllamaModel[] = [
    { name: "qwen2.5-coder:7b", parameterSize: "7B", quantization: "Q4", sizeBytes: 4000000000, family: "qwen2", recommended: false, recommendationReason: null },
    { name: "qwen2.5-coder:32b", parameterSize: "32B", quantization: "Q4", sizeBytes: 20000000000, family: "qwen2", recommended: false, recommendationReason: null },
  ];

  it("filters models by gpu tier — recommends gpu-tier models only", () => {
    // qwen2.5-coder:32b has tier ["gpu"] — should be recommended for gpu tier with enough RAM
    const ramBytes = 100 * 1024 * 1024 * 1024; // 100GB
    const result = getRecommendedModel(baseModels, ramBytes, "gpu");
    const recommended = result.filter((m) => m.recommended);
    expect(recommended.length).toBeGreaterThanOrEqual(1);
  });

  it("without tier parameter, behavior is unchanged", () => {
    const ramBytes = 100 * 1024 * 1024 * 1024;
    const withTier = getRecommendedModel(baseModels, ramBytes, "gpu");
    const withoutTier = getRecommendedModel(baseModels, ramBytes);
    // Both should find recommendations
    expect(withTier.filter((m) => m.recommended).length).toBeGreaterThanOrEqual(0);
    expect(withoutTier.filter((m) => m.recommended).length).toBeGreaterThanOrEqual(0);
  });
});
