import { describe, expect, test } from "bun:test";
import { join } from "node:path";
import { createBackgroundHostMetricsSampler, createSshHostMetricsSampler, type SyncSpawnExec } from "./host-metrics";
import type { HostMetricsSampler } from "./metrics";

const home = "/fixture/home";

const machinePayload = {
  sessions: 3,
  load: 1.25,
  cores: 2,
  coreLoadPercent: [12.5, 87.5],
  memUsedBytes: 8_000_000_000,
  memTotalBytes: 16_000_000_000,
  swapUsedBytes: 0,
  swapTotalBytes: 4_000_000_000,
  netRxBytesPerSecond: 1_024,
  netTxBytesPerSecond: 2_048,
  disks: [{ mountpoint: "/", freeBytes: 100, sizeBytes: 200 }],
  temperatures: { pkg: 55, max: 61, crit: 95 },
  guard: {
    memoryStallSome60: 1.5,
    memoryStallFull60: 0.25,
    memoryStallFull300: 0,
    tmpUsedBytes: 69_464_064,
    tmpSizeBytes: 16_714_129_408,
    workSlices: [
      { slice: "agent.slice", memoryBytes: 2_000_000, oomKillTotal: 1, pidsCurrent: 12 },
      { slice: "build.slice", memoryBytes: 0, oomKillTotal: 0, pidsCurrent: 0 },
    ],
  },
};

// A box on the previous telemetry build publishes no remoteWork; the sampler fills null.
const sampledMachine = { ...machinePayload, remoteWork: null };

const publishedAgo = (ageMs: number) => JSON.stringify({
  sampledAtEpochMs: Date.now() - ageMs,
  ...machinePayload,
});

