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

import {
  CreatePaymentOrderResponse,
  type PayChannel,
  PaymentOrderStatusResponse,
  PaymentPlansResponse,
} from "@/data/dto/payment";
import { paymentMachine } from "@/stores/payment/payment-machine";
import {
  MAX_ORDER_POLLING_MS,
  PAYMENT_TIMEOUT_ERROR_MESSAGE,
} from "@/stores/payment/payment-machine.helpers";

interface CreateOrderInput {
  planId: string;
  payChannel: PayChannel;
  autoRenew: boolean;
}

type CreateOrderSpy = (input: CreateOrderInput) => void;

const monthlyPlan = {
  planId: "vip_monthly",
  planName: "VIP Monthly",
  orderType: "vip_monthly",
  vipDays: 30,
  dolAmount: null,
  creditBalance: 3000,
  amountCents: 999,
  originalAmountCents: null,
  dailyPriceCents: 33,
  currency: "usd",
};

const lifetimePlan = {
  planId: "vip_lifetime",
  planName: "VIP Lifetime",
  orderType: "vip_lifetime",
  vipDays: 3650,
  dolAmount: null,
  creditBalance: 0,
  amountCents: 4999,
  originalAmountCents: null,
  dailyPriceCents: null,
  currency: "usd",
};

const creditPlan = {
  planId: "dol_1000",
  planName: "1000 Credits",
  orderType: "dol",
  vipDays: null,
  dolAmount: 1000,
  creditBalance: 1000,
  amountCents: 990,
  originalAmountCents: null,
  dailyPriceCents: null,
  currency: "usd",
};

const quarterlyPlan = {
  planId: "vip_quarterly",
  planName: "VIP Quarterly",
  orderType: "vip_quarterly",
  vipDays: 90,
  dolAmount: null,
  creditBalance: 9000,
  amountCents: 2499,
  originalAmountCents: null,
  dailyPriceCents: 28,
  currency: "usd",
};

function createTestPaymentMachine(
  overrides: Partial<{
    createOrderSpy: CreateOrderSpy;
    orderStatus: "pending" | "paid" | "failed";
    orderStatuses: Array<"pending" | "paid" | "failed">;
  }> = {},
) {
  const createOrderSpy = overrides.createOrderSpy ?? vi.fn<CreateOrderSpy>();
  const orderStatus = overrides.orderStatus ?? "paid";
  const orderStatuses = overrides.orderStatuses ?? [];
  let pollCount = 0;

  return paymentMachine.provide({
    actors: {
      loadCachedPlans: fromPromise<PaymentPlansResponse | null>(
        async () => null,
      ),
      refreshPlans: fromPromise(async () =>
        PaymentPlansResponse.from({
          plans: [monthlyPlan, lifetimePlan],
        }),
      ),
      createOrder: fromPromise<
        CreatePaymentOrderResponse,
        CreateOrderInput
      >(async ({ input }) => {
        createOrderSpy(input);
        return CreatePaymentOrderResponse.from({
          orderId: "pay_test_001",
          payParams: {
            provider: "stripe",
            clientSecret: "pi_test_secret_test",
          },
        });
      }),
      pollOrderStatus: fromPromise(async () => {
        const status =
          orderStatuses[Math.min(pollCount, orderStatuses.length - 1)] ??
          orderStatus;
        pollCount += 1;

        return PaymentOrderStatusResponse.from({
          orderId: "pay_test_001",
          status,
          orderType: "vip_monthly",
          planId: "vip_monthly",
        });
      }),
    },
  });
}

