import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { EventEmitter } from "node:events";
import { createRequire, syncBuiltinESMExports } from "node:module";

const require = createRequire(import.meta.url);
const childProcess = require("node:child_process");

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-version-manager-"));
const TEST_CONFIG_DIR = path.join(TEST_DATA_DIR, "cliproxyapi-config");
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.CLIPROXYAPI_CONFIG_DIR = TEST_CONFIG_DIR;

const coreDb = await import("../../src/lib/db/core.ts");
const versionManagerDb = await import("../../src/lib/db/versionManager.ts");
const processManager = await import("../../src/lib/versionManager/processManager.ts");
const versionManager = await import("../../src/lib/versionManager/index.ts");
const healthMonitor = await import("../../src/lib/versionManager/healthMonitor.ts");

const originalFetch = globalThis.fetch;
const originalSpawn = childProcess.spawn;
const originalProcessKill = process.kill;
const originalSetTimeout = globalThis.setTimeout;
const originalSetInterval = globalThis.setInterval;
const originalClearTimeout = globalThis.clearTimeout;
const originalClearInterval = globalThis.clearInterval;

async function resetStorage() {
  healthMonitor.stopMonitoring("cliproxyapi");
  coreDb.resetDbInstance();

  for (let attempt = 0; attempt < 10; attempt++) {
    try {
      if (fs.existsSync(TEST_DATA_DIR)) {
        fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
      }
      break;
    } catch (error: any) {
      if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
        await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
      } else {
        throw error;
      }
    }
  }

  fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
  fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
}

function installSpawnStub(startPid = 6100) {
  const calls = [];
  let nextPid = startPid;

  childProcess.spawn = (command, args, options) => {
    const child = new EventEmitter();
    child.pid = nextPid++;
    child.stdout = new EventEmitter();
    child.stderr = new EventEmitter();
    child.kill = () => true;
    calls.push({ command, args, options, child });
    return child;
  };
  syncBuiltinESMExports();

  return {
    calls,
    restore() {
      childProcess.spawn = originalSpawn;
      syncBuiltinESMExports();
    },
  };
}

function installProcessKillStub(initialRunning = []) {
  const running = new Set(initialRunning);
  const calls = [];

  process.kill = (pid, signal = 0) => {
    calls.push({ pid, signal });

    if (signal === 0 || signal === undefined) {
      if (running.has(pid)) {
        return true;
      }
      const error = new Error("ESRCH");
      error.code = "ESRCH";
      throw error;
    }

    if (signal === "SIGTERM" || signal === "SIGKILL") {
      running.delete(pid);
      return true;
    }

    return true;
  };

  return {
    calls,
    running,
    restore() {
      process.kill = originalProcessKill;
    },
  };
}

function installTimerStubs() {
  const timeouts = [];
  const intervals = [];

  globalThis.setTimeout = (fn, ms) => {
    const handle = {
      fn,
      ms,
      cleared: false,
      unrefCalled: false,
      unref() {
        this.unrefCalled = true;
        return this;
      },
    };
    timeouts.push(handle);
    return handle;
  };

  globalThis.setInterval = (fn, ms) => {
    const handle = {
      fn,
      ms,
      cleared: false,
      unrefCalled: false,
      unref() {
        this.unrefCalled = true;
        return this;
      },
    };
    intervals.push(handle);
    return handle;
  };

  globalThis.clearTimeout = (handle) => {
    if (handle) {
      handle.cleared = true;
    }
  };

  globalThis.clearInterval = (handle) => {
    if (handle) {
      handle.cleared = true;
    }
  };

  return {
    timeouts,
    intervals,
    restore() {
      globalThis.setTimeout = originalSetTimeout;
      globalThis.setInterval = originalSetInterval;
      globalThis.clearTimeout = originalClearTimeout;
      globalThis.clearInterval = originalClearInterval;
    },
  };
}

