import { afterEach, describe, expect, it, vi } from "vitest";
import { createActor, waitFor } from "xstate";

import { behaviorAnalytics } from "@/lib/analytics";
import {
  MAX_ORDER_POLLING_MS,
  PAYMENT_TIMEOUT_ERROR_MESSAGE,
} from "@/stores/payment/helper";

import {
  createTestPaymentMachine,
  creditPlan,
  type CreateOrderSpy,
} from "./payment-machine.test-utils";

describe("payment order flow", () => {
  afterEach(() => {
    vi.useRealTimers();
    vi.restoreAllMocks();
  });

  it("does not create an order before agreement is accepted", async () => {
    const createOrderSpy = vi.fn<CreateOrderSpy>();
    const actor = createActor(
      createTestPaymentMachine({ createOrderSpy }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentAgreementChanged", agreed: false });
    actor.send({ type: "PaymentCreateOrderSubmitted" });

    expect(actor.getSnapshot().matches("ready")).toBe(true);
    expect(actor.getSnapshot().context.errorMessage).toBe(
      "Choose a plan and accept the agreement before continuing.",
    );
    expect(createOrderSpy).not.toHaveBeenCalled();
    actor.stop();
  });

  it("creates an order, polls it, and reaches paid", async () => {
    const createOrderStart = vi
      .spyOn(behaviorAnalytics, "createOrderStart")
      .mockImplementation(() => undefined);
    const createOrderSuccess = vi
      .spyOn(behaviorAnalytics, "createOrderSuccess")
      .mockImplementation(() => undefined);
    const statusPollStart = vi
      .spyOn(behaviorAnalytics, "statusPollStart")
      .mockImplementation(() => undefined);
    const statusPollUpdate = vi
      .spyOn(behaviorAnalytics, "statusPollUpdate")
      .mockImplementation(() => undefined);
    const createOrderSpy = vi.fn<CreateOrderSpy>();
    const actor = createActor(
      createTestPaymentMachine({ createOrderSpy }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));

    expect(createOrderSpy).toHaveBeenCalledWith({
      planId: "vip_monthly",
      payChannel: "stripe",
      autoRenew: true,
    });
    expect(actor.getSnapshot().context).toMatchObject({
      currentOrderId: "pay_test_001",
      orderStatus: "paid",
      tipMessage: null,
      tipMessageError: null,
      orderPollingStartedAt: null,
      launchNonce: 1,
    });
    expect(createOrderStart).toHaveBeenCalledOnce();
    expect(createOrderSuccess).toHaveBeenCalledOnce();
    expect(statusPollStart).toHaveBeenCalledOnce();
    expect(statusPollUpdate).toHaveBeenCalledWith(
      "pay_test_001",
      "paid",
      expect.objectContaining({ planId: "vip_monthly" }),
    );
    actor.stop();
  });

  it("carries the server-issued commercial offer into order creation", async () => {
    const createOrderSpy = vi.fn<CreateOrderSpy>();
    const actor = createActor(
      createTestPaymentMachine({ createOrderSpy }),
    ).start();
    actor.send({
      type: "PaymentInit",
      planId: "vip_monthly",
      commercialOfferId: "offer-user-bound-1",
    });
    await waitFor(actor, (snapshot) => snapshot.matches("ready"));

    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));

    expect(createOrderSpy).toHaveBeenCalledWith({
      planId: "vip_monthly",
      payChannel: "stripe",
      autoRenew: true,
      commercialOfferId: "offer-user-bound-1",
    });
    actor.stop();
  });

  it.each(["elio", "maya-tan", "nayeli-cervantes"])(
    "binds a default VIP or credit order to the source chat character %s",
    async (characterId) => {
      const createOrderSpy = vi.fn<CreateOrderSpy>();
      const actor = createActor(
        createTestPaymentMachine({ createOrderSpy }),
      ).start();
      actor.send({ type: "PaymentInit", characterId });
      await waitFor(actor, (snapshot) => snapshot.matches("ready"));

      actor.send({ type: "PaymentCreateOrderSubmitted" });
      await waitFor(actor, (snapshot) => snapshot.matches("paid"));

      expect(createOrderSpy).toHaveBeenCalledWith({
        planId: "vip_monthly",
        payChannel: "stripe",
        autoRenew: true,
        recipientCharacterId: characterId,
      });
      actor.stop();
    },
  );

  it("carries the originating chat action into order creation", async () => {
    const createOrderSpy = vi.fn<CreateOrderSpy>();
    const actor = createActor(
      createTestPaymentMachine({ createOrderSpy }),
    ).start();
    actor.send({
      type: "PaymentInit",
      planId: "vip_monthly",
      chatActionId: "019c8f8d-17d3-7a42-b1cc-b6927b1927d5",
    });
    await waitFor(actor, (snapshot) => snapshot.matches("ready"));

    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));

    expect(createOrderSpy).toHaveBeenCalledWith({
      planId: "vip_monthly",
      payChannel: "stripe",
      autoRenew: true,
      chatActionId: "019c8f8d-17d3-7a42-b1cc-b6927b1927d5",
    });
    actor.stop();
  });

  it("loads a stable Tip message after payment and clears it on reset", async () => {
    const loadTipMessage = vi.fn();
    const actor = createActor(
      createTestPaymentMachine({
        orderType: "tip",
        orderPlanId: "tip_coffee_usd_9_99",
        onLoadTipMessage: loadTipMessage,
        tipMessage: {
          orderId: "pay_test_001",
          characterId: "elio",
          planId: "tip_coffee_usd_9_99",
          productName: "Golden Reserve",
          tipCount: 2,
          poolIndex: 18,
          message: "You made my day.",
        },
      }),
    ).start();
    await initializeTip(actor);
    actor.send({
      type: "PaymentCreateOrderSubmitted",
      recipientCharacterId: "elio",
    });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));

    expect(loadTipMessage).toHaveBeenCalledWith("pay_test_001");
    expect(actor.getSnapshot().context).toMatchObject({
      orderStatus: "paid",
      tipMessage: expect.objectContaining({
        tipCount: 2,
        message: "You made my day.",
      }),
      tipMessageError: null,
    });

    actor.send({ type: "PaymentReset" });
    await waitFor(actor, (snapshot) => snapshot.matches("ready"));
    expect(actor.getSnapshot().context).toMatchObject({
      tipMessage: null,
      tipMessageError: null,
    });
    actor.stop();
  });

  it("clears the previous Tip result before creating another order", async () => {
    const actor = createActor(
      createTestPaymentMachine({
        orderType: "tip",
        orderPlanId: "tip_coffee_usd_4_99",
      }),
    ).start();
    await initializeTip(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));

    actor.send({ type: "PaymentCreateOrderSubmitted" });
    expect(actor.getSnapshot().matches("creatingOrder")).toBe(true);
    expect(actor.getSnapshot().context).toMatchObject({
      tipMessage: null,
      tipMessageError: null,
    });
    actor.stop();
  });

  it("clears the previous Tip result when the payment channel changes", async () => {
    const actor = createActor(
      createTestPaymentMachine({
        orderType: "tip",
        orderPlanId: "tip_coffee_usd_4_99",
      }),
    ).start();
    await initializeTip(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));

    actor.send({ type: "PaymentPayChannelChanged", payChannel: "ezpay" });
    expect(actor.getSnapshot().matches("ready")).toBe(true);
    expect(actor.getSnapshot().context).toMatchObject({
      payChannel: "ezpay",
      tipMessage: null,
      tipMessageError: null,
    });
    actor.stop();
  });

  it("passes the selected tip recipient to order creation", async () => {
    const createOrderSpy = vi.fn<CreateOrderSpy>();
    const actor = createActor(
      createTestPaymentMachine({ createOrderSpy }),
    ).start();
    await initialize(actor);
    actor.send({
      type: "PaymentCreateOrderSubmitted",
      recipientCharacterId: "maya-tan",
    });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));

    expect(createOrderSpy).toHaveBeenCalledWith({
      planId: "vip_monthly",
      payChannel: "stripe",
      autoRenew: true,
      recipientCharacterId: "maya-tan",
    });
    actor.stop();
  });

  it("tracks order creation failures", async () => {
    const createOrderFailed = vi
      .spyOn(behaviorAnalytics, "createOrderFailed")
      .mockImplementation(() => undefined);
    const actor = createActor(
      createTestPaymentMachine({
        createOrderError: new Error("backend unavailable"),
      }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("failed"));

    expect(createOrderFailed).toHaveBeenCalledOnce();
    expect(actor.getSnapshot().context.errorMessage).toBe(
      "backend unavailable",
    );
    actor.stop();
  });

  it("waits for pending payment and can reset", async () => {
    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "pending" }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("waitingForPayment"));
    expect(actor.getSnapshot().context.orderStatus).toBe("pending");

    actor.send({ type: "PaymentReset" });
    await waitFor(actor, (snapshot) => snapshot.matches("ready"));
    expect(actor.getSnapshot().context).toMatchObject({
      currentOrderId: null,
      payParams: null,
      orderStatus: null,
      tipMessage: null,
      tipMessageError: null,
      orderPollingStartedAt: null,
      errorMessage: null,
    });
    actor.stop();
  });

  it("moves to failed when polling reports failure", async () => {
    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "failed" }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("failed"));

    expect(actor.getSnapshot().context).toMatchObject({
      orderStatus: "failed",
      orderPollingStartedAt: null,
      errorMessage: "Payment failed or was cancelled.",
    });
    actor.stop();
  });

  it("moves expired orders to a dedicated terminal state", async () => {
    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "expired" }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("expired"));

    expect(actor.getSnapshot().context).toMatchObject({
      orderStatus: "expired",
      payParams: {
        provider: "stripe",
        clientSecret: "pi_test_secret_test",
      },
      errorMessage:
        "This payment order has expired. Please create a new order.",
    });
    actor.stop();
  });

  it("keeps paid state when the Tip message fails and retries only the message", async () => {
    const loadTipMessage = vi.fn();
    const actor = createActor(
      createTestPaymentMachine({
        orderType: "tip",
        orderPlanId: "tip_coffee_usd_4_99",
        tipMessageError: new Error("message unavailable"),
        onLoadTipMessage: loadTipMessage,
      }),
    ).start();
    await initializeTip(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("tipMessageFailed"));

    expect(actor.getSnapshot().context).toMatchObject({
      orderStatus: "paid",
      tipMessage: null,
      tipMessageError: "message unavailable",
    });
    actor.send({ type: "PaymentTipMessageRetryRequested" });
    await waitFor(actor, () => loadTipMessage.mock.calls.length === 2);
    expect(actor.getSnapshot().context.orderStatus).toBe("paid");
    actor.stop();
  });

  it("times out an old pending order", async () => {
    const statusPollTimeout = vi
      .spyOn(behaviorAnalytics, "statusPollTimeout")
      .mockImplementation(() => undefined);
    const now = new Date("2026-06-30T00:00:00.000Z");
    vi.useFakeTimers();
    vi.setSystemTime(now);
    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "pending" }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("waitingForPayment"));

    vi.setSystemTime(now.getTime() + MAX_ORDER_POLLING_MS);
    await vi.advanceTimersByTimeAsync(4000);
    await waitFor(actor, (snapshot) => snapshot.matches("failed"));
    expect(actor.getSnapshot().context.errorMessage).toBe(
      PAYMENT_TIMEOUT_ERROR_MESSAGE,
    );
    expect(statusPollTimeout).toHaveBeenCalledOnce();
    actor.stop();
  });

  it("prioritizes paid response over timeout", async () => {
    const now = new Date("2026-06-30T00:00:00.000Z");
    vi.useFakeTimers();
    vi.setSystemTime(now);
    const actor = createActor(
      createTestPaymentMachine({ orderStatuses: ["pending", "paid"] }),
    ).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("waitingForPayment"));

    vi.setSystemTime(now.getTime() + MAX_ORDER_POLLING_MS);
    await vi.advanceTimersByTimeAsync(4000);
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));
    expect(actor.getSnapshot().context.errorMessage).toBeNull();
    actor.stop();
  });

  it("uses restored order creation time", async () => {
    const now = new Date("2026-06-30T00:00:00.000Z");
    vi.useFakeTimers();
    vi.setSystemTime(now);
    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "pending" }),
    ).start();
    await initialize(actor);
    actor.send({
      type: "PaymentReturned",
      orderId: "pay_returned_001",
      createdAt: now.getTime() - MAX_ORDER_POLLING_MS,
    });
    await waitFor(actor, (snapshot) => snapshot.matches("failed"));

    expect(actor.getSnapshot().context).toMatchObject({
      currentOrderId: "pay_returned_001",
      orderStatus: "failed",
      errorMessage: PAYMENT_TIMEOUT_ERROR_MESSAGE,
    });
    actor.stop();
  });

  it("clears order data and disables renewal for lifetime plans", async () => {
    const actor = createActor(createTestPaymentMachine()).start();
    await initialize(actor);
    actor.send({ type: "PaymentCreateOrderSubmitted" });
    await waitFor(actor, (snapshot) => snapshot.matches("paid"));
    actor.send({ type: "PaymentPlanSelected", planId: "vip_lifetime" });
    await waitFor(actor, (snapshot) => snapshot.matches("ready"));

    expect(actor.getSnapshot().context).toMatchObject({
      selectedPlanId: "vip_lifetime",
      autoRenew: false,
      currentOrderId: null,
      payParams: null,
      orderStatus: null,
    });
    actor.stop();
  });

  it("disables renewal for credit recharge plans", async () => {
    const actor = createActor(createTestPaymentMachine()).start();
    await initialize(actor);
    actor.send({ type: "PaymentPlanSelected", planId: creditPlan.planId });
    expect(actor.getSnapshot().context).toMatchObject({
      selectedPlanId: creditPlan.planId,
      autoRenew: false,
    });
    actor.stop();
  });
});

async function initialize(
  actor: ReturnType<typeof createActor<ReturnType<typeof createTestPaymentMachine>>>,
): Promise<void> {
  actor.send({ type: "PaymentInit" });
  await waitFor(actor, (snapshot) => snapshot.matches("ready"));
}

async function initializeTip(
  actor: ReturnType<typeof createActor<ReturnType<typeof createTestPaymentMachine>>>,
): Promise<void> {
  actor.send({ type: "PaymentInit", catalog: "tip", characterId: "elio" });
  await waitFor(actor, (snapshot) => snapshot.matches("ready"));
}
