/**
 * Phase 1 — orderTimelineEvents integration tests
 *
 * Covers:
 *  - recordOrderTimelineEvent (helper, via t.run)
 *  - fulfillFromCheckout → timeline event created (source: stripe_webhook)
 *  - fulfillFromCheckout → idempotent: no duplicate event on replay
 *  - orders.updateStatus → timeline event with fromStatus/toStatus/userId
 *  - orders.cancel → timeline event customer_cancel
 *  - orders.getTimeline → ordering, auth enforcement
 */
import { convexTest } from "convex-test";
import { describe, it, expect } from "vitest";
import { api, internal } from "./_generated/api";
import type { MutationCtx } from "./_generated/server";
import schema from "./schema";
import { recordOrderTimelineEvent } from "./model/orders";

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

// ─── Shared helpers ───────────────────────────────────────────────────────────

async function setupCustomerAndProduct(t: ReturnType<typeof convexTest>) {
  const asCustomer = t.withIdentity({
    name: "Alice",
    email: "alice@example.com",
    subject: "clerk_alice_tl",
  });
  const userId = await asCustomer.mutation(api.users.store, {});

  let variantId: any;
  await t.run(async (ctx) => {
    const categoryId = await ctx.db.insert("categories", {
      name: "Toys",
      slug: "toys-tl",
    });
    const productId = await ctx.db.insert("products", {
      name: "Ball",
      slug: "ball-tl",
      status: "active",
      categoryId,
      tags: [],
      parentCategorySlug: "toys-tl",
      childCategorySlug: "toys-tl",
    });
    variantId = await ctx.db.insert("productVariants", {
      productId,
      name: "Red Ball",
      sku: "BALL-TL-001",
      price: 999,
      stockQuantity: 50,
      isActive: true,
      weight: 200,
      weightUnit: "g",
    });
  });

  return { asCustomer, userId, variantId };
}

async function setupAdmin(t: ReturnType<typeof convexTest>) {
  const asAdmin = t.withIdentity({
    name: "Admin",
    email: "admin@example.com",
    subject: "clerk_admin_tl",
  });
  const adminId = await asAdmin.mutation(api.users.store, {});
  await t.run(async (ctx) => {
    await ctx.db.patch(adminId, { role: "admin" });
  });
  return { asAdmin, adminId };
}

/** Insert a confirmed order + one order item directly into the DB. */
async function makeConfirmedOrder(
  t: ReturnType<typeof convexTest>,
  userId: any,
  variantId: any,
  quantity = 2,
) {
  let orderId: any;
  await t.run(async (ctx) => {
    orderId = await ctx.db.insert("orders", {
      orderNumber: `ORD-TL-${Math.random().toString(36).slice(2, 7).toUpperCase()}`,
      userId,
      email: "alice@example.com",
      status: "confirmed",
      paymentStatus: "paid",
      subtotal: 999 * quantity,
      tax: 0,
      shipping: 500,
      discount: 0,
      total: 999 * quantity + 500,
      currency: "GBP",
      shippingAddressSnapshot: {
        fullName: "Alice Smith",
        firstName: "Alice",
        lastName: "Smith",
        addressLine1: "1 Test Lane",
        city: "London",
        postalCode: "E1 1AA",
        country: "GB",
      },
      billingAddressSnapshot: {
        firstName: "Alice",
        lastName: "Smith",
        addressLine1: "1 Test Lane",
        city: "London",
        postalCode: "E1 1AA",
        country: "GB",
      },
      shippoShipmentId: "shp_test",
      shippingMethod: "Standard",
      shippingServiceCode: "std",
      carrier: "DPD",
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
    await ctx.db.insert("orderItems", {
      orderId,
      variantId,
      productName: "Ball",
      variantName: "Red Ball",
      sku: "BALL-TL-001",
      quantity,
      unitPrice: 999,
      totalPrice: 999 * quantity,
    });
  });
  return orderId;
}

/** Full setup required to invoke fulfillFromCheckout (user, address, cart item). */
async function setupFulfillmentData(t: ReturnType<typeof convexTest>) {
  const { asCustomer, userId, variantId } = await setupCustomerAndProduct(t);

  let addressId: any;
  await t.run(async (ctx) => {
    addressId = await ctx.db.insert("addresses", {
      userId,
      type: "shipping",
      fullName: "Alice Smith",
      firstName: "Alice",
      lastName: "Smith",
      phone: "+447911123456",
      addressLine1: "1 Test Lane",
      city: "London",
      postalCode: "E1 1AA",
      country: "GB",
      isDefault: true,
    });
  });

  await asCustomer.mutation(api.carts.addItem, { variantId, quantity: 2 });

  return { asCustomer, userId, variantId, addressId };
}

// ─── recordOrderTimelineEvent helper ─────────────────────────────────────────

describe("recordOrderTimelineEvent", () => {
  it("inserts a timeline event with all provided fields", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await t.run(async (ctx) => {
      await recordOrderTimelineEvent(ctx as unknown as MutationCtx, {
        orderId,
        eventType: "status_change",
        source: "admin",
        fromStatus: "confirmed",
        toStatus: "shipped",
        userId,
      });
    });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events).toHaveLength(1);
    expect(events[0].eventType).toBe("status_change");
    expect(events[0].source).toBe("admin");
    expect(events[0].fromStatus).toBe("confirmed");
    expect(events[0].toStatus).toBe("shipped");
    expect(events[0].userId).toBe(userId);
  });

  it("sets createdAt automatically", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);
    const before = Date.now();

    await t.run(async (ctx) => {
      await recordOrderTimelineEvent(ctx as unknown as MutationCtx, {
        orderId,
        eventType: "status_change",
        source: "admin",
        toStatus: "confirmed",
      });
    });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events[0].createdAt).toBeGreaterThanOrEqual(before);
  });

  it("stores optional payload as a JSON string", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);
    const payload = JSON.stringify({ amount: 1999, currency: "gbp" });

    await t.run(async (ctx) => {
      await recordOrderTimelineEvent(ctx as unknown as MutationCtx, {
        orderId,
        eventType: "refund",
        source: "admin",
        payload,
      });
    });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events[0].payload).toBe(payload);
    expect(JSON.parse(events[0].payload!).amount).toBe(1999);
  });
});

