import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

describe("webhook delivery", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    vi.resetModules();
  });

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

  it("delivers a batch of events", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://siem.example.com/input", "Bearer test-token", [
      { event: "LOGIN_SUCCESS", timestamp: "2026-01-01T00:00:00Z" },
    ]);

    expect(result.success).toBe(true);
    expect(result.attempts).toBe(1);
    expect(fetchMock).toHaveBeenCalledOnce();
    const [url, opts] = fetchMock.mock.calls[0];
    expect(url).toBe("https://siem.example.com/input");
    expect(opts.headers.Authorization).toBe("Bearer test-token");
    const body = JSON.parse(opts.body);
    expect(body.source).toBe("snapotter");
    expect(body.version).toBe("1");
    expect(body.events).toHaveLength(1);
  });

  it("retries on server error", async () => {
    const fetchMock = vi
      .fn()
      .mockResolvedValueOnce({ ok: false, status: 502 })
      .mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://siem.example.com/input", "", [{ event: "test" }], {
      maxRetries: 3,
      initialDelayMs: 1,
    });

    expect(result.success).toBe(true);
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("does not retry on 4xx client errors", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 401 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://siem.example.com/input", "", [{ event: "test" }], {
      maxRetries: 3,
      initialDelayMs: 1,
    });

    expect(result.success).toBe(false);
    expect(result.statusCode).toBe(401);
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("retries on network errors", async () => {
    const fetchMock = vi
      .fn()
      .mockRejectedValueOnce(new Error("connection refused"))
      .mockRejectedValueOnce(new Error("connection refused"))
      .mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://siem.example.com/input", "", [{ event: "test" }], {
      maxRetries: 3,
      initialDelayMs: 1,
    });

    expect(result.success).toBe(true);
    expect(fetchMock).toHaveBeenCalledTimes(3);
  });

  it("fails after exhausting retries", async () => {
    const fetchMock = vi.fn().mockRejectedValue(new Error("down"));
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://siem.example.com/input", "", [{ event: "test" }], {
      maxRetries: 2,
      initialDelayMs: 1,
    });

    expect(result.success).toBe(false);
    expect(result.error).toBe("down");
    expect(result.attempts).toBe(3); // initial + 2 retries
    expect(fetchMock).toHaveBeenCalledTimes(3);
  });

  it("omits Authorization header when auth is empty", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    await deliverWebhook("https://example.com", "", [{ event: "test" }]);

    const headers = fetchMock.mock.calls[0][1].headers;
    expect(headers.Authorization).toBeUndefined();
  });

  it("returns error when request times out", async () => {
    const abortError = new Error("The operation was aborted");
    abortError.name = "AbortError";
    const fetchMock = vi.fn().mockRejectedValue(abortError);
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://example.com", "", [{ event: "test" }], {
      maxRetries: 0,
      initialDelayMs: 1,
    });

    expect(result.success).toBe(false);
    expect(result.error).toBe("The operation was aborted");
    expect(result.attempts).toBe(1);
  });

  it("follows exponential backoff delay pattern", async () => {
    vi.useFakeTimers();
    const fetchMock = vi.fn().mockRejectedValue(new Error("down"));
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const promise = deliverWebhook("https://example.com", "", [{ event: "test" }], {
      maxRetries: 3,
      initialDelayMs: 1000,
    });

    expect(fetchMock).toHaveBeenCalledTimes(1);

    await vi.advanceTimersByTimeAsync(1000);
    expect(fetchMock).toHaveBeenCalledTimes(2);

    await vi.advanceTimersByTimeAsync(2000);
    expect(fetchMock).toHaveBeenCalledTimes(3);

    await vi.advanceTimersByTimeAsync(4000);
    expect(fetchMock).toHaveBeenCalledTimes(4);

    const result = await promise;
    expect(result.success).toBe(false);
    expect(result.attempts).toBe(4);
    vi.useRealTimers();
  });

  it("limits retries when maxRetries is 1", async () => {
    const fetchMock = vi.fn().mockRejectedValue(new Error("down"));
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://example.com", "", [{ event: "test" }], {
      maxRetries: 1,
      initialDelayMs: 1,
    });

    expect(result.success).toBe(false);
    expect(result.attempts).toBe(2);
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("makes no retries when maxRetries is 0", async () => {
    const fetchMock = vi.fn().mockRejectedValue(new Error("down"));
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://example.com", "", [{ event: "test" }], {
      maxRetries: 0,
      initialDelayMs: 1,
    });

    expect(result.success).toBe(false);
    expect(result.attempts).toBe(1);
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("applies custom initialDelayMs to backoff schedule", async () => {
    vi.useFakeTimers();
    const fetchMock = vi.fn().mockRejectedValue(new Error("down"));
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const promise = deliverWebhook("https://example.com", "", [{ event: "test" }], {
      maxRetries: 2,
      initialDelayMs: 500,
    });

    expect(fetchMock).toHaveBeenCalledTimes(1);

    await vi.advanceTimersByTimeAsync(500);
    expect(fetchMock).toHaveBeenCalledTimes(2);

    await vi.advanceTimersByTimeAsync(1000);
    expect(fetchMock).toHaveBeenCalledTimes(3);

    const result = await promise;
    expect(result.success).toBe(false);
    expect(result.attempts).toBe(3);
    vi.useRealTimers();
  });

  it("sends payload with source, version, and events fields", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const events = [
      { event: "FILE_UPLOADED", userId: "u1" },
      { event: "FILE_DELETED", userId: "u2" },
    ];
    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    await deliverWebhook("https://example.com/webhook", "", events);

    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body).toEqual({
      source: "snapotter",
      version: "1",
      events,
    });
  });

  it("sends Content-Type application/json header", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    await deliverWebhook("https://example.com", "", [{ event: "test" }]);

    const headers = fetchMock.mock.calls[0][1].headers;
    expect(headers["Content-Type"]).toBe("application/json");
  });

  it("delivers empty events array", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://example.com", "", []);

    expect(result.success).toBe(true);
    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body.events).toEqual([]);
  });

  it("handles very large events array", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const events = Array.from({ length: 1000 }, (_, i) => ({ event: "BULK_OP", index: i }));
    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook("https://example.com", "", events);

    expect(result.success).toBe(true);
    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body.events).toHaveLength(1000);
  });

  it("sends to URL with path and query components", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    const result = await deliverWebhook(
      "https://api.example.com/v2/webhooks/ingest?source=snapotter",
      "",
      [{ event: "test" }],
    );

    expect(result.success).toBe(true);
    expect(fetchMock.mock.calls[0][0]).toBe(
      "https://api.example.com/v2/webhooks/ingest?source=snapotter",
    );
  });

  it("passes through Authorization header with Bearer prefix", async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    vi.stubGlobal("fetch", fetchMock);

    const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
    await deliverWebhook("https://example.com", "Bearer sk-live-abc123", [{ event: "test" }]);

    const headers = fetchMock.mock.calls[0][1].headers;
    expect(headers.Authorization).toBe("Bearer sk-live-abc123");
  });
});
