import { describe, expect, test } from "bun:test";
import {
  OffLaptopWatchdog,
  WATCHDOG_PROBE_TIMEOUT_MS,
  type WatchdogNotification,
  type WatchdogTarget,
} from "./watchdog";

const targets: WatchdogTarget[] = [
  {
    name: "controller",
    url: "http://laptop:8787/heartbeat",
    restartArgv: ["systemctl", "--user", "restart", "overdeck-controller"],
  },
  {
    name: "collector",
    url: "http://laptop:3001/health",
    restartArgv: ["systemctl", "--user", "restart", "overdeck-collector"],
  },
];

describe("off-laptop watchdog", () => {
  test("healthy controller and collector cause no action", async () => {
    const fixture = watchdogFixture(async () => Response.json({ ok: true }));

    await fixture.watchdog.pollOnce();

    expect(fixture.restarts).toEqual([]);
    expect(fixture.notifications).toEqual([]);
  });

  test("controller failure restarts once then pages once while collector remains healthy", async () => {
    const fixture = watchdogFixture(async (url) => {
      if (url.includes("8787")) throw new Error("controller unavailable");
      return Response.json({ ok: true });
    });

    await fixture.watchdog.pollOnce();
    await fixture.watchdog.pollOnce();

    expect(fixture.restarts).toEqual([["controller", targets[0]!.restartArgv!]]);
    expect(fixture.notifications.map(({ target }) => target)).toEqual(["controller"]);
  });

  test("collector failure is detected independently of healthy controller", async () => {
    const fixture = watchdogFixture(async (url) => {
      if (url.includes("3001")) return new Response("down", { status: 503 });
      return Response.json({ ok: true, revision: 4, lastEventTs: null });
    });

    await fixture.watchdog.pollOnce();

    expect(fixture.restarts).toEqual([["collector", targets[1]!.restartArgv!]]);
    expect(fixture.notifications.map(({ target }) => target)).toEqual(["collector"]);
  });

  test("both failures each receive one independent restart and page", async () => {
    const fixture = watchdogFixture(async () => {
      throw new Error("laptop unavailable");
    });

    await fixture.watchdog.pollOnce();

    expect(fixture.restarts).toEqual([
      ["controller", targets[0]!.restartArgv!],
      ["collector", targets[1]!.restartArgv!],
    ]);
    expect(fixture.notifications.map(({ target }) => target)).toEqual(["controller", "collector"]);
  });

  test("recovery rearms paging for a later failure", async () => {
    let controllerHealthy = false;
    const fixture = watchdogFixture(async (url) => {
      if (url.includes("8787") && !controllerHealthy) throw new Error("down");
      return Response.json({ ok: true });
    });

    await fixture.watchdog.pollOnce();
    controllerHealthy = true;
    await fixture.watchdog.pollOnce();
    controllerHealthy = false;
    await fixture.watchdog.pollOnce();

    expect(fixture.restarts).toEqual([
      ["controller", targets[0]!.restartArgv!],
      ["controller", targets[0]!.restartArgv!],
    ]);
    expect(fixture.notifications).toHaveLength(2);
  });

  test("sends bearer token to both observation endpoints", async () => {
    const authorizations: Array<string | null> = [];
    const fixture = watchdogFixture(async (_url, init) => {
      authorizations.push(new Headers(init?.headers).get("authorization"));
      return Response.json({ ok: true });
    });

    await fixture.watchdog.pollOnce();

    expect(authorizations).toEqual(["Bearer observer-token", "Bearer observer-token"]);
  });

  test("observe-only targets notify without restart when restartArgv is absent or empty", async () => {
    const observeOnlyTargets: WatchdogTarget[] = [
      { name: "controller", url: "http://laptop:8787/heartbeat" },
      { name: "collector", url: "http://laptop:3001/health", restartArgv: [] },
    ];
    const restarts: string[] = [];
    const notifications: WatchdogNotification[] = [];
    const watchdog = new OffLaptopWatchdog({
      targets: observeOnlyTargets,
      token: "observer-token",
      fetcher: async () => { throw new Error("down"); },
      clock: { sleep: async () => {} },
      restarter: {
        restart: async (target) => { restarts.push(target); },
      },
      notifier: { notify: async (notification) => { notifications.push(notification); } },
    });

    await watchdog.pollOnce();

    expect(restarts).toEqual([]);
    expect(notifications.map(({ target }) => target)).toEqual(["controller", "collector"]);
  });

  test("health fetch uses a 10s abort timeout signal", async () => {
    const signals: AbortSignal[] = [];
    const fixture = watchdogFixture(async (_url, init) => {
      if (init?.signal) signals.push(init.signal);
      return Response.json({ ok: true });
    });

    await fixture.watchdog.pollOnce();

    expect(signals).toHaveLength(2);
    for (const signal of signals) {
      expect(signal).toBeInstanceOf(AbortSignal);
    }
  });

  test("aborted probe is treated as a failed health check", async () => {
    const fixture = watchdogFixture(async (url, init) => {
      if (!url.includes("8787")) return Response.json({ ok: true });
      await new Promise<void>((_resolve, reject) => {
        const signal = init?.signal;
        if (!signal) throw new Error("missing abort signal");
        if (signal.aborted) {
          reject(signal.reason);
          return;
        }
        signal.addEventListener("abort", () => {
          reject(signal.reason);
        }, { once: true });
      });
      return Response.json({ ok: true });
    }, { probeTimeoutMs: 25 });

    await fixture.watchdog.pollOnce();

    expect(fixture.restarts).toEqual([["controller", targets[0]!.restartArgv!]]);
    expect(fixture.notifications).toHaveLength(1);
    expect(fixture.notifications[0]?.target).toBe("controller");
  });
});

function watchdogFixture(
  fetcher: (url: string, init?: RequestInit) => Promise<Response>,
  options?: { probeTimeoutMs?: number },
): {
  watchdog: OffLaptopWatchdog;
  restarts: Array<[string, string[]]>;
  notifications: WatchdogNotification[];
} {
  const restarts: Array<[string, string[]]> = [];
  const notifications: WatchdogNotification[] = [];
  return {
    watchdog: new OffLaptopWatchdog({
      targets,
      token: "observer-token",
      fetcher,
      clock: { sleep: async () => {} },
      restarter: {
        restart: async (target, restartArgv) => { restarts.push([target, restartArgv]); },
      },
      notifier: { notify: async (notification) => { notifications.push(notification); } },
      probeTimeoutMs: options?.probeTimeoutMs ?? WATCHDOG_PROBE_TIMEOUT_MS,
    }),
    restarts,
    notifications,
  };
}