describe("paymentMachine", () => {
  afterEach(() => {
    vi.useRealTimers();
  });

  it("loads plans and selects the default monthly VIP plan", async () => {
    const actor = createActor(createTestPaymentMachine()).start();

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

    const context = actor.getSnapshot().context;
    expect(context.plans).toHaveLength(2);
    expect(context.selectedPlanId).toBe("vip_monthly");
    expect(context.autoRenew).toBe(true);
    expect(context.errorMessage).toBeNull();

    actor.stop();
  });

  it("hydrates cached plans before refreshing with network plans", async () => {
    let resolveRefresh!: (value: PaymentPlansResponse) => void;
    const refreshPlansPromise = new Promise<PaymentPlansResponse>((resolve) => {
      resolveRefresh = resolve;
    });
    const actor = createActor(
      paymentMachine.provide({
        actors: {
          loadCachedPlans: fromPromise<PaymentPlansResponse | null>(
            async () =>
              PaymentPlansResponse.from({
                plans: [quarterlyPlan],
              }),
          ),
          refreshPlans: fromPromise<PaymentPlansResponse>(
            async () => refreshPlansPromise,
          ),
        },
      }),
    ).start();

    actor.send({ type: "PaymentInit" });

    await waitFor(actor, (snapshot) =>
      snapshot.matches("refreshingPlans"),
    );
    expect(actor.getSnapshot().context.plans[0]?.planId).toBe(
      "vip_quarterly",
    );

    resolveRefresh(
      PaymentPlansResponse.from({
        plans: [monthlyPlan, creditPlan],
      }),
    );
    await waitFor(actor, (snapshot) => snapshot.matches("ready"));

    const context = actor.getSnapshot().context;
    expect(context.plans.map((plan) => plan.planId)).toEqual([
      "vip_monthly",
      "dol_1000",
    ]);
    expect(context.selectedPlanId).toBe("vip_monthly");

    actor.stop();
  });

  it("does not create an order before the agreement is accepted", async () => {
    const createOrderSpy = vi.fn<CreateOrderSpy>();
    const actor = createActor(
      createTestPaymentMachine({ createOrderSpy }),
    ).start();

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

    actor.send({ type: "PaymentAgreementChanged", agreed: false });
    actor.send({ type: "PaymentCreateOrderSubmitted" });

    const snapshot = actor.getSnapshot();
    expect(snapshot.matches("ready")).toBe(true);
    expect(snapshot.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 state", async () => {
    const createOrderSpy = vi.fn<CreateOrderSpy>();
    const actor = createActor(
      createTestPaymentMachine({ createOrderSpy }),
    ).start();

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

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

    const context = actor.getSnapshot().context;
    expect(createOrderSpy).toHaveBeenCalledWith({
      planId: "vip_monthly",
      payChannel: "stripe",
      autoRenew: true,
    });
    expect(context.currentOrderId).toBe("pay_test_001");
    expect(context.orderStatus).toBe("paid");
    expect(context.orderPollingStartedAt).toBeNull();
    expect(context.launchNonce).toBe(1);

    actor.stop();
  });

  it("moves to waiting state for pending orders and can reset order data", async () => {
    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "pending" }),
    ).start();

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

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

    expect(actor.getSnapshot().context.currentOrderId).toBe("pay_test_001");
    expect(actor.getSnapshot().context.orderStatus).toBe("pending");
    expect(actor.getSnapshot().context.orderPollingStartedAt).not.toBeNull();

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

    const context = actor.getSnapshot().context;
    expect(context.currentOrderId).toBeNull();
    expect(context.payParams).toBeNull();
    expect(context.orderStatus).toBeNull();
    expect(context.orderPollingStartedAt).toBeNull();
    expect(context.errorMessage).toBeNull();

    actor.stop();
  });

  it("moves to failed state when polling returns failed", async () => {
    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "failed" }),
    ).start();

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

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

    const context = actor.getSnapshot().context;
    expect(context.orderStatus).toBe("failed");
    expect(context.orderPollingStartedAt).toBeNull();
    expect(context.errorMessage).toBe("Payment failed or was cancelled.");

    actor.stop();
  });

  it("moves to failed state when a pending order exceeds polling timeout", async () => {
    const now = new Date("2026-06-30T00:00:00.000Z");
    vi.useFakeTimers();
    vi.setSystemTime(now);

    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "pending" }),
    ).start();

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

    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"));

    const context = actor.getSnapshot().context;
    expect(context.orderStatus).toBe("failed");
    expect(context.orderPollingStartedAt).toBeNull();
    expect(context.errorMessage).toBe(PAYMENT_TIMEOUT_ERROR_MESSAGE);

    actor.stop();
  });

  it("keeps paid response higher priority than polling 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();

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

    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"));

    const context = actor.getSnapshot().context;
    expect(context.orderStatus).toBe("paid");
    expect(context.orderPollingStartedAt).toBeNull();
    expect(context.errorMessage).toBeNull();

    actor.stop();
  });

  it("uses restored order creation time when polling returned payment", async () => {
    const now = new Date("2026-06-30T00:00:00.000Z");
    vi.useFakeTimers();
    vi.setSystemTime(now);

    const actor = createActor(
      createTestPaymentMachine({ orderStatus: "pending" }),
    ).start();

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

    actor.send({
      type: "PaymentReturned",
      orderId: "pay_returned_001",
      createdAt: now.getTime() - MAX_ORDER_POLLING_MS,
    });
    await waitFor(actor, (snapshot) => snapshot.matches("failed"));

    const context = actor.getSnapshot().context;
    expect(context.currentOrderId).toBe("pay_returned_001");
    expect(context.orderStatus).toBe("failed");
    expect(context.orderPollingStartedAt).toBeNull();
    expect(context.errorMessage).toBe(PAYMENT_TIMEOUT_ERROR_MESSAGE);

    actor.stop();
  });

  it("clears stale order data and disables auto renew for lifetime plans", async () => {
    const actor = createActor(createTestPaymentMachine()).start();

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

    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"));

    const context = actor.getSnapshot().context;
    expect(context.selectedPlanId).toBe("vip_lifetime");
    expect(context.autoRenew).toBe(false);
    expect(context.currentOrderId).toBeNull();
    expect(context.payParams).toBeNull();
    expect(context.orderStatus).toBeNull();
    expect(context.orderPollingStartedAt).toBeNull();

    actor.stop();
  });

  it("disables auto renew for credit recharge plans", async () => {
    const actor = createActor(createTestPaymentMachine()).start();

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

    actor.send({ type: "PaymentPlanSelected", planId: creditPlan.planId });

    const context = actor.getSnapshot().context;
    expect(context.selectedPlanId).toBe("dol_1000");
    expect(context.autoRenew).toBe(false);

    actor.stop();
  });
});
