import { describe, it, expect, vi, beforeEach } from "vitest";

// Mock systeminformation before any imports that use it
vi.mock("systeminformation", () => ({
  default: {
    graphics: vi.fn().mockResolvedValue({ controllers: [] }),
  },
}));

// Mock child_process execFile before importing hardware service
vi.mock("node:child_process", () => {
  const execFileCb = vi.fn();
  return { execFile: execFileCb };
});

// Mock node:os so we can control RAM/CPU model
vi.mock("node:os", () => {
  const osActual = {
    totalmem: vi.fn().mockReturnValue(16 * 1024 * 1024 * 1024),
    freemem: vi.fn().mockReturnValue(8 * 1024 * 1024 * 1024),
    cpus: vi.fn().mockReturnValue([{ model: "Intel Core i9" }]),
  };
  return { default: osActual, ...osActual };
});

import { execFile } from "node:child_process";
import os from "node:os";
import si from "systeminformation";
import { hardwareService, _resetHardwareCache } from "../services/hardware.js";

// Helper: make execFile resolve for specific commands
function mockExecFilePartial(resolveFor: string[]) {
  (execFile as ReturnType<typeof vi.fn>).mockImplementation(
    (c: string, _args: string[], _opts: unknown, cb: (err: null | Error, stdout: string, stderr: string) => void) => {
      if (resolveFor.includes(c)) {
        cb(null, "version output", "");
      } else {
        const err = Object.assign(new Error("ENOENT"), { code: "ENOENT" });
        cb(err as Error, "", "");
      }
    }
  );
}

// Helper: make execFile reject with ENOENT for all
function mockExecFileAllFail() {
  (execFile as ReturnType<typeof vi.fn>).mockImplementation(
    (_c: string, _args: string[], _opts: unknown, cb: (err: Error, stdout: string, stderr: string) => void) => {
      const err = Object.assign(new Error("ENOENT"), { code: "ENOENT" });
      cb(err, "", "");
    }
  );
}

describe("voice capability detection", () => {
  beforeEach(() => {
    vi.resetAllMocks();
    // Re-stub si.graphics after resetAllMocks clears the mock
    vi.mocked(si.graphics).mockResolvedValue({ controllers: [] } as Awaited<ReturnType<typeof si.graphics>>);
    // Restore default os mocks
    vi.mocked(os.totalmem).mockReturnValue(16 * 1024 * 1024 * 1024);
    vi.mocked(os.freemem).mockReturnValue(8 * 1024 * 1024 * 1024);
    vi.mocked(os.cpus).mockReturnValue([{ model: "Intel Core i9" } as ReturnType<typeof os.cpus>[0]]);
    _resetHardwareCache();
  });

  it("returns whisperAvailable=true, piperAvailable=true when both binaries resolve", async () => {
    mockExecFilePartial(["whisper-cpp", "piper"]);
    const svc = hardwareService();
    const info = await svc.detect();
    expect(info.voiceCapability.whisperAvailable).toBe(true);
    expect(info.voiceCapability.piperAvailable).toBe(true);
  });

  it("returns whisperAvailable=false, piperAvailable=false when both binaries throw ENOENT", async () => {
    mockExecFileAllFail();
    const svc = hardwareService();
    const info = await svc.detect();
    expect(info.voiceCapability.whisperAvailable).toBe(false);
    expect(info.voiceCapability.piperAvailable).toBe(false);
  });

  it("returns whisperAvailable=true, piperAvailable=false when only whisper-cpp is found", async () => {
    mockExecFilePartial(["whisper-cpp"]);
    const svc = hardwareService();
    const info = await svc.detect();
    expect(info.voiceCapability.whisperAvailable).toBe(true);
    expect(info.voiceCapability.piperAvailable).toBe(false);
  });

  it("returns whisperAvailable=true when whisper (fallback) resolves but whisper-cpp fails", async () => {
    mockExecFilePartial(["whisper"]);
    const svc = hardwareService();
    const info = await svc.detect();
    expect(info.voiceCapability.whisperAvailable).toBe(true);
  });

  it("sets voiceTierSufficient=false for cpu_only tier with < 4GB free RAM", async () => {
    mockExecFileAllFail();

    vi.mocked(os.freemem).mockReturnValue(2 * 1024 * 1024 * 1024); // 2GB free
    vi.mocked(os.totalmem).mockReturnValue(8 * 1024 * 1024 * 1024);
    vi.mocked(os.cpus).mockReturnValue([{ model: "Intel Core i5" } as ReturnType<typeof os.cpus>[0]]);

    // linux = non-Apple, cpu_only path
    const savedPlatform = process.platform;
    Object.defineProperty(process, "platform", { value: "linux", configurable: true });

    _resetHardwareCache();
    const svc = hardwareService();
    const info = await svc.detect();
    expect(info.hardwareTier).toBe("cpu_only");
    expect(info.voiceCapability.voiceTierSufficient).toBe(false);

    Object.defineProperty(process, "platform", { value: savedPlatform, configurable: true });
  });

  it("sets voiceTierSufficient=true for apple_silicon tier", async () => {
    mockExecFileAllFail();

    vi.mocked(os.totalmem).mockReturnValue(16 * 1024 * 1024 * 1024);
    vi.mocked(os.freemem).mockReturnValue(8 * 1024 * 1024 * 1024);
    vi.mocked(os.cpus).mockReturnValue([{ model: "Apple M4" } as ReturnType<typeof os.cpus>[0]]);

    const savedPlatform = process.platform;
    Object.defineProperty(process, "platform", { value: "darwin", configurable: true });

    _resetHardwareCache();
    const svc = hardwareService();
    const info = await svc.detect();
    expect(info.hardwareTier).toBe("apple_silicon");
    expect(info.voiceCapability.voiceTierSufficient).toBe(true);

    Object.defineProperty(process, "platform", { value: savedPlatform, configurable: true });
  });
});
