import { convexTest } from "convex-test";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";
import type { Id } from "./_generated/dataModel";

const { mockCustomersCreate, mockSessionsCreate, mockSessionsRetrieve } =
  vi.hoisted(() => ({
    mockCustomersCreate: vi.fn(),
    mockSessionsCreate: vi.fn(),
    mockSessionsRetrieve: vi.fn(),
  }));

vi.mock("stripe", () => {
  return {
    default: class Stripe {
      customers = { create: mockCustomersCreate };
      checkout = {
        sessions: {
          create: mockSessionsCreate,
          retrieve: mockSessionsRetrieve,
        },
      };
    },
  };
});

const modules = import.meta.glob("./**/*.ts");

const identity = {
  name: "Alice Smith",
  email: "alice@example.com",
  subject: "clerk_alice_123",
};

const baseAddress = {
  firstName: "Alice",
  lastName: "Smith",
  phone: "+447911123456",
  addressLine1: "10 Downing Street",
  city: "London",
  postalCode: "SW1A 2AA",
  country: "GB",
};

const shippingRate = {
  provider: "DPD UK",
  serviceName: "Next Day",
  serviceToken: "dpd_uk_next_day",
  amount: 5.5,
  currency: "GBP",
  estimatedDays: 1,
  durationTerms: "1-2 business days",
  carrierAccount: "ca_dpd_001",
};

beforeEach(() => {
  vi.stubEnv("STRIPE_SECRET_KEY", "sk_test_fake_key");
  vi.stubEnv("STOREFRONT_URL", "http://localhost:3000");

  mockCustomersCreate.mockResolvedValue({ id: "cus_new_123" });
  mockSessionsCreate.mockResolvedValue({
    id: "cs_test_session_123",
    client_secret: "cs_test_secret_abc",
    status: "open",
    payment_status: "unpaid",
  });
  mockSessionsRetrieve.mockResolvedValue({
    id: "cs_test_session_123",
    status: "complete",
    payment_status: "paid",
    customer_details: { email: "alice@example.com" },
  });
});

afterEach(() => {
  vi.unstubAllEnvs();
  mockCustomersCreate.mockReset();
  mockSessionsCreate.mockReset();
  mockSessionsRetrieve.mockReset();
});

async function setupFullCheckoutContext(
  t: ReturnType<typeof convexTest>,
  overrides?: {
    stockQuantity?: number;
    price?: number;
    isActive?: boolean;
    stripeCustomerId?: string;
  },
) {
  const asA = t.withIdentity(identity);
  const userId = await asA.mutation(api.users.store, {});

  if (overrides?.stripeCustomerId) {
    await t.run(async (ctx) => {
      await ctx.db.patch(userId, {
        stripeCustomerId: overrides.stripeCustomerId,
      });
    });
  }

  const addressId = await asA.mutation(api.addresses.add, {
    ...baseAddress,
    type: "shipping",
    additionalInformation: "Flat 2",
    isDefault: true,
    isValidated: true,
  });

  let productId: Id<"products">;
  let variantId: Id<"productVariants">;

  await t.run(async (ctx) => {
    const categoryId = await ctx.db.insert("categories", {
      name: "Dog Food",
      slug: "dog-food",
    });
    productId = await ctx.db.insert("products", {
      name: "Premium Kibble",
      slug: "premium-kibble",
      status: "active",
      categoryId,
      tags: [],
      parentCategorySlug: "dogs",
      childCategorySlug: "dog-food",
    });
    variantId = await ctx.db.insert("productVariants", {
      productId,
      name: "1kg Bag",
      sku: "PK-001",
      price: overrides?.price ?? 2499,
      stockQuantity: overrides?.stockQuantity ?? 50,
      isActive: overrides?.isActive ?? true,
      weight: 1000,
      weightUnit: "g",
      length: 30,
      width: 20,
      height: 10,
      dimensionUnit: "cm",
    });
    await ctx.db.insert("productImages", {
      productId,
      url: "https://example.com/kibble.jpg",
      position: 0,
    });
    await ctx.db.insert("carts", {
      userId,
      items: [
        {
          productId,
          variantId,
          quantity: 2,
          price: overrides?.price ?? 2499,
        },
      ],
      createdAt: Date.now(),
      updatedAt: Date.now(),
      expiresAt: Date.now() + 86400000,
    });
  });

  return {
    userId,
    addressId,
    productId: productId!,
    variantId: variantId!,
    asA,
  };
}

