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

vi.mock("~/server/stripe", () => ({
  stripe: {
    customers: { create: vi.fn() },
    checkout: { sessions: { create: vi.fn() } },
    billingPortal: { sessions: { create: vi.fn() } },
    subscriptions: { update: vi.fn(), retrieve: vi.fn() },
    invoices: { list: vi.fn() },
    webhooks: { constructEvent: vi.fn() },
  },
}));

vi.mock("~/server/db", () => ({
  db: {
    select: vi.fn(),
    insert: vi.fn(),
    update: vi.fn(),
    query: {
      subscriptions: {
        findFirst: vi.fn(),
      },
    },
  },
}));

import { stripe } from "~/server/stripe";
import { db } from "~/server/db";
import {
  getOrCreateCustomer,
  createCheckoutSession,
  createPortalSession,
  cancelSubscription,
  reactivateSubscription,
  listInvoices,
  handleWebhookEvent,
} from "./billing.service";

beforeEach(() => {
  vi.clearAllMocks();
});

describe("getOrCreateCustomer", () => {
  it("returns existing stripeCustomerId if present", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([
            { id: "u1", email: "a@b.com", stripeCustomerId: "cus_existing" },
          ]),
        }),
      }),
    });

    const result = await getOrCreateCustomer("u1", "a@b.com");
    expect(result).toBe("cus_existing");
    expect(stripe.customers.create).not.toHaveBeenCalled();
  });

  it("creates a new Stripe customer when no stripeCustomerId", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([
            { id: "u1", email: "a@b.com", stripeCustomerId: null },
          ]),
        }),
      }),
    });

    (stripe.customers.create as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "cus_new" });

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([{ id: "u1" }]),
      }),
    });

    const result = await getOrCreateCustomer("u1", "a@b.com");
    expect(result).toBe("cus_new");
    expect(stripe.customers.create).toHaveBeenCalledWith({
      email: "a@b.com",
      metadata: { userId: "u1" },
    });
  });

  it("throws NOT_FOUND for missing user", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([]),
        }),
      }),
    });

    await expect(getOrCreateCustomer("u-missing", "x@y.com")).rejects.toThrow(
      "User not found",
    );
  });
});

describe("createCheckoutSession", () => {
  it("creates a Stripe checkout session and returns URL", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([
            { id: "u1", email: "a@b.com", stripeCustomerId: "cus_123" },
          ]),
        }),
      }),
    });

    (stripe.checkout.sessions.create as ReturnType<typeof vi.fn>).mockResolvedValue({
      url: "https://checkout.stripe.com/session_123",
      id: "session_123",
    });

    const result = await createCheckoutSession(
      "u1",
      "a@b.com",
      "price_basic",
      "https://example.com/success",
      "https://example.com/cancel",
    );

    expect(result.url).toBe("https://checkout.stripe.com/session_123");
    expect(result.sessionId).toBe("session_123");
  });
});

describe("createPortalSession", () => {
  it("creates a Stripe billing portal session", async () => {
    (stripe.billingPortal.sessions.create as ReturnType<typeof vi.fn>).mockResolvedValue({
      url: "https://billing.stripe.com/portal/session_456",
    });

    const result = await createPortalSession(
      "cus_123",
      "https://example.com/return",
    );

    expect(result.url).toBe("https://billing.stripe.com/portal/session_456");
  });
});

describe("cancelSubscription", () => {
  it("sets cancel_at_period_end on Stripe subscription", async () => {
    (stripe.subscriptions.update as ReturnType<typeof vi.fn>).mockResolvedValue({});
    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([]),
      }),
    });

    const result = await cancelSubscription("sub_123");
    expect(result.cancelAtPeriodEnd).toBe(true);
    expect(stripe.subscriptions.update).toHaveBeenCalledWith("sub_123", {
      cancel_at_period_end: true,
    });
  });
});

describe("reactivateSubscription", () => {
  it("removes cancel_at_period_end on Stripe subscription", async () => {
    (stripe.subscriptions.update as ReturnType<typeof vi.fn>).mockResolvedValue({});
    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([]),
      }),
    });

    const result = await reactivateSubscription("sub_123");
    expect(result.cancelAtPeriodEnd).toBe(false);
    expect(stripe.subscriptions.update).toHaveBeenCalledWith("sub_123", {
      cancel_at_period_end: false,
    });
  });
});

describe("listInvoices", () => {
  it("returns invoices list from Stripe", async () => {
    (stripe.invoices.list as ReturnType<typeof vi.fn>).mockResolvedValue({
      data: [{ id: "in_1" }, { id: "in_2" }],
      has_more: false,
    });

    const result = await listInvoices("cus_123", 10);
    expect(result.invoices).toHaveLength(2);
    expect(result.hasMore).toBe(false);
  });
});

describe("handleWebhookEvent", () => {
  it("handles checkout.session.completed", async () => {
    (db.insert as ReturnType<typeof vi.fn>).mockReturnValue({
      values: vi.fn().mockReturnValue({
        onConflictDoNothing: vi.fn().mockResolvedValue(undefined),
      }),
    });

    (stripe.subscriptions.retrieve as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: "sub_new",
      items: { data: [{ price: { id: "price_premium" } }] },
      current_period_start: 1700000000,
      current_period_end: 1702592000,
      status: "active",
      cancel_at_period_end: false,
    });

    await handleWebhookEvent({
      type: "checkout.session.completed",
      data: {
        object: {
          metadata: { userId: "u1" },
          subscription: "sub_new",
        },
      },
    } as never);

    expect(db.insert).toHaveBeenCalled();
  });

  it("handles invoice.paid", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([{ id: "sub_db_1" }]),
        }),
      }),
    });

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          returning: vi.fn().mockResolvedValue([{ id: "sub_db_1", status: "active" }]),
        }),
      }),
    });

    await handleWebhookEvent({
      type: "invoice.paid",
      data: {
        object: {
          subscription: "sub_123",
        },
      },
    } as never);
  });

  it("handles invoice.payment_failed", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([{ id: "sub_db_1" }]),
        }),
      }),
    });

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          returning: vi.fn().mockResolvedValue([{ id: "sub_db_1", status: "past_due" }]),
        }),
      }),
    });

    await handleWebhookEvent({
      type: "invoice.payment_failed",
      data: {
        object: {
          subscription: "sub_123",
        },
      },
    } as never);
  });

  it("handles customer.subscription.updated", async () => {
    (db.query.subscriptions.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue(null);
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([]),
        }),
      }),
    });

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          returning: vi.fn().mockResolvedValue([{ id: "sub_db_1", status: "active" }]),
        }),
      }),
    });

    await handleWebhookEvent({
      type: "customer.subscription.updated",
      data: {
        object: {
          id: "sub_123",
          metadata: { userId: "u1" },
          items: { data: [{ price: { id: "price_plus" } }] },
          current_period_start: 1700000000,
          current_period_end: 1702592000,
          status: "active",
          cancel_at_period_end: false,
        },
      },
    } as never);
  });

  it("handles customer.subscription.deleted", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([{ id: "sub_db_1" }]),
        }),
      }),
    });

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          returning: vi.fn().mockResolvedValue([{ id: "sub_db_1", status: "canceled" }]),
        }),
      }),
    });

    await handleWebhookEvent({
      type: "customer.subscription.deleted",
      data: {
        object: {
          id: "sub_123",
        },
      },
    } as never);
  });
});