describe("createSshHostMetricsSampler", () => {
  test("rejects unsafe hostnames without spawning", () => {
    let spawned = false;
    const exec: SyncSpawnExec = () => {
      spawned = true;
      return { exitCode: 0, stdout: "", stderr: "" };
    };

    const sampler = createSshHostMetricsSampler(exec, home);
    expect(sampler.sample(["-evil", ""])).toEqual({
      metrics: [],
      gaps: [
        { host: "-evil", reason: "not a valid hostname: -evil", at: expect.any(String) },
        { host: "", reason: "not a valid hostname: ", at: expect.any(String) },
      ],
    });
    expect(spawned).toBe(false);
  });

  test("invokes exact ssh argv transport and maps JSON metrics", () => {
    const calls: Array<{ cmd: string[]; options: { shell: false; timeoutMs?: number } }> = [];
    const exec: SyncSpawnExec = (cmd, options) => {
      calls.push({ cmd, options });
      return {
        exitCode: 0,
        stdout: `${publishedAgo(0)}\n`,
        stderr: "",
      };
    };

    const sampler = createSshHostMetricsSampler(exec, home, 5_000);
    expect(sampler.sample(["debian1"])).toEqual({
      metrics: [{ host: "debian1", ...sampledMachine }],
      gaps: [],
    });
    expect(calls).toHaveLength(1);
    expect(calls[0]?.cmd).toEqual([
      "ssh",
      "-p",
      "2222",
      "-i",
      join(home, ".ssh/id_ed25519_buildbox"),
      "debian1",
      "cat",
      ".local/state/overdeck/buildbox-telemetry.json",
    ]);
    expect(calls[0]?.options).toEqual({ shell: false, timeoutMs: 5_000 });
  });

  test("keeps a summary published one box cadence ago", () => {
    const exec: SyncSpawnExec = () => ({ exitCode: 0, stdout: publishedAgo(15_000), stderr: "" });
    expect(createSshHostMetricsSampler(exec, home).sample(["debian1"])).toEqual({
      metrics: [{ host: "debian1", ...sampledMachine }],
      gaps: [],
    });
  });

  test("reports a frozen summary as a stale-telemetry gap, not as absence", () => {
    const exec: SyncSpawnExec = () => ({ exitCode: 0, stdout: publishedAgo(600_000), stderr: "" });
    const { metrics, gaps } = createSshHostMetricsSampler(exec, home).sample(["debian1"]);
    expect(metrics).toEqual([]);
    expect(gaps[0]?.reason).toBe("telemetry is 600s old (max 90s)");
  });

  test("names clock disagreement for a summary stamped in the future", () => {
    const exec: SyncSpawnExec = () => ({ exitCode: 0, stdout: publishedAgo(-600_000), stderr: "" });
    const { gaps } = createSshHostMetricsSampler(exec, home).sample(["debian1"]);
    expect(gaps[0]?.reason).toBe(
      "telemetry is 600s in the future — clock disagreement",
    );
  });

  test("names the missing field for a summary with no publication stamp", () => {
    const exec: SyncSpawnExec = () => ({
      exitCode: 0,
      stdout: JSON.stringify(machinePayload),
      stderr: "",
    });
    const { metrics, gaps } = createSshHostMetricsSampler(exec, home).sample(["debian1"]);
    expect(metrics).toEqual([]);
    expect(gaps[0]?.reason).toContain("sampledAtEpochMs");
  });

  test("does not leak the publication stamp into the reported metrics", () => {
    const exec: SyncSpawnExec = () => ({ exitCode: 0, stdout: publishedAgo(0), stderr: "" });
    const [metrics] = createSshHostMetricsSampler(exec, home).sample(["debian1"]).metrics;
    expect(metrics && "sampledAtEpochMs" in metrics).toBe(false);
  });

  test("states why for non-zero exit, timeout, invalid JSON, empty file, and schema mismatch", () => {
    const failingExec: SyncSpawnExec = () => ({ exitCode: 1, stdout: "", stderr: "ssh failed" });
    expect(createSshHostMetricsSampler(failingExec, home).sample(["debian1"]).gaps[0]?.reason)
      .toBe(
        "ssh read of .local/state/overdeck/buildbox-telemetry.json exited 1: ssh failed",
      );

    const timedOutExec: SyncSpawnExec = () => ({ exitCode: null, stdout: "", stderr: "" });
    expect(createSshHostMetricsSampler(timedOutExec, home, 8_000).sample(["debian1"]).gaps[0]?.reason)
      .toBe(
        "ssh read of .local/state/overdeck/buildbox-telemetry.json timed out after 8000ms",
      );

    const emptyExec: SyncSpawnExec = () => ({ exitCode: 0, stdout: "  \n", stderr: "" });
    expect(createSshHostMetricsSampler(emptyExec, home).sample(["debian1"]).gaps[0]?.reason)
      .toBe(".local/state/overdeck/buildbox-telemetry.json is empty on debian1");

    const badJsonExec: SyncSpawnExec = () => ({ exitCode: 0, stdout: "not-json", stderr: "" });
    expect(createSshHostMetricsSampler(badJsonExec, home).sample(["debian1"]).gaps[0]?.reason)
      .toContain("is not valid JSON");

    const badShapeExec: SyncSpawnExec = () => ({
      exitCode: 0,
      stdout: JSON.stringify({ sampledAtEpochMs: Date.now(), ...machinePayload, cores: -1 }),
      stderr: "",
    });
    expect(createSshHostMetricsSampler(badShapeExec, home).sample(["debian1"]).gaps[0]?.reason)
      .toContain("does not match the telemetry schema: cores");
  });

  test("returns metrics only for successful hosts in a mixed batch", () => {
    let call = 0;
    const exec: SyncSpawnExec = (cmd) => {
      call += 1;
      const host = cmd[5];
      if (host === "debian2") {
        return { exitCode: 1, stdout: "", stderr: "" };
      }
      return { exitCode: 0, stdout: publishedAgo(0), stderr: "" };
    };

    const sampler = createSshHostMetricsSampler(exec, home);
    const { metrics, gaps } = sampler.sample(["debian1", "debian2", "debian3"]);
    expect(metrics.map(({ host }) => host)).toEqual(["debian1", "debian3"]);
    expect(gaps.map(({ host }) => host)).toEqual(["debian2"]);
    expect(call).toBe(3);
  });

  test("background sampler returns cached metrics without calling inner sample synchronously", async () => {
    let calls = 0;
    const inner: HostMetricsSampler = {
      sample() {
        calls += 1;
        return { metrics: [{ host: "debian1", ...sampledMachine }], gaps: [] };
      },
    };
    const background = createBackgroundHostMetricsSampler(inner, () => ["debian1"], 60_000);
    expect(background.sample(["debian1"])).toEqual({
      metrics: [],
      gaps: [{ host: "debian1", reason: "not sampled yet", at: null }],
    });
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(calls).toBeGreaterThan(0);
    expect(background.sample(["debian1"])).toEqual({
      metrics: [{ host: "debian1", ...sampledMachine }],
      gaps: [],
    });
    background.stop();
  });
});