// ─── fulfillFromCheckout → timeline ──────────────────────────────────────────

describe("orders.fulfillFromCheckout - timeline events", () => {
  it("records a status_change event with toStatus: confirmed after order creation", async () => {
    const t = convexTest(schema, modules);
    const { userId, addressId } = await setupFulfillmentData(t);

    const orderId = await t.mutation(internal.orders.fulfillFromCheckout, {
      stripeCheckoutSessionId: "cs_test_tl_001",
      stripePaymentIntentId: "pi_test_tl_001",
      convexUserId: userId,
      addressId,
      shipmentObjectId: "shp_tl_001",
      shippingMethod: "Standard",
      shippingServiceCode: "std",
      carrier: "DPD",
      amountTotal: 2498,
      amountShipping: 500,
      currency: "gbp",
    });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events).toHaveLength(1);
    expect(events[0].eventType).toBe("status_change");
    expect(events[0].source).toBe("stripe_webhook");
    expect(events[0].toStatus).toBe("confirmed");
    expect(events[0].fromStatus).toBeUndefined();
    expect(events[0].userId).toBeUndefined();
  });

  it("does not record a duplicate event on replay (idempotent)", async () => {
    const t = convexTest(schema, modules);
    const { userId, addressId } = await setupFulfillmentData(t);

    const fulfillArgs = {
      stripeCheckoutSessionId: "cs_test_tl_idem",
      stripePaymentIntentId: null,
      convexUserId: userId,
      addressId,
      shipmentObjectId: "shp_tl_idem",
      shippingMethod: "Standard",
      shippingServiceCode: "std",
      carrier: "DPD",
      amountTotal: null,
      amountShipping: 500,
      currency: null,
    };

    const orderId = await t.mutation(
      internal.orders.fulfillFromCheckout,
      fulfillArgs,
    );
    // Replay — order already exists; should return same id without inserting a new event
    const orderId2 = await t.mutation(
      internal.orders.fulfillFromCheckout,
      fulfillArgs,
    );

    expect(orderId2).toBe(orderId);

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events).toHaveLength(1);
  });
});

// ─── orders.updateStatus → timeline ──────────────────────────────────────────

describe("orders.updateStatus - timeline events", () => {
  it("records a status_change event with correct fromStatus and toStatus", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const { asAdmin } = await setupAdmin(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await asAdmin.mutation(api.orders.updateStatus, {
      id: orderId,
      status: "shipped",
    });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events).toHaveLength(1);
    expect(events[0].eventType).toBe("status_change");
    expect(events[0].source).toBe("admin");
    expect(events[0].fromStatus).toBe("confirmed");
    expect(events[0].toStatus).toBe("shipped");
  });

  it("records the admin userId on the event", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const { asAdmin, adminId } = await setupAdmin(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await asAdmin.mutation(api.orders.updateStatus, {
      id: orderId,
      status: "processing",
    });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events[0].userId).toBe(adminId);
  });

  it("records a separate event for each status change", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const { asAdmin } = await setupAdmin(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await asAdmin.mutation(api.orders.updateStatus, {
      id: orderId,
      status: "processing",
    });
    await asAdmin.mutation(api.orders.updateStatus, {
      id: orderId,
      status: "shipped",
    });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events).toHaveLength(2);
    expect(events.map((e) => e.toStatus)).toEqual(
      expect.arrayContaining(["processing", "shipped"]),
    );
  });

  it("updates the order updatedAt timestamp", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const { asAdmin } = await setupAdmin(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    const before = Date.now();
    await asAdmin.mutation(api.orders.updateStatus, {
      id: orderId,
      status: "shipped",
    });

    const order = await t.run(async (ctx) => ctx.db.get(orderId));
    expect(order!.updatedAt).toBeGreaterThanOrEqual(before);
  });
});

