import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { startAdmissionReconcileLoop } from "./admission-loop";
import { CapabilityService } from "./capability";
import { ControllerStore } from "./store";
import { TransitionEngine } from "./transitions";

describe("startAdmissionReconcileLoop", () => {
  let dir: string;
  let store: ControllerStore;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), "admission-loop-"));
    store = new ControllerStore(join(dir, "state.sqlite"));
  });

  afterEach(() => {
    store.close();
    rmSync(dir, { recursive: true, force: true });
  });

  const engineWith = (admitted: boolean, failureReason: string | null) =>
    new TransitionEngine(
      store,
      () => Date.now(),
      new CapabilityService(store, {
        probe: async () => ({
          commandPresent: admitted,
          version: admitted ? "ssh-exit-0" : null,
          writablePaths: [],
          diskFreeBytes: 0,
          systemd: false,
          failureReason,
        }),
      }),
    );

  test("promotes an enrolling host with no operator action", async () => {
    store.upsertHost({ hostname: "debian3", state: "maintenance", enrolling: true, capabilityOk: false });
    const loop = startAdmissionReconcileLoop({
      store,
      engine: engineWith(true, null),
      intervalMs: 3_600_000,
      log: () => {},
    });

    await loop.firstPass;
    loop.stop();

    expect(store.getHost("debian3")).toMatchObject({
      state: "available",
      enrolling: false,
      capabilityOk: true,
      capabilityReason: null,
    });
    expect(store.getHost("debian3")?.capabilityCheckedAt).toEqual(expect.any(String));
  });

  test("records and logs the probe's reason when the host stays out", async () => {
    store.upsertHost({ hostname: "debian3", state: "maintenance", enrolling: true, capabilityOk: false });
    const lines: string[] = [];
    const loop = startAdmissionReconcileLoop({
      store,
      engine: engineWith(false, "ssh debian3:2222 exited 255 (ssh transport, auth, or host-key failure)"),
      intervalMs: 3_600_000,
      log: (line) => lines.push(line),
    });

    await loop.firstPass;
    loop.stop();

    expect(store.getHost("debian3")).toMatchObject({
      state: "maintenance",
      enrolling: true,
      capabilityOk: false,
      capabilityReason: "ssh debian3:2222 exited 255 (ssh transport, auth, or host-key failure)",
    });
    expect(lines).toContain(
      "admission-loop: debian3 not admitted — ssh debian3:2222 exited 255 (ssh transport, auth, or host-key failure)",
    );
  });

  test("continues host admission when delivery lifecycle reconciliation throws", async () => {
    store.upsertHost({ hostname: "debian3", state: "maintenance", enrolling: true, capabilityOk: false });
    const lines: string[] = [];
    const loop = startAdmissionReconcileLoop({
      store,
      engine: engineWith(true, null),
      reconcileDeliveryLifecycle: () => { throw new Error("expiry failed"); },
      intervalMs: 3_600_000,
      log: (line) => lines.push(line),
    });
    await loop.firstPass;
    loop.stop();
    expect(store.getHost("debian3")).toMatchObject({ state: "available", enrolling: false });
    expect(lines).toContain("admission-loop: delivery lifecycle reconcile threw: expiry failed");
  });

  test("skips the pass entirely when nothing is enrolling", async () => {
    store.upsertHost({ hostname: "debian1", state: "available", enrolling: false, capabilityOk: true });
    let probed = false;
    const engine = new TransitionEngine(
      store,
      () => Date.now(),
      new CapabilityService(store, {
        probe: async () => {
          probed = true;
          return {
            commandPresent: true, version: "ssh-exit-0", writablePaths: [],
            diskFreeBytes: 0, systemd: false, failureReason: null,
          };
        },
      }),
    );
    const loop = startAdmissionReconcileLoop({ store, engine, intervalMs: 3_600_000, log: () => {} });

    expect(await loop.firstPass).toBeNull();
    loop.stop();
    expect(probed).toBe(false);
  });

  test("does not start a second pass while one is in flight", async () => {
    store.upsertHost({ hostname: "debian3", state: "maintenance", enrolling: true, capabilityOk: false });
    const release = Promise.withResolvers<void>();
    let probes = 0;
    const engine = new TransitionEngine(
      store,
      () => Date.now(),
      new CapabilityService(store, {
        probe: async () => {
          probes += 1;
          await release.promise;
          return {
            commandPresent: true, version: "ssh-exit-0", writablePaths: [],
            diskFreeBytes: 0, systemd: false, failureReason: null,
          };
        },
      }),
    );
    let lifecyclePasses = 0;
    const loop = startAdmissionReconcileLoop({ store, engine, reconcileDeliveryLifecycle: async () => { lifecyclePasses += 1; await release.promise; }, intervalMs: 3_600_000, log: () => {} });
    const first = loop.firstPass;
    await Bun.sleep(0);
    expect(await loop.tick()).toBeNull();
    release.resolve();
    await first;
    loop.stop();
    expect(probes).toBe(1);
    expect(lifecyclePasses).toBe(1);
  });
});
