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 { buildControllerStatus } from "./status";
import { ControllerStore } from "./store";

describe("reported job status", () => {
  let dir: string;
  let store: ControllerStore;

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

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

  test("projects stale running jobs at read time without writing", () => {
    store.upsertJob({
      id: "stale-job", repo: "owner/repo", host: "debian1", snapshot: "1",
      stage: "running", attempt: 1, rc: null, infraFailure: false,
      startedAt: "2026-07-20T00:00:00.000Z",
      lastReportAt: "2026-07-20T00:00:05.000Z", timeoutSec: 10,
    });
    const revision = store.getRevision();
    const status = buildControllerStatus(
      store,
      undefined,
      () => Date.parse("2026-07-20T00:00:15.000Z"),
    );
    expect(status.jobs[0]?.stage).toBe("stale");
    expect(store.getJob("stale-job")?.stage).toBe("running");
    expect(store.getRevision()).toBe(revision);
    expect(store.readEvents()).toEqual([]);
  });

  test("bounds terminal job history in status", () => {
    const now = Date.parse("2026-07-20T12:00:00.000Z");
    store.upsertJob({
      id: "running",
      repo: "owner/repo",
      host: "debian1",
      snapshot: "running",
      stage: "running",
      attempt: 1,
      rc: null,
      infraFailure: false,
      startedAt: "2026-07-18T00:00:00.000Z",
    });
    for (let index = 0; index < 101; index += 1) {
      store.upsertJob({
        id: `finished-${String(index).padStart(3, "0")}`,
        repo: "owner/repo",
        host: "debian1",
        snapshot: String(index),
        stage: "succeeded",
        attempt: 1,
        rc: 0,
        infraFailure: false,
        startedAt: new Date(now - (index + 1) * 60_000).toISOString(),
        finishedAt: new Date(now - index * 60_000).toISOString(),
      });
    }

    const jobs = buildControllerStatus(store, undefined, () => now).jobs;
    expect(jobs.filter((job) => job.rc !== null)).toHaveLength(100);
    expect(jobs.some((job) => job.id === "running")).toBe(true);
  });

  test("derives observed 24h route, success, and exit-127 samples from the job ledger", () => {
    const now = Date.parse("2026-07-20T12:00:00.000Z");
    const jobs = [
      { id: "ok", host: "debian1", startedAt: "2026-07-20T10:00:00.000Z", finishedAt: "2026-07-20T10:10:00.000Z", rc: 0 },
      { id: "missing", host: "debian2", startedAt: "2026-07-20T09:00:00.000Z", finishedAt: "2026-07-20T09:10:00.000Z", rc: 127 },
      { id: "running", host: "debian1", startedAt: "2026-07-20T08:00:00.000Z", rc: null },
      { id: "old", host: "debian1", startedAt: "2026-07-19T11:59:59.000Z", finishedAt: "2026-07-19T12:10:00.000Z", rc: 0 },
    ];
    for (const job of jobs) {
      store.upsertJob({
        id: job.id, repo: "owner/repo", host: job.host, snapshot: job.id,
        stage: job.rc === null ? "running" : job.rc === 0 ? "succeeded" : "failed",
        attempt: 1, rc: job.rc, infraFailure: false, startedAt: job.startedAt,
        ...("finishedAt" in job ? { finishedAt: job.finishedAt } : {}),
      });
    }

    expect(buildControllerStatus(store, undefined, () => now).kpis).toEqual({
      remote24h: 3,
      remoteSuccessPct: 50,
      exit127: { count: 1, hosts: ["debian2"] },
    });
  });

  test("leaves success unrecorded when no 24h job has a terminal result", () => {
    store.upsertJob({
      id: "running", repo: "owner/repo", host: "debian1", snapshot: "1",
      stage: "running", attempt: 1, rc: null, infraFailure: false,
      startedAt: "2026-07-20T10:00:00.000Z",
    });
    expect(buildControllerStatus(
      store,
      undefined,
      () => Date.parse("2026-07-20T12:00:00.000Z"),
    ).kpis).toEqual({ remote24h: 1, exit127: { count: 0, hosts: [] } });
  });

  test("surfaces only recorded land conduct health keyed by repository root", () => {
    store.recordLandConductResult("/repo/b", "2026-08-14T12:00:00.000Z", false, "rc=7");
    store.recordLandConductResult("/repo/a", "2026-08-14T12:01:00.000Z", true, "");

    expect(buildControllerStatus(store).landConduct).toEqual({
      "/repo/a": {
        lastPassAt: "2026-08-14T12:01:00.000Z", lastOk: true,
        lastDetail: "", consecutiveFailures: 0,
      },
      "/repo/b": {
        lastPassAt: "2026-08-14T12:00:00.000Z", lastOk: false,
        lastDetail: "rc=7", consecutiveFailures: 1,
      },
    });
  });

  test("reports deployWatcher as null when the watcher has never recorded a result", () => {
    expect(buildControllerStatus(store).deployWatcher).toBeNull();
  });

  test("surfaces the deploy watcher's last recorded result", () => {
    store.recordDeployWatcherResult({
      targetSha: "a".repeat(40),
      attempts: 2,
      lastStatus: "deploy-clone-dirty",
      lastDetail: "local changes",
      lastAt: "2026-08-15T12:00:00.000Z",
      lastOk: false,
      failureClass: "transient",
      nextRetryAt: "2026-08-15T12:05:00.000Z",
    });

    expect(buildControllerStatus(store).deployWatcher).toEqual({
      targetSha: "a".repeat(40),
      attempts: 2,
      lastStatus: "deploy-clone-dirty",
      lastDetail: "local changes",
      lastAt: "2026-08-15T12:00:00.000Z",
      lastOk: false,
      failureClass: "transient",
      nextRetryAt: "2026-08-15T12:05:00.000Z",
    });
  });
});
