/* Tests for createCheckoutSession — multi-currency + extra seats.

   The action is heavy (auth guard, DB, Stripe SDK). We mock everything
   below the action's own logic so we can assert the exact arguments
   passed to Stripe. */

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

/* ---------- module mocks ---------- */

/* Stripe price IDs from BILLABLE_PLANS / SEAT_ADDON come from env at module
   load. We freeze them here so the assertions are deterministic regardless
   of the developer's local .env.local. */
vi.mock("@/lib/billing/plans", async () => {
  const actual = await vi.importActual<typeof import("@/lib/billing/plans")>("@/lib/billing/plans");
  return {
    ...actual,
    BILLABLE_PLANS: {
      ...actual.BILLABLE_PLANS,
      starter: {
        ...actual.BILLABLE_PLANS.starter,
        stripePriceIds: { month: "price_starter_month", year: "price_starter_year" },
      },
      pro: {
        ...actual.BILLABLE_PLANS.pro,
        stripePriceIds: { month: "price_pro_month", year: "price_pro_year" },
      },
      business: {
        ...actual.BILLABLE_PLANS.business,
        stripePriceIds: { month: "price_business_month", year: "price_business_year" },
      },
    },
    SEAT_ADDON: {
      ...actual.SEAT_ADDON,
      stripePriceIds: { month: "price_seat_month", year: undefined },
    },
  };
});

/* env — provide minimal shape so importing the action doesn't trip
   process.env validation in jsdom-less environments. */
vi.mock("@/lib/env", () => ({
  env: {
    BETTER_AUTH_URL: "https://test.elevaseo.com",
    STRIPE_TAX_ENABLED: false,
  },
}));

/* Auth guard always returns the same active org id. */
vi.mock("@/lib/auth-guards", () => ({
  requireOrgMember: vi.fn(async () => ({ activeOrganizationId: "org_test_123" })),
}));

vi.mock("@/lib/billing/customer", () => ({
  ensureStripeCustomer: vi.fn(async () => "cus_test_abc"),
}));

type StripeSessionCreateParams = Record<string, unknown>;
type StripeSessionResult = { url: string };

const sessionsCreateMock = vi.fn<
  (params: StripeSessionCreateParams) => Promise<StripeSessionResult>
>(async () => ({ url: "https://checkout.stripe.com/c/pay/cs_test" }));

vi.mock("@/lib/billing/stripe", () => ({
  getStripe: vi.fn(() => ({
    checkout: { sessions: { create: sessionsCreateMock } },
  })),
}));

/* db is imported by the action module but only used in cancelAtPeriodEnd /
   getCurrentPlanForOrg, neither of which we test here. A stub is enough
   to make the import resolve. */
vi.mock("@/lib/db", () => ({ db: {} }));

/* drizzle's `eq` is imported at the top of the action file but only used
   by the cancel/get-plan helpers. A stub suffices. */
vi.mock("drizzle-orm", () => ({ eq: vi.fn() }));

/* next/cache revalidatePath is only used by cancelAtPeriodEnd. */
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));

/* ---------- system under test ---------- */

import { createCheckoutSession } from "./billing";

beforeEach(() => {
  sessionsCreateMock.mockClear();
  sessionsCreateMock.mockResolvedValue({ url: "https://checkout.stripe.com/c/pay/cs_test" });
});

