import prismock from "@calcom/testing/lib/__mocks__/prisma";

import { describe, it, expect, vi, beforeEach } from "vitest";

import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData";
import dayjs from "@calcom/dayjs";
import { RefundPolicy } from "@calcom/lib/payment/types";

import { handlePaymentRefund } from "./handlePaymentRefund";
import { processPaymentRefund } from "./processPaymentRefund";

vi.mock("@calcom/app-store/_utils/payments/getPaymentAppData", () => ({
  getPaymentAppData: vi.fn(),
}));

vi.mock("./handlePaymentRefund", () => ({
  handlePaymentRefund: vi.fn(),
}));
const mockedGetPaymentAppData = vi.mocked(getPaymentAppData);

describe("processPaymentRefund", () => {
  const mockStartTime = new Date("2025-01-01T10:00:00Z");

  const mockPayment = [
    {
      id: 1,
      uid: "unique-id-1",
      appId: "123",
      bookingId: 456,
      amount: 1000,
      fee: 50,
      currency: "USD",
      success: true,
      refunded: false,
      data: {},
      externalId: "ext-1234",
      paymentOption: null,
    },
  ];

  const mockBooking = {
    startTime: mockStartTime,
    endTime: new Date("2025-01-01T11:00:00Z"),
    payment: mockPayment,
    eventType: {
      owner: { id: 1 },
      metadata: {},
    },
  };

  const mockAppData = {
    refundPolicy: RefundPolicy.DAYS,
    refundCountCalendarDays: false,
    refundDaysCount: 3,
  };

  const mockApp = {
    id: "123",
    slug: "stripe",
    dirName: "app1",
    categories: ["payment"],
    keys: {},
    enabled: true,
  };

  const mockCredential = {
    id: 1,
    key: "key1",
    appId: "123",
    userId: 1,
    teamId: null,
    type: "stripe_payment",
    invalid: false,
  };

  beforeEach(async () => {
    vi.clearAllMocks();
    await prismock.credential.deleteMany();
    await prismock.app.deleteMany();
  });

  it("should not process refund if no teamId or eventType owner", async () => {
    await processPaymentRefund({ booking: mockBooking, teamId: null });
    expect(handlePaymentRefund).not.toHaveBeenCalled();
  });

  it("should not process refund if no successful payment found", async () => {
    const invalidBooking = { ...mockBooking, payment: [{ ...mockPayment[0], success: false }] };
    await processPaymentRefund({ booking: invalidBooking, teamId: 1 });
    expect(handlePaymentRefund).not.toHaveBeenCalled();
  });

  it("should not process refund if refund policy is NEVER", async () => {
    mockedGetPaymentAppData.mockReturnValueOnce({ ...mockAppData, refundPolicy: RefundPolicy.NEVER });
    await processPaymentRefund({ booking: mockBooking, teamId: 1 });
    expect(handlePaymentRefund).not.toHaveBeenCalled();
  });

  it("should process refund if refund policy is DAYS and within refund window", async () => {
    mockedGetPaymentAppData.mockReturnValueOnce(mockAppData);
    await prismock.app.create({ data: mockApp });
    await prismock.credential.create({ data: mockCredential });

    const mockNow = dayjs(mockStartTime).subtract(8, "days").toDate();
    vi.useFakeTimers();
    vi.setSystemTime(mockNow);

    await processPaymentRefund({ booking: mockBooking, teamId: 1 });

    expect(handlePaymentRefund).toHaveBeenCalledWith(
      1,
      expect.objectContaining({
        key: "key1",
        appId: "123",
      })
    );
  });

  it("should refund every successful payment on a multi-seat booking, not just the first", async () => {
    mockedGetPaymentAppData.mockReturnValue(mockAppData);
    await prismock.app.create({ data: mockApp });
    await prismock.credential.create({ data: mockCredential });

    // A paid seated booking fans out into one Payment row per seat on the same bookingId.
    const multiSeatBooking = {
      ...mockBooking,
      payment: [
        { ...mockPayment[0], id: 1, uid: "seat-1-pay", externalId: "ext-seat-1" },
        { ...mockPayment[0], id: 2, uid: "seat-2-pay", externalId: "ext-seat-2", amount: 5000 },
      ],
    };

    const mockNow = new Date(mockStartTime.getTime() - 8 * 24 * 60 * 60 * 1000);
    vi.useFakeTimers();
    vi.setSystemTime(mockNow);

    await processPaymentRefund({ booking: multiSeatBooking, teamId: 1 });

    expect(handlePaymentRefund).toHaveBeenCalledTimes(2);
    expect(handlePaymentRefund).toHaveBeenCalledWith(1, expect.objectContaining({ appId: "123" }));
    expect(handlePaymentRefund).toHaveBeenCalledWith(2, expect.objectContaining({ appId: "123" }));
  });

  it("should attempt every refund even when one fails, then surface an aggregate error", async () => {
    mockedGetPaymentAppData.mockReturnValue(mockAppData);
    await prismock.app.create({ data: mockApp });
    await prismock.credential.create({ data: mockCredential });
    vi.mocked(handlePaymentRefund)
      .mockRejectedValueOnce(new Error("provider down"))
      .mockResolvedValueOnce(undefined);

    const multiSeatBooking = {
      ...mockBooking,
      payment: [
        { ...mockPayment[0], id: 1, uid: "seat-1-pay", externalId: "ext-seat-1" },
        { ...mockPayment[0], id: 2, uid: "seat-2-pay", externalId: "ext-seat-2", amount: 5000 },
      ],
    };

    const mockNow = new Date(mockStartTime.getTime() - 8 * 24 * 60 * 60 * 1000);
    vi.useFakeTimers();
    vi.setSystemTime(mockNow);

    await expect(processPaymentRefund({ booking: multiSeatBooking, teamId: 1 })).rejects.toThrow();

    // Both seats are attempted even though the first refund failed.
    expect(handlePaymentRefund).toHaveBeenCalledTimes(2);
  });

  it("should not process refund if past the refund deadline", async () => {
    mockedGetPaymentAppData.mockReturnValueOnce(mockAppData);
    await prismock.app.create({ data: mockApp });
    await prismock.credential.create({ data: mockCredential });

    const mockNow = dayjs(mockStartTime).subtract(2, "days").toDate();

    vi.useFakeTimers();
    vi.setSystemTime(mockNow);

    await processPaymentRefund({ booking: mockBooking, teamId: 1 });

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

  it("should process refund if business days are considered and before deadline", async () => {
    mockedGetPaymentAppData.mockReturnValueOnce({ ...mockAppData, refundCountCalendarDays: false });
    await prismock.app.create({ data: mockApp });
    await prismock.credential.create({ data: mockCredential });

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    //@ts-ignore
    const mockNow = dayjs(mockStartTime).businessDaysSubtract(3).toDate();

    vi.useFakeTimers();
    vi.setSystemTime(mockNow);

    await processPaymentRefund({ booking: mockBooking, teamId: 1 });

    expect(handlePaymentRefund).toHaveBeenCalledWith(
      1,
      expect.objectContaining({
        key: "key1",
        appId: "123",
      })
    );
  });

  it("should not process refund if business days are considered and after deadline", async () => {
    mockedGetPaymentAppData.mockReturnValueOnce({ ...mockAppData, refundCountCalendarDays: false });
    await prismock.app.create({ data: mockApp });
    await prismock.credential.create({ data: mockCredential });

    const mockNow = dayjs(mockStartTime).subtract(3, "days").toDate();

    vi.useFakeTimers();
    vi.setSystemTime(mockNow);

    await processPaymentRefund({ booking: mockBooking, teamId: 1 });

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

  it("should not process refund if paymentAppCredential is not found", async () => {
    mockedGetPaymentAppData.mockReturnValueOnce(mockAppData);

    await processPaymentRefund({ booking: mockBooking, teamId: 1 });
    expect(handlePaymentRefund).not.toHaveBeenCalled();
  });
});
