import { describe, it, expect, vi, beforeEach } from "vitest";
import { healthMiddleware } from "./health.js";
import type { ApiClient } from "@/shared/api/client.js";

function createMockApiClient(
  behavior: "ok" | "error" | "timeout" = "ok",
): ApiClient {
  const client = {
    get: vi.fn(),
    post: vi.fn(),
  } as unknown as ApiClient;

  if (behavior === "ok") {
    vi.mocked(client.get).mockResolvedValue({ status: "ok" });
  } else if (behavior === "error") {
    vi.mocked(client.get).mockRejectedValue(new Error("upstream down"));
  } else {
    vi.mocked(client.get).mockImplementation(
      () => new Promise(() => {}), // never resolves
    );
  }

  return client;
}

function createMockRes() {
  const res = {
    statusCode: 200,
    status: vi.fn().mockReturnThis(),
    json: vi.fn().mockReturnThis(),
    setHeader: vi.fn(),
  };
  return res;
}

function createRequest() {
  return {} as Parameters<ReturnType<typeof healthMiddleware>>[0];
}

describe("healthMiddleware", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  it("returns 200 when upstream is reachable", async () => {
    const apiClient = createMockApiClient("ok");
    const handler = healthMiddleware({ apiClient });

    const req = createRequest();
    const res = createMockRes();
    const next = vi.fn();

    await handler(req, res, next);

    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.json).toHaveBeenCalledWith({ status: "ok" });
  });

  it("returns 503 when upstream ping fails", async () => {
    const apiClient = createMockApiClient("error");
    const handler = healthMiddleware({ apiClient });

    const req = createRequest();
    const res = createMockRes();
    const next = vi.fn();

    await handler(req, res, next);

    expect(res.status).toHaveBeenCalledWith(503);
    expect(res.json).toHaveBeenCalledWith({
      status: "degraded",
      reason: "upstream_unreachable",
    });
  });

  it("returns 200 if last success is within 60s even if current ping fails", async () => {
    const apiClient = createMockApiClient("ok");
    const handler = healthMiddleware({ apiClient });

    const req = createRequest();
    const res1 = createMockRes();
    await handler(req, res1, vi.fn());
    expect(res1.status).toHaveBeenCalledWith(200);

    // Now make it fail
    vi.mocked(apiClient.get).mockRejectedValue(new Error("fail"));

    // Advance 30s — still within 60s window
    vi.advanceTimersByTime(30_000);

    const res2 = createMockRes();
    await handler(req, res2, vi.fn());
    expect(res2.status).toHaveBeenCalledWith(200);
  });

  it("returns 503 if last success is older than 60s", async () => {
    const apiClient = createMockApiClient("ok");
    const handler = healthMiddleware({ apiClient });

    const req = createRequest();
    const res1 = createMockRes();
    await handler(req, res1, vi.fn());
    expect(res1.status).toHaveBeenCalledWith(200);

    // Now make it fail
    vi.mocked(apiClient.get).mockRejectedValue(new Error("fail"));

    // Advance 61s — beyond 60s window
    vi.advanceTimersByTime(61_000);

    const res2 = createMockRes();
    await handler(req, res2, vi.fn());
    expect(res2.status).toHaveBeenCalledWith(503);
  });

  it("respects custom upstreamTimeoutMs", async () => {
    const apiClient = createMockApiClient("timeout");
    const handler = healthMiddleware({
      apiClient,
      upstreamTimeoutMs: 100,
    });

    const req = createRequest();
    const res = createMockRes();
    const next = vi.fn();

    // The handler should abort the ping after 100ms
    const promise = handler(req, res, next);
    vi.advanceTimersByTime(200);
    await promise;

    expect(res.status).toHaveBeenCalledWith(503);
  });

  it("does not call next — health is a terminal response", async () => {
    const apiClient = createMockApiClient("ok");
    const handler = healthMiddleware({ apiClient });

    const req = createRequest();
    const res = createMockRes();
    const next = vi.fn();

    await handler(req, res, next);

    expect(next).not.toHaveBeenCalled();
  });
});