describe("createCheckoutSession — happy paths", () => {
  it("creates a tier-only session with currency=eur and extraSeats=0", async () => {
    const result = await createCheckoutSession({
      plan: "pro",
      interval: "month",
      currency: "eur",
    });

    expect(result).toEqual({ url: "https://checkout.stripe.com/c/pay/cs_test" });
    expect(sessionsCreateMock).toHaveBeenCalledTimes(1);

    const params = sessionsCreateMock.mock.calls[0]![0] as Record<string, unknown>;
    expect(params.currency).toBe("eur");
    expect(params.line_items).toEqual([{ price: "price_pro_month", quantity: 1 }]);
    expect(params.metadata).toMatchObject({
      orgId: "org_test_123",
      plan: "pro",
      interval: "month",
      currency: "eur",
      extraSeats: "0",
    });
    expect(params.mode).toBe("subscription");
    expect(params.customer).toBe("cus_test_abc");
    expect(params.allow_promotion_codes).toBe(true);
    expect(params.billing_address_collection).toBe("required");
    /* CRITICAL: we must NOT pass payment_method_types. */
    expect(params.payment_method_types).toBeUndefined();
  });

  it("appends a seat add-on line item when extraSeats > 0 (currency=usd, 3 seats, monthly)", async () => {
    await createCheckoutSession({
      plan: "starter",
      interval: "month",
      currency: "usd",
      extraSeats: 3,
    });

    expect(sessionsCreateMock).toHaveBeenCalledTimes(1);
    const params = sessionsCreateMock.mock.calls[0]![0] as Record<string, unknown>;

    expect(params.currency).toBe("usd");

    const lineItems = params.line_items as Array<{ price: string; quantity: number }>;
    expect(lineItems).toHaveLength(2);
    expect(lineItems[0]).toEqual({ price: "price_starter_month", quantity: 1 });
    expect(lineItems[1]).toEqual({ price: "price_seat_month", quantity: 3 });

    expect(params.metadata).toMatchObject({
      currency: "usd",
      extraSeats: "3",
      plan: "starter",
      interval: "month",
    });

    /* success_url / cancel_url should carry the choice as query params. */
    const successUrl = String(params.success_url);
    expect(successUrl).toContain("session_id={CHECKOUT_SESSION_ID}");
    expect(successUrl).toContain("currency=usd");
    expect(successUrl).toContain("extraSeats=3");
    const cancelUrl = String(params.cancel_url);
    expect(cancelUrl).toContain("currency=usd");
    expect(cancelUrl).toContain("extraSeats=3");
  });

  it("omits the seat line item when extraSeats is explicitly 0", async () => {
    await createCheckoutSession({
      plan: "business",
      interval: "year",
      currency: "jpy",
      extraSeats: 0,
    });

    const params = sessionsCreateMock.mock.calls[0]![0] as Record<string, unknown>;
    const lineItems = params.line_items as Array<{ price: string; quantity: number }>;
    expect(lineItems).toHaveLength(1);
    expect(lineItems[0]).toEqual({ price: "price_business_year", quantity: 1 });
    expect(params.currency).toBe("jpy");
    expect((params.metadata as Record<string, string>).extraSeats).toBe("0");
  });
});

describe("createCheckoutSession — validation failures", () => {
  it("throws when currency is not a supported code", async () => {
    await expect(
      createCheckoutSession({
        plan: "pro",
        interval: "month",
        /* @ts-expect-error — intentional invalid input. */
        currency: "xyz",
      }),
    ).rejects.toThrow();
    expect(sessionsCreateMock).not.toHaveBeenCalled();
  });

  it("throws when extraSeats is negative", async () => {
    await expect(
      createCheckoutSession({
        plan: "pro",
        interval: "month",
        currency: "eur",
        extraSeats: -1,
      }),
    ).rejects.toThrow();
    expect(sessionsCreateMock).not.toHaveBeenCalled();
  });

  it("throws when extraSeats is not an integer", async () => {
    await expect(
      createCheckoutSession({
        plan: "pro",
        interval: "month",
        currency: "eur",
        extraSeats: 1.5,
      }),
    ).rejects.toThrow();
    expect(sessionsCreateMock).not.toHaveBeenCalled();
  });

  it("throws when extraSeats exceeds the 0..100 bound", async () => {
    await expect(
      createCheckoutSession({
        plan: "pro",
        interval: "month",
        currency: "eur",
        extraSeats: 101,
      }),
    ).rejects.toThrow();
    expect(sessionsCreateMock).not.toHaveBeenCalled();
  });

  it("throws when extraSeats > 0 but the SEAT_ADDON price for the interval is missing", async () => {
    /* In the mock above, SEAT_ADDON.stripePriceIds.year is undefined. */
    await expect(
      createCheckoutSession({
        plan: "pro",
        interval: "year",
        currency: "eur",
        extraSeats: 2,
      }),
    ).rejects.toThrow(/Seat add-on price not configured for interval year/);
    expect(sessionsCreateMock).not.toHaveBeenCalled();
  });
});