function installFetchStub() {
  const calls = [];

  globalThis.fetch = async (url, options = {}) => {
    calls.push({ url: String(url), options });

    if (String(url).includes("/v1/models")) {
      return new Response(JSON.stringify({ data: [{ id: "gpt-4.1" }, { id: "o3-mini" }] }), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      });
    }

    if (String(url).includes("/releases/latest")) {
      return new Response(
        JSON.stringify({
          tag_name: "v2.0.0",
          assets: [],
          published_at: "2026-04-06T00:00:00Z",
          body: "Latest release",
        }),
        {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }
      );
    }

    return new Response(JSON.stringify({ ok: true }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  };

  return {
    calls,
    restore() {
      globalThis.fetch = originalFetch;
    },
  };
}

async function seedTool(overrides = {}) {
  return versionManagerDb.upsertVersionManagerTool({
    tool: "cliproxyapi",
    installedVersion: "2.0.0",
    binaryPath: path.join(TEST_DATA_DIR, "bin", "cliproxyapi"),
    status: "installed",
    port: 8317,
    ...overrides,
  });
}

async function prepareInstalledVersions(versions) {
  const binDir = path.join(TEST_DATA_DIR, "bin");
  fs.mkdirSync(binDir, { recursive: true });

  for (const version of versions) {
    const versionDir = path.join(binDir, `cliproxyapi-${version}`);
    fs.mkdirSync(versionDir, { recursive: true });
    fs.writeFileSync(path.join(versionDir, "CLIProxyAPI"), "#!/bin/sh\necho ok\n");
  }

  const symlinkPath = path.join(binDir, "cliproxyapi");
  try {
    fs.unlinkSync(symlinkPath);
  } catch {}
  fs.symlinkSync(path.join(binDir, "cliproxyapi-2.0.0", "CLIProxyAPI"), symlinkPath);
}

async function flushAsyncTurns(count = 3) {
  for (let i = 0; i < count; i++) {
    await Promise.resolve();
  }
}

test.beforeEach(async () => {
  await resetStorage();
});

test.afterEach(() => {
  healthMonitor.stopMonitoring("cliproxyapi");
  childProcess.spawn = originalSpawn;
  process.kill = originalProcessKill;
  globalThis.fetch = originalFetch;
  globalThis.setTimeout = originalSetTimeout;
  globalThis.setInterval = originalSetInterval;
  globalThis.clearTimeout = originalClearTimeout;
  globalThis.clearInterval = originalClearInterval;
  syncBuiltinESMExports();
});

test.after(async () => {
  await resetStorage();
  fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});

test("processManager reuses an alive persisted pid without spawning a new process", async () => {
  await seedTool({ pid: 4321, port: 8450, status: "running" });
  const spawnStub = installSpawnStub();
  const killStub = installProcessKillStub([4321]);

  try {
    const result = await processManager.startProcess("/tmp/cli-proxy-api");

    assert.deepEqual(result, { pid: 4321, port: 8450 });
    assert.equal(spawnStub.calls.length, 0);
    assert.deepEqual(killStub.calls, [{ pid: 4321, signal: 0 }]);
  } finally {
    spawnStub.restore();
    killStub.restore();
  }
});

test("processManager writes config, starts a process, stops it gracefully and reports process info", async () => {
  await seedTool({ pid: null, status: "installed" });
  const spawnStub = installSpawnStub(5100);
  const killStub = installProcessKillStub();
  const timers = installTimerStubs();

  try {
    const result = await processManager.startProcess(
      "/tmp/cli-proxy-api",
      8401,
      path.join(TEST_CONFIG_DIR, "custom")
    );

    assert.deepEqual(result, { pid: 5100, port: 8401 });
    assert.equal(spawnStub.calls.length, 1);
    assert.equal(spawnStub.calls[0].command, "/tmp/cli-proxy-api");
    assert.deepEqual(spawnStub.calls[0].args, [
      "-c",
      path.join(TEST_CONFIG_DIR, "custom", "config.yaml"),
    ]);

    const config = fs.readFileSync(path.join(TEST_CONFIG_DIR, "custom", "config.yaml"), "utf8");
    assert.match(config, /port: 8401/);
    assert.match(config, /host: 127\.0\.0\.1/);

    const persisted = await versionManagerDb.getVersionManagerTool("cliproxyapi");
    assert.equal(persisted.status, "running");
    assert.equal(persisted.pid, 5100);

    killStub.running.add(5100);
    killStub.running.add(process.pid);
    const stopPromise = processManager.stopProcess(5100);
    await timers.intervals.find((handle) => handle.ms === 200).fn();
    await stopPromise;

    assert.deepEqual(
      killStub.calls.filter((call) => call.signal !== 0),
      [{ pid: 5100, signal: "SIGTERM" }]
    );

    const info = await processManager.getProcessInfo(process.pid);
    assert.equal(info.pid, process.pid);
    assert.equal(info.alive, true);
    if (process.platform === "linux") {
      assert.ok(info.memoryUsage > 0);
    }
  } finally {
    timers.restore();
    spawnStub.restore();
    killStub.restore();
  }
});

test("versionManager start, health, restart and stop flow updates monitoring and persisted state", async () => {
  await seedTool({ pid: null, port: 8511, status: "installed" });
  const spawnStub = installSpawnStub(6200);
  const killStub = installProcessKillStub();
  const fetchStub = installFetchStub();

  try {
    const started = await versionManager.startTool("cliproxyapi");
    assert.deepEqual(
      {
        pid: started.pid,
        port: started.port,
        healthy: started.health.healthy,
        modelCount: started.health.modelCount,
      },
      { pid: 6200, port: 8511, healthy: true, modelCount: 2 }
    );
    assert.equal(healthMonitor.isMonitoring("cliproxyapi"), true);

    const health = await versionManager.getToolHealth("cliproxyapi");
    assert.equal(health.healthy, true);
    assert.equal(health.modelCount, 2);

    killStub.running.add(started.pid);
    const restarted = await versionManager.restartTool("cliproxyapi");

    assert.deepEqual(restarted, { pid: 6201, port: 8511 });
    assert.equal(healthMonitor.isMonitoring("cliproxyapi"), true);

    killStub.running.add(restarted.pid);
    await versionManager.stopTool("cliproxyapi");

    const stopped = await versionManagerDb.getVersionManagerTool("cliproxyapi");
    assert.equal(stopped.status, "stopped");
    assert.equal(healthMonitor.isMonitoring("cliproxyapi"), false);
    assert.ok(fetchStub.calls.some((call) => call.url.includes("/v1/models")));
  } finally {
    fetchStub.restore();
    spawnStub.restore();
    killStub.restore();
  }
});

test("versionManager checks releases, persists pinning and rolls back to a previous version", async () => {
  await prepareInstalledVersions(["2.0.0", "1.0.0"]);
  await seedTool({
    installedVersion: "2.0.0",
    port: 8522,
    status: "running",
    pid: 7300,
  });

  const spawnStub = installSpawnStub(7301);
  const killStub = installProcessKillStub([7300]);
  const fetchStub = installFetchStub();

  try {
    const updateCheck = await versionManager.checkForUpdates("cliproxyapi");
    assert.deepEqual(updateCheck, {
      current: "2.0.0",
      latest: "2.0.0",
      updateAvailable: false,
    });

    await versionManager.pinVersion("cliproxyapi", "1.9.0");
    assert.equal(
      (await versionManagerDb.getVersionManagerTool("cliproxyapi")).pinnedVersion,
      "1.9.0"
    );

    await versionManager.unpinVersion("cliproxyapi");
    assert.equal((await versionManagerDb.getVersionManagerTool("cliproxyapi")).pinnedVersion, null);

    await versionManagerDb.updateVersionManagerTool("cliproxyapi", { installedVersion: "1.0.0" });
    const secondUpdateCheck = await versionManager.checkForUpdates("cliproxyapi");
    assert.deepEqual(secondUpdateCheck, {
      current: "1.0.0",
      latest: "2.0.0",
      updateAvailable: true,
    });

    await versionManagerDb.updateVersionManagerTool("cliproxyapi", { installedVersion: "2.0.0" });
    const rolledBack = await versionManager.rollbackTool("cliproxyapi");

    assert.equal(rolledBack, "1.0.0");
    assert.equal(
      (await versionManagerDb.getVersionManagerTool("cliproxyapi")).installedVersion,
      "1.0.0"
    );
    assert.ok(spawnStub.calls.length >= 1);
    assert.ok(fetchStub.calls.some((call) => call.url.includes("/releases/latest")));
  } finally {
    fetchStub.restore();
    spawnStub.restore();
    killStub.restore();
  }
});