// ─── createCheckoutSession ───────────────────────────────────────────────────

describe("stripeActions.createCheckoutSession", () => {
  it("throws when user is not authenticated", async () => {
    const t = convexTest(schema, modules);
    const asA = t.withIdentity(identity);
    await asA.mutation(api.users.store, {});

    const addressId = await asA.mutation(api.addresses.add, {
      ...baseAddress,
      type: "shipping",
      isDefault: true,
    });

    await expect(
      t.action(api.stripeActions.createCheckoutSession, {
        addressId,
        shipmentObjectId: "shp_test_123",
        shippingRate,
      }),
    ).rejects.toThrow(/signed in/i);
  });

  it("throws when cart is empty", async () => {
    const t = convexTest(schema, modules);
    const asA = t.withIdentity(identity);
    await asA.mutation(api.users.store, {});

    const addressId = await asA.mutation(api.addresses.add, {
      ...baseAddress,
      type: "shipping",
      isDefault: true,
    });

    await expect(
      asA.action(api.stripeActions.createCheckoutSession, {
        addressId,
        shipmentObjectId: "shp_test_123",
        shippingRate,
      }),
    ).rejects.toThrow(/cart is empty/i);
  });

  it("throws when cart has blocking issues (out of stock)", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t, {
      stockQuantity: 0,
    });

    await expect(
      asA.action(api.stripeActions.createCheckoutSession, {
        addressId,
        shipmentObjectId: "shp_test_123",
        shippingRate,
      }),
    ).rejects.toThrow(/cart has issues/i);
  });

  it("creates a Stripe customer when user has none and patches the user", async () => {
    const t = convexTest(schema, modules);
    const { userId, addressId, asA } = await setupFullCheckoutContext(t);

    mockCustomersCreate.mockResolvedValue({ id: "cus_fresh_456" });

    await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

    expect(mockCustomersCreate).toHaveBeenCalledOnce();
    expect(mockCustomersCreate).toHaveBeenCalledWith({
      email: "alice@example.com",
      name: "Alice Smith",
      metadata: { convexUserId: userId },
    });

    const updatedUser = await t.run(async (ctx) => ctx.db.get(userId));
    expect(updatedUser?.stripeCustomerId).toBe("cus_fresh_456");
  });

  it("reuses existing Stripe customer and does not create a new one", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t, {
      stripeCustomerId: "cus_existing_789",
    });

    await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

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

  it("returns clientSecret on success", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    const result = await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

    expect(result).toEqual({ clientSecret: "cs_test_secret_abc" });
  });

  it("builds correct line items from cart", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

    const createCall = mockSessionsCreate.mock.calls[0][0];
    expect(createCall.line_items).toHaveLength(1);
    expect(createCall.line_items[0]).toEqual({
      price_data: {
        currency: "gbp",
        product_data: { name: "Premium Kibble — 1kg Bag" },
        unit_amount: 2499,
      },
      quantity: 2,
    });
  });

  it("passes correct shipping options with amount in pence", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

    const createCall = mockSessionsCreate.mock.calls[0][0];
    expect(createCall.shipping_options).toHaveLength(1);

    const rateData = createCall.shipping_options[0].shipping_rate_data;
    expect(rateData.fixed_amount.amount).toBe(550);
    expect(rateData.fixed_amount.currency).toBe("gbp");
    expect(rateData.display_name).toBe("DPD UK — Next Day");
    expect(rateData.delivery_estimate.minimum).toEqual({
      unit: "business_day",
      value: 1,
    });
  });

  it("passes correct metadata including all shipping fields", async () => {
    const t = convexTest(schema, modules);
    const { userId, addressId, asA } = await setupFullCheckoutContext(t);

    await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

    const createCall = mockSessionsCreate.mock.calls[0][0];
    expect(createCall.metadata).toEqual({
      convexUserId: userId,
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingMethod: "DPD UK — Next Day",
      shippingServiceCode: "dpd_uk_next_day",
      carrier: "DPD UK",
      carrierAccount: "ca_dpd_001",
    });
  });

  it("creates session with ui_mode custom and correct return_url", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

    const createCall = mockSessionsCreate.mock.calls[0][0];
    expect(createCall.mode).toBe("payment");
    expect(createCall.ui_mode).toBe("custom");
    expect(createCall.return_url).toBe(
      "http://localhost:3000/checkout/success?session_id={CHECKOUT_SESSION_ID}",
    );
  });

  it("passes Stripe customer ID to session creation", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t, {
      stripeCustomerId: "cus_existing_789",
    });

    await asA.action(api.stripeActions.createCheckoutSession, {
      addressId,
      shipmentObjectId: "shp_test_123",
      shippingRate,
    });

    const createCall = mockSessionsCreate.mock.calls[0][0];
    expect(createCall.customer).toBe("cus_existing_789");
  });

  it("throws when STOREFRONT_URL env var is missing", async () => {
    vi.stubEnv("STOREFRONT_URL", "");
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    await expect(
      asA.action(api.stripeActions.createCheckoutSession, {
        addressId,
        shipmentObjectId: "shp_test_123",
        shippingRate,
      }),
    ).rejects.toThrow(/STOREFRONT_URL/i);
  });

  it("throws when Stripe returns no client_secret", async () => {
    mockSessionsCreate.mockResolvedValue({
      id: "cs_test_no_secret",
      client_secret: null,
    });

    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    await expect(
      asA.action(api.stripeActions.createCheckoutSession, {
        addressId,
        shipmentObjectId: "shp_test_123",
        shippingRate,
      }),
    ).rejects.toThrow(/client_secret/i);
  });
});

