import { describe, it, expect, vi, afterEach } from "vitest";
import { GracefulShutdown } from "./graceful-shutdown.js";

afterEach(() => {
  vi.restoreAllMocks();
});

function createShutdown(timeoutMs?: number) {
  const exitSpy = vi.fn<(code: number) => void>();
  const shutdown = new GracefulShutdown({ timeoutMs, exit: exitSpy });
  return { shutdown, exitSpy };
}

describe("GracefulShutdown", () => {
  it("executes steps in registration order", async () => {
    const order: string[] = [];
    const { shutdown, exitSpy } = createShutdown();

    shutdown.register("first", () => { order.push("first"); });
    shutdown.register("second", () => { order.push("second"); });
    shutdown.register("third", () => { order.push("third"); });

    await shutdown.shutdown();

    expect(order).toEqual(["first", "second", "third"]);
    expect(exitSpy).toHaveBeenCalledWith(0);
  });

  it("continues after a failing step", async () => {
    const order: string[] = [];
    const { shutdown, exitSpy } = createShutdown();

    shutdown.register("ok-1", () => { order.push("ok-1"); });
    shutdown.register("fail", () => { throw new Error("boom"); });
    shutdown.register("ok-2", () => { order.push("ok-2"); });

    await shutdown.shutdown();

    expect(order).toEqual(["ok-1", "ok-2"]);
    expect(exitSpy).toHaveBeenCalledWith(0);
  });

  it("handles async steps", async () => {
    const order: string[] = [];
    const { shutdown, exitSpy } = createShutdown();

    shutdown.register("async-step", async () => {
      await new Promise((r) => setTimeout(r, 10));
      order.push("async-done");
    });

    await shutdown.shutdown();

    expect(order).toEqual(["async-done"]);
    expect(exitSpy).toHaveBeenCalledWith(0);
  });

  it("handles async rejection without aborting", async () => {
    const order: string[] = [];
    const { shutdown, exitSpy } = createShutdown();

    shutdown.register("async-fail", async () => {
      throw new Error("async boom");
    });
    shutdown.register("after", () => { order.push("after"); });

    await shutdown.shutdown();

    expect(order).toEqual(["after"]);
    expect(exitSpy).toHaveBeenCalledWith(0);
  });

  it("ignores duplicate shutdown calls", async () => {
    const callCount = { value: 0 };
    const { shutdown } = createShutdown();

    shutdown.register("count", () => { callCount.value++; });

    // Fire two shutdown calls concurrently
    const p1 = shutdown.shutdown();
    const p2 = shutdown.shutdown();
    await Promise.all([p1, p2]);

    expect(callCount.value).toBe(1);
  });

  it("works with zero registered steps", async () => {
    const { shutdown, exitSpy } = createShutdown();
    await shutdown.shutdown();

    expect(exitSpy).toHaveBeenCalledWith(0);
  });

  it("installs signal handlers without throwing", () => {
    const { shutdown } = createShutdown();
    expect(() => shutdown.installSignalHandlers()).not.toThrow();
  });

  it("force-exits after timeout", async () => {
    vi.useFakeTimers();
    const { shutdown, exitSpy } = createShutdown(50);

    shutdown.register("hang", () => new Promise(() => {
      // never resolves
    }));

    // Start shutdown but don't await (it will never finish due to hanging step)
    const promise = shutdown.shutdown();

    // Advance past the timeout
    await vi.advanceTimersByTimeAsync(100);

    expect(exitSpy).toHaveBeenCalledWith(1);

    vi.useRealTimers();
    // Clean up the promise (it's stuck, but we've validated the timeout behavior)
    void promise;
  });
});
