import { describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { CapabilityService } from "./capability";
import {
  buildReportFromPod,
  createK3sJobWatcher,
  k3sWatcherConfig,
  loadKubeClient,
  newestTerminatedPodsByJob,
  type K8sFetcher,
} from "./k3s-watcher";
import { ControllerStore } from "./store";

function tempStore() {
  const dir = mkdtempSync(join(tmpdir(), "controller-k3s-watcher-"));
  const store = new ControllerStore(join(dir, "state.sqlite"));
  return { store, capability: new CapabilityService(store), cleanup: () => rmSync(dir, { recursive: true, force: true }) };
}

const KUBECONFIG_YAML = `
apiVersion: v1
kind: Config
current-context: default
clusters:
  - name: default
    cluster:
      server: https://100.101.104.41:6443
      certificate-authority-data: ${Buffer.from("ca-pem").toString("base64")}
users:
  - name: default
    user:
      client-certificate-data: ${Buffer.from("cert-pem").toString("base64")}
      client-key-data: ${Buffer.from("key-pem").toString("base64")}
contexts:
  - name: default
    context:
      cluster: default
      user: default
`;

function pod(overrides: Record<string, unknown> = {}) {
  return {
    metadata: {
      name: "rb-echo-abc123",
      uid: "uid-1",
      creationTimestamp: "2026-08-08T10:00:00Z",
      labels: { "app.kubernetes.io/name": "overdeck-remote-build", "job-name": "rb-echo-abc123" },
      annotations: {
        "overdeck.dev/build-key": "echo hi",
        "overdeck.dev/mirror": "echo-hi",
        "overdeck.dev/repo": "echo-hi",
        "overdeck.dev/snapshot": "abc1234567",
        "overdeck.dev/timeout-sec": "1800",
      },
    },
    spec: {
      nodeName: "debian2",
      containers: [{ name: "build", args: ["exec \"$@\"", "overdeck-remote-build", "echo", "hi"] }],
    },
    status: {
      containerStatuses: [
        { name: "build", state: { terminated: { exitCode: 0, startedAt: "2026-08-08T10:00:01Z", finishedAt: "2026-08-08T10:00:02Z" } } },
      ],
    },
    ...overrides,
  };
}

describe("k3sWatcherConfig", () => {
  test("disabled by default, config flag arms it, env overrides both ways", () => {
    expect(k3sWatcherConfig({}, () => ({})).enabled).toBe(false);
    expect(k3sWatcherConfig({}, () => ({ k3s_enabled: true })).enabled).toBe(true);
    expect(k3sWatcherConfig({ BUILD_REMOTE_K3S: "0" }, () => ({ k3s_enabled: true })).enabled).toBe(false);
    expect(k3sWatcherConfig({ BUILD_REMOTE_K3S: "1" }, () => ({})).enabled).toBe(true);
    expect(k3sWatcherConfig({}, () => { throw new Error("missing"); }).enabled).toBe(false);
  });

  test("namespace/kubeconfig mirror the same config keys as k3sConfig()", () => {
    const cfg = k3sWatcherConfig({}, () => ({ k3s_namespace: "builds", k3s_kubeconfig: "/kc" }));
    expect(cfg.namespace).toBe("builds");
    expect(cfg.kubeconfig).toBe("/kc");
    expect(k3sWatcherConfig({}, () => ({})).namespace).toBe("default");
  });
});

describe("loadKubeClient", () => {
  test("parses server/CA/client-cert from a kubeconfig", () => {
    const client = loadKubeClient("/fake/kubeconfig", () => KUBECONFIG_YAML);
    expect(client.server).toBe("https://100.101.104.41:6443");
    expect(client.ca).toBe("ca-pem");
    expect(client.cert).toBe("cert-pem");
    expect(client.key).toBe("key-pem");
  });

  test("throws on a kubeconfig missing the current context", () => {
    expect(() => loadKubeClient("/fake", () => "current-context: nope\nclusters: []\nusers: []\ncontexts: []")).toThrow();
  });
});

describe("newestTerminatedPodsByJob", () => {
  test("picks the newest pod per job-name, ignores pods without a terminated build container", () => {
    const older = pod({ metadata: { ...pod().metadata, creationTimestamp: "2026-08-08T09:00:00Z" }, status: { containerStatuses: [{ name: "build", state: { terminated: { exitCode: 3, startedAt: "a", finishedAt: "b" } } }] } });
    const newer = pod();
    const running = pod({ metadata: { ...pod().metadata, labels: { "app.kubernetes.io/name": "overdeck-remote-build", "job-name": "rb-running" } }, status: { containerStatuses: [{ name: "build", state: {} }] } });
    const result = newestTerminatedPodsByJob([older, newer, running] as never);
    expect(result).toHaveLength(1);
    expect(result[0]?.exitCode).toBe(0);
  });
});

describe("buildReportFromPod", () => {
  test("builds the emitJobReport() shape from annotations + container args", () => {
    const entry = newestTerminatedPodsByJob([pod()] as never)[0]!;
    const body = buildReportFromPod(entry);
    expect(body).toEqual({
      source: "remote-build",
      host: "debian2",
      key: "echo hi",
      mirror: "echo-hi",
      repo: "echo-hi",
      snapshot: "abc1234567",
      argv: ["echo", "hi"],
      attempt: 1,
      stage: "finished",
      rc: 0,
      startedAt: "2026-08-08T10:00:01Z",
      finishedAt: "2026-08-08T10:00:02Z",
      timeoutSec: 1800,
    });
  });

  test("returns null when the pod is missing report annotations", () => {
    const bare = pod({ metadata: { ...pod().metadata, annotations: {} } });
    const entry = newestTerminatedPodsByJob([bare] as never)[0]!;
    expect(buildReportFromPod(entry)).toBeNull();
  });
});

describe("createK3sJobWatcher", () => {
  test("no-ops without touching the network when k3s_enabled is false", async () => {
    const { store, capability, cleanup } = tempStore();
    let fetchCalls = 0;
    const fetcher: K8sFetcher = async () => { fetchCalls += 1; throw new Error("must not be called"); };
    const watcher = createK3sJobWatcher({
      store, capability, fetcher,
      readConfig: () => ({}),
      scheduleTimer: () => 0,
      clearScheduledTimer: () => {},
    });
    const emitted = await watcher.tick();
    watcher.stop();
    expect(emitted).toBe(0);
    expect(fetchCalls).toBe(0);
    cleanup();
  });

  test("emits a /jobs/report for a newly completed pod and does not re-report it", async () => {
    const { store, capability, cleanup } = tempStore();
    const responses: unknown[] = [];
    const fetcher: K8sFetcher = async (url) => {
      if (url.includes("/log")) return new Response("hi\n", { status: 200 });
      return Response.json({ items: [pod()] });
    };
    const watcher = createK3sJobWatcher({
      store, capability, fetcher,
      readConfig: () => ({ k3s_enabled: true }),
      readKubeconfig: () => KUBECONFIG_YAML,
      scheduleTimer: () => 0,
      clearScheduledTimer: () => {},
      log: (line) => responses.push(line),
    });

    const first = await watcher.tick();
    expect(first).toBe(1);
    expect(store.getJob(require("node:crypto").createHash("sha256").update("echo hiecho-hi", "utf8").digest("hex"))).toMatchObject({ stage: "succeeded", rc: 0 });

    const second = await watcher.tick();
    expect(second).toBe(0);
    watcher.stop();
    cleanup();
  });

  test("tick() rejects when the k3s API is unreachable, so the run loop can back off", async () => {
    const { store, capability, cleanup } = tempStore();
    const fetcher: K8sFetcher = async () => { throw new Error("connection refused"); };
    const watcher = createK3sJobWatcher({
      store, capability, fetcher,
      readConfig: () => ({ k3s_enabled: true }),
      readKubeconfig: () => KUBECONFIG_YAML,
      scheduleTimer: () => 0,
      clearScheduledTimer: () => {},
    });
    await expect(watcher.tick()).rejects.toThrow(/connection refused/);
    watcher.stop();
    cleanup();
  });

  test("run loop backs off on repeated failure and resets on success, never a hot loop", async () => {
    const { store, capability, cleanup } = tempStore();
    let calls = 0;
    const fetcher: K8sFetcher = async () => {
      calls += 1;
      if (calls <= 2) throw new Error("unreachable");
      return Response.json({ items: [] });
    };
    const delays: number[] = [];
    let scheduled: (() => void) | undefined;
    const watcher = createK3sJobWatcher({
      store, capability, fetcher,
      readConfig: () => ({ k3s_enabled: true }),
      readKubeconfig: () => KUBECONFIG_YAML,
      pollIntervalMs: 1000,
      backoffCapMs: 4000,
      scheduleTimer: (fn, ms) => { delays.push(ms); scheduled = fn; return 0; },
      clearScheduledTimer: () => {},
      log: () => {},
    });
    // constructor already scheduled pass #1 at delay 0; drive it and the next two manually.
    for (let i = 0; i < 3 && scheduled; i += 1) {
      const run = scheduled;
      scheduled = undefined;
      await run();
    }
    watcher.stop();
    expect(delays[0]).toBe(0);
    expect(delays[1]).toBe(2000); // failure #1: doubled from the base interval
    expect(delays[2]).toBe(4000); // failure #2: doubled again, capped at 4000
    expect(delays[3]).toBe(1000); // success: reset to the base interval
    cleanup();
  });
});
