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 modules = import.meta.glob("./**/*.ts");

const identity = {
  name: "Alice",
  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",
};

let fetchSpy: ReturnType<typeof vi.fn>;

beforeEach(() => {
  fetchSpy = vi.fn();
  vi.stubGlobal("fetch", fetchSpy);
  vi.stubEnv("SHIPPO_API_KEY", "shippo_test_key_123");
  vi.stubEnv("SHIPPO_SOURCE_ADDRESS_ID", "addr_warehouse_001");
});

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

function mockShippoShipmentResponse(overrides?: {
  shipmentId?: string;
  rates?: Array<{
    object_id: string;
    provider: string;
    servicelevel: { name: string; token: string };
    amount: string;
    currency: string;
    estimated_days: number;
    duration_terms: string;
    arrives_by?: string | null;
    carrier_account: string;
  }>;
}) {
  const defaultRates = [
    {
      object_id: "rate_001",
      provider: "DPD UK",
      servicelevel: { name: "Next Day", token: "dpd_uk_next_day" },
      amount: "5.50",
      currency: "GBP",
      estimated_days: 1,
      duration_terms: "1-2 business days",
      arrives_by: null,
      carrier_account: "ca_dpd_001",
    },
    {
      object_id: "rate_002",
      provider: "UPS",
      servicelevel: { name: "Standard", token: "ups_standard" },
      amount: "7.99",
      currency: "GBP",
      estimated_days: 3,
      duration_terms: "3-5 business days",
      carrier_account: "ca_ups_001",
    },
  ];

  fetchSpy.mockResolvedValue({
    ok: true,
    status: 200,
    json: () =>
      Promise.resolve({
        object_id: overrides?.shipmentId ?? "shp_test_123",
        rates: overrides?.rates ?? defaultRates,
      }),
  });
}

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

  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 };
}

describe("checkoutActions.getShippingRate", () => {
  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.checkoutActions.getShippingRate, { addressId }),
    ).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.checkoutActions.getShippingRate, { addressId }),
    ).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,
    });

    mockShippoShipmentResponse();

    await expect(
      asA.action(api.checkoutActions.getShippingRate, { addressId }),
    ).rejects.toThrow(/cart has issues/i);
  });

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

    mockShippoShipmentResponse();

    const result = await asA.action(api.checkoutActions.getShippingRate, {
      addressId,
    });

    expect(result.shipmentObjectId).toBe("shp_test_123");

    expect(result.selectedRate.provider).toBe("DPD UK");
    expect(result.selectedRate.serviceName).toBe("Next Day");
    expect(result.selectedRate.serviceToken).toBe("dpd_uk_next_day");
    expect(result.selectedRate.amount).toBe(5.5);
    expect(result.selectedRate.currency).toBe("GBP");
    expect(result.selectedRate.estimatedDays).toBe(1);
    expect(result.selectedRate.durationTerms).toBe("1-2 business days");
    expect(result.selectedRate.carrierAccount).toBe("ca_dpd_001");

    expect(result.alternativeRates).toHaveLength(1);
    expect(result.alternativeRates[0].provider).toBe("UPS");

    expect(result.cartSubtotal).toBe(2499 * 2);
    expect(result.shippingTotal).toBe(5.5);
    expect(result.orderTotal).toBe(2499 * 2 + 5.5);
  });

  it("sends correct Shippo request with mapped address fields", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    mockShippoShipmentResponse();

    await asA.action(api.checkoutActions.getShippingRate, { addressId });

    expect(fetchSpy).toHaveBeenCalledTimes(1);
    const [url, opts] = fetchSpy.mock.calls[0];
    expect(url).toBe("https://api.goshippo.com/shipments/");
    expect(opts.method).toBe("POST");

    const body = JSON.parse(opts.body);
    expect(body.address_from).toBe("addr_warehouse_001");
    expect(body.address_to.name).toBe("Alice Smith");
    expect(body.address_to.street1).toBe("10 Downing Street");
    expect(body.address_to.street2).toBe("Flat 2");
    expect(body.address_to.city).toBe("London");
    expect(body.address_to.zip).toBe("SW1A 2AA");
    expect(body.address_to.country).toBe("GB");
    expect(body.address_to.phone).toBe("+447911123456");
    expect(body.async).toBe(false);
    expect(body.parcels).toHaveLength(1);
    expect(body.parcels[0].weight).toBe("2000");
    expect(body.parcels[0].mass_unit).toBe("g");
  });

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

    await expect(
      asA.action(api.checkoutActions.getShippingRate, { addressId }),
    ).rejects.toThrow(/missing source address/i);
  });

  it("throws when Shippo API returns no rates", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    mockShippoShipmentResponse({ rates: [] });

    await expect(
      asA.action(api.checkoutActions.getShippingRate, { addressId }),
    ).rejects.toThrow(/no shipping rates available/i);
  });

  it("falls back to non-preferred carriers when no preferred carriers in response", async () => {
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    mockShippoShipmentResponse({
      rates: [
        {
          object_id: "rate_non_pref",
          provider: "Royal Mail",
          servicelevel: { name: "2nd Class", token: "royal_mail_2nd" },
          amount: "3.00",
          currency: "GBP",
          estimated_days: 3,
          duration_terms: "2-4 business days",
          arrives_by: null,
          carrier_account: "ca_rm_001",
        },
      ],
    });

    const result = await asA.action(api.checkoutActions.getShippingRate, {
      addressId,
    });

    expect(result.selectedRate.provider).toBe("Royal Mail");
    expect(result.selectedRate.amount).toBe(3.0);
    expect(warnSpy).toHaveBeenCalledWith(
      "No preferred carriers returned rates. Falling back to all carriers.",
    );

    warnSpy.mockRestore();
  });

  it("correctly computes parcel from cart items with dimensions", async () => {
    const t = convexTest(schema, modules);
    const { addressId, asA } = await setupFullCheckoutContext(t);

    mockShippoShipmentResponse();

    await asA.action(api.checkoutActions.getShippingRate, { addressId });

    const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
    const parcel = body.parcels[0];
    expect(parcel.length).toBe("30");
    expect(parcel.width).toBe("20");
    expect(parcel.height).toBe("20");
    expect(parcel.distance_unit).toBe("cm");
  });
});
