import fs from "fs";
import os from "os";
import path from "path";
import { afterEach, describe, expect, it } from "vitest";
import { createRuntimeManager } from "./runtime-manager.mjs";

const cleanups = [];

afterEach(() => {
  while (cleanups.length > 0) {
    cleanups.pop()();
  }
});

function makeTempDir(prefix) {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
  cleanups.push(() => fs.rmSync(dir, { recursive: true, force: true }));
  return dir;
}

function makeLifecycle() {
  return {
    signal: new AbortController().signal,
    isStopRequested() {
      return false;
    },
    trackGraphContext() {},
  };
}

function makeTask(id, extras = {}) {
  return {
    id,
    graphKey: "api",
    locks: [],
    ...extras,
  };
}

function makeHooks(events) {
  return {
    createRuntimeInstanceContext(runtimeId, graph, productDir) {
      const runtimeDir = path.join(productDir, ".testkit", "_graphs", graph.dirName, "runtimes", runtimeId);
      fs.mkdirSync(runtimeDir, { recursive: true });
      events.created.push(runtimeId);
      return {
        graphKey: graph.key,
        runtimeDir,
        runtimeId,
        targetNames: [...graph.targetNames],
      };
    },
    async ensureRuntimeInstanceReady(context) {
      events.ready.push(context.runtimeId);
    },
    async cleanupRuntimeInstanceContext(context) {
      events.cleaned.push(context.runtimeId);
      fs.rmSync(context.runtimeDir, { recursive: true, force: true });
    },
    async sleep() {
      await new Promise((resolve) => setTimeout(resolve, 0));
    },
  };
}

describe("runtime-manager", () => {
  it("spreads concurrent leases across runtime instances", async () => {
    const productDir = makeTempDir("testkit-runtime-manager-");
    const events = { created: [], ready: [], cleaned: [] };
    const manager = createRuntimeManager({
      productDir,
      lifecycle: makeLifecycle(),
      graphs: [
        {
          key: "api",
          dirName: "api",
          targetNames: ["api"],
          instanceCount: 2,
          maxConcurrentTasks: 1,
        },
      ],
      hooks: makeHooks(events),
    });

    const leaseOne = await manager.acquire(makeTask(1));
    const leaseTwo = await manager.acquire(makeTask(2));

    expect(leaseOne.slot.runtimeId).toBe("runtime-1");
    expect(leaseTwo.slot.runtimeId).toBe("runtime-2");
    expect(events.created).toEqual(["runtime-1", "runtime-2"]);

    await manager.release(leaseOne);
    await manager.release(leaseTwo);
    await manager.cleanupAll();
  });

  it("marks conflicting locks unavailable until the first lease releases", async () => {
    const productDir = makeTempDir("testkit-runtime-manager-");
    const manager = createRuntimeManager({
      productDir,
      lifecycle: makeLifecycle(),
      graphs: [
        {
          key: "api",
          dirName: "api",
          targetNames: ["api"],
          instanceCount: 1,
          maxConcurrentTasks: 1,
        },
      ],
      hooks: makeHooks({ created: [], ready: [], cleaned: [] }),
    });

    const firstLease = await manager.acquire(makeTask(1, { locks: ["shared-lock"] }));
    expect(manager.canAcquire(makeTask(2, { locks: ["shared-lock"] }))).toBe(false);

    await manager.release(firstLease);
    const secondLease = await manager.acquire(makeTask(2, { locks: ["shared-lock"] }));

    await manager.release(secondLease);
    await manager.cleanupAll();
  });

  it("does not drain an invalidated runtime slot until all active leases release", async () => {
    const productDir = makeTempDir("testkit-runtime-manager-");
    const events = { created: [], ready: [], cleaned: [] };
    const manager = createRuntimeManager({
      productDir,
      lifecycle: makeLifecycle(),
      graphs: [
        {
          key: "api",
          dirName: "api",
          targetNames: ["api"],
          instanceCount: 1,
          maxConcurrentTasks: 2,
        },
      ],
      hooks: makeHooks(events),
    });

    const leaseOne = await manager.acquire(makeTask(1));
    const leaseTwo = await manager.acquire(makeTask(2));

    await manager.release(leaseOne, { invalidate: true });
    expect(events.cleaned).toEqual([]);

    await manager.release(leaseTwo);
    expect(events.cleaned).toEqual(["runtime-1"]);
  });

  it("removes lease directories on release", async () => {
    const productDir = makeTempDir("testkit-runtime-manager-");
    const manager = createRuntimeManager({
      productDir,
      lifecycle: makeLifecycle(),
      graphs: [
        {
          key: "api",
          dirName: "api",
          targetNames: ["api"],
          instanceCount: 1,
          maxConcurrentTasks: 1,
        },
      ],
      hooks: makeHooks({ created: [], ready: [], cleaned: [] }),
    });

    const lease = await manager.acquire(makeTask(1));
    expect(fs.existsSync(lease.leaseDir)).toBe(true);

    await manager.release(lease);
    expect(fs.existsSync(lease.leaseDir)).toBe(false);

    await manager.cleanupAll();
  });

  it("exposes runtime capacity through canAcquire", async () => {
    const productDir = makeTempDir("testkit-runtime-manager-");
    const manager = createRuntimeManager({
      productDir,
      lifecycle: makeLifecycle(),
      graphs: [
        {
          key: "api",
          dirName: "api",
          targetNames: ["api"],
          instanceCount: 1,
          maxConcurrentTasks: 2,
        },
      ],
      hooks: makeHooks({ created: [], ready: [], cleaned: [] }),
    });

    expect(manager.canAcquire(makeTask(1))).toBe(true);
    const leaseOne = await manager.acquire(makeTask(1));
    expect(manager.canAcquire(makeTask(2))).toBe(true);
    const leaseTwo = await manager.acquire(makeTask(2));
    expect(manager.canAcquire(makeTask(3))).toBe(false);

    await manager.release(leaseOne);
    expect(manager.canAcquire(makeTask(3))).toBe(true);
    await manager.release(leaseTwo);
    await manager.cleanupAll();
  });
});