// ─── orders.cancel → timeline ─────────────────────────────────────────────────

describe("orders.cancel - timeline events", () => {
  it("records a customer_cancel event on cancel", async () => {
    const t = convexTest(schema, modules);
    const { asCustomer, userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await asCustomer.mutation(api.orders.cancel, { id: orderId });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events).toHaveLength(1);
    expect(events[0].eventType).toBe("customer_cancel");
    expect(events[0].source).toBe("customer_cancel");
  });

  it("records fromStatus: confirmed and toStatus: cancelled", async () => {
    const t = convexTest(schema, modules);
    const { asCustomer, userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await asCustomer.mutation(api.orders.cancel, { id: orderId });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events[0].fromStatus).toBe("confirmed");
    expect(events[0].toStatus).toBe("cancelled");
  });

  it("records the customer userId on the event", async () => {
    const t = convexTest(schema, modules);
    const { asCustomer, userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await asCustomer.mutation(api.orders.cancel, { id: orderId });

    const events = await t.run(async (ctx) =>
      ctx.db
        .query("orderTimelineEvents")
        .withIndex("by_order", (q) => q.eq("orderId", orderId))
        .collect(),
    );

    expect(events[0].userId).toBe(userId);
  });
});

// ─── orders.getTimeline ───────────────────────────────────────────────────────

describe("orders.getTimeline", () => {
  it("returns an empty array for an order with no timeline events", async () => {
    const t = convexTest(schema, modules);
    const { asCustomer, userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    const events = await asCustomer.query(api.orders.getTimeline, { orderId });
    expect(events).toHaveLength(0);
  });

  it("returns events in ascending createdAt order", async () => {
    const t = convexTest(schema, modules);
    const { asCustomer, userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await t.run(async (ctx) => {
      await ctx.db.insert("orderTimelineEvents", {
        orderId,
        eventType: "status_change",
        source: "stripe_webhook",
        toStatus: "confirmed",
        createdAt: 3000,
      });
      await ctx.db.insert("orderTimelineEvents", {
        orderId,
        eventType: "status_change",
        source: "admin",
        fromStatus: "confirmed",
        toStatus: "processing",
        createdAt: 1000,
      });
      await ctx.db.insert("orderTimelineEvents", {
        orderId,
        eventType: "status_change",
        source: "admin",
        fromStatus: "processing",
        toStatus: "shipped",
        createdAt: 2000,
      });
    });

    const events = await asCustomer.query(api.orders.getTimeline, { orderId });

    expect(events).toHaveLength(3);
    expect(events[0].createdAt).toBe(1000);
    expect(events[1].createdAt).toBe(2000);
    expect(events[2].createdAt).toBe(3000);
  });

  it("throws if the order belongs to a different user", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    const asBob = t.withIdentity({
      name: "Bob",
      email: "bob@example.com",
      subject: "clerk_bob_tl",
    });
    await asBob.mutation(api.users.store, {});

    await expect(
      asBob.query(api.orders.getTimeline, { orderId }),
    ).rejects.toThrow("Unauthorized: order does not belong to you");
  });

  it("admin can view the timeline for any order", async () => {
    const t = convexTest(schema, modules);
    const { userId, variantId } = await setupCustomerAndProduct(t);
    const { asAdmin } = await setupAdmin(t);
    const orderId = await makeConfirmedOrder(t, userId, variantId);

    await t.run(async (ctx) => {
      await ctx.db.insert("orderTimelineEvents", {
        orderId,
        eventType: "status_change",
        source: "stripe_webhook",
        toStatus: "confirmed",
        createdAt: Date.now(),
      });
    });

    const events = await asAdmin.query(api.orders.getTimeline, { orderId });
    expect(events).toHaveLength(1);
  });

  it("throws if the order does not exist", async () => {
    const t = convexTest(schema, modules);
    const { asCustomer, userId, variantId } = await setupCustomerAndProduct(t);

    // Create and delete an order to get a stale ID
    const orderId = await makeConfirmedOrder(t, userId, variantId);
    await t.run(async (ctx) => {
      await ctx.db.delete(orderId);
    });

    await expect(
      asCustomer.query(api.orders.getTimeline, { orderId }),
    ).rejects.toThrow("Order not found");
  });
});
