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

const mockWebhookSelect = vi.fn();
const mockInsert = vi.fn();
const mockInsertValues = vi.fn().mockResolvedValue(undefined);

vi.mock("@epicure/db", () => ({
  db: {
    select: vi.fn(() => ({
      from: vi.fn(() => ({
        where: mockWebhookSelect,
      })),
    })),
    insert: vi.fn(() => ({ values: mockInsertValues })),
  },
  webhooks: {},
  webhookDeliveries: {},
  eq: vi.fn(),
  and: vi.fn(),
}));

let fetchCalls: { url: string; body: string; headers: Record<string, string> }[] = [];

global.fetch = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
  const body = (init?.body as string) ?? "";
  fetchCalls.push({
    url: url as string,
    body,
    headers: (init?.headers as Record<string, string>) ?? {},
  });
  return new Response(null, { status: 200 });
});

const { dispatchWebhook } = await import("../webhooks");

beforeEach(() => {
  fetchCalls = [];
  vi.clearAllMocks();
  mockInsert.mockReturnValue({ values: mockInsertValues });
  global.fetch = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
    const body = (init?.body as string) ?? "";
    fetchCalls.push({
      url: url as string,
      body,
      headers: (init?.headers as Record<string, string>) ?? {},
    });
    return new Response(null, { status: 200 });
  });
});

describe("dispatchWebhook", () => {
  it("does not fetch when no matching webhooks", async () => {
    mockWebhookSelect.mockResolvedValue([]);
    await dispatchWebhook("user1", "recipe.created", { id: "r1" });
    expect(global.fetch).not.toHaveBeenCalled();
  });

  it("sends POST to webhook URL with event and payload", async () => {
    mockWebhookSelect.mockResolvedValue([
      { id: "wh1", url: "https://example.com/hook", secret: "mysecret", events: [], active: true },
    ]);

    await dispatchWebhook("user1", "recipe.created", { id: "r1", title: "Test" });

    expect(global.fetch).toHaveBeenCalledOnce();
    const call = fetchCalls[0]!;
    expect(call.url).toBe("https://example.com/hook");
    const parsed = JSON.parse(call.body) as { event: string; payload: unknown };
    expect(parsed.event).toBe("recipe.created");
    expect(parsed.payload).toMatchObject({ id: "r1", title: "Test" });
  });

  it("includes valid HMAC-SHA256 signature header", async () => {
    const secret = "test-webhook-secret";
    mockWebhookSelect.mockResolvedValue([
      { id: "wh1", url: "https://example.com/hook", secret, events: [], active: true },
    ]);

    await dispatchWebhook("user1", "recipe.updated", { id: "r2" });

    const call = fetchCalls[0]!;
    const sigHeader = call.headers["X-Epicure-Signature"];
    expect(sigHeader).toBeTruthy();
    expect(sigHeader).toMatch(/^sha256=/);

    const expectedSig = "sha256=" + crypto.createHmac("sha256", secret).update(call.body).digest("hex");
    expect(sigHeader).toBe(expectedSig);
  });

  it("sends X-Epicure-Event header", async () => {
    mockWebhookSelect.mockResolvedValue([
      { id: "wh1", url: "https://example.com/hook", secret: "s", events: [], active: true },
    ]);

    await dispatchWebhook("user1", "comment.added", {});

    expect(fetchCalls[0]!.headers["X-Epicure-Event"]).toBe("comment.added");
  });

  it("filters webhooks by event subscription", async () => {
    mockWebhookSelect.mockResolvedValue([
      { id: "wh1", url: "https://a.com/hook", secret: "s", events: ["recipe.deleted"], active: true },
      { id: "wh2", url: "https://b.com/hook", secret: "s", events: [], active: true },
    ]);

    await dispatchWebhook("user1", "recipe.created", {});

    // wh1 only subscribes to recipe.deleted, wh2 subscribes to all (empty = all)
    expect(global.fetch).toHaveBeenCalledOnce();
    expect(fetchCalls[0]!.url).toBe("https://b.com/hook");
  });

  it("records delivery success in DB", async () => {
    const { db } = await import("@epicure/db");
    mockWebhookSelect.mockResolvedValue([
      { id: "wh1", url: "https://example.com/hook", secret: "s", events: [], active: true },
    ]);

    await dispatchWebhook("user1", "recipe.created", {});

    expect(db.insert).toHaveBeenCalled();
    const insertedRow = mockInsertValues.mock.calls[0]![0] as Record<string, unknown>;
    expect(insertedRow.success).toBe(true);
    expect(insertedRow.event).toBe("recipe.created");
  });

  it("records delivery failure when fetch throws", async () => {
    const { db } = await import("@epicure/db");
    global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
    mockWebhookSelect.mockResolvedValue([
      { id: "wh1", url: "https://example.com/hook", secret: "s", events: [], active: true },
    ]);

    await dispatchWebhook("user1", "recipe.created", {});

    expect(db.insert).toHaveBeenCalled();
    const insertedRow = mockInsertValues.mock.calls[0]![0] as Record<string, unknown>;
    expect(insertedRow.success).toBe(false);
    expect(insertedRow.statusCode).toBe(0);
  });

  it("still dispatches remaining hooks if one fails (allSettled)", async () => {
    global.fetch = vi.fn()
      .mockRejectedValueOnce(new Error("Timeout"))
      .mockResolvedValueOnce(new Response(null, { status: 200 }));

    mockWebhookSelect.mockResolvedValue([
      { id: "wh1", url: "https://fail.com/hook", secret: "s", events: [], active: true },
      { id: "wh2", url: "https://ok.com/hook", secret: "s", events: [], active: true },
    ]);

    await expect(dispatchWebhook("user1", "recipe.created", {})).resolves.toBeUndefined();
    expect(global.fetch).toHaveBeenCalledTimes(2);
  });
});