// ─── getCheckoutSessionStatus ────────────────────────────────────────────────

describe("stripeActions.getCheckoutSessionStatus", () => {
  it("returns complete status with customer email", async () => {
    mockSessionsRetrieve.mockResolvedValue({
      status: "complete",
      payment_status: "paid",
      customer_details: { email: "alice@example.com" },
    });

    const t = convexTest(schema, modules);
    const result = await t.action(api.stripeActions.getCheckoutSessionStatus, {
      sessionId: "cs_test_session_123",
    });

    expect(result).toEqual({
      status: "complete",
      paymentStatus: "paid",
      customerEmail: "alice@example.com",
    });
    expect(mockSessionsRetrieve).toHaveBeenCalledWith("cs_test_session_123");
  });

  it("returns expired status", async () => {
    mockSessionsRetrieve.mockResolvedValue({
      status: "expired",
      payment_status: "unpaid",
      customer_details: null,
    });

    const t = convexTest(schema, modules);
    const result = await t.action(api.stripeActions.getCheckoutSessionStatus, {
      sessionId: "cs_expired_456",
    });

    expect(result).toEqual({
      status: "expired",
      paymentStatus: "unpaid",
      customerEmail: null,
    });
  });

  it("returns open status when payment not completed", async () => {
    mockSessionsRetrieve.mockResolvedValue({
      status: "open",
      payment_status: "unpaid",
      customer_details: { email: "alice@example.com" },
    });

    const t = convexTest(schema, modules);
    const result = await t.action(api.stripeActions.getCheckoutSessionStatus, {
      sessionId: "cs_open_789",
    });

    expect(result).toEqual({
      status: "open",
      paymentStatus: "unpaid",
      customerEmail: "alice@example.com",
    });
  });

  it("returns null email when customer_details is absent", async () => {
    mockSessionsRetrieve.mockResolvedValue({
      status: "complete",
      payment_status: "paid",
      customer_details: undefined,
    });

    const t = convexTest(schema, modules);
    const result = await t.action(api.stripeActions.getCheckoutSessionStatus, {
      sessionId: "cs_no_email",
    });

    expect(result.customerEmail).toBeNull();
  });
});
