import { Test, TestingModule } from "@nestjs/testing";
import { PaymentsService } from "./payments.service";
import { PaymentClientService } from "./payment-client.service";
import { PrismaService } from "../../common/prisma.service";
import { SeatsService } from "../seats/seats.service";
import { TicketsService } from "../tickets/tickets.service";
import { EventEmitter2 } from "@nestjs/event-emitter";
import { PaymentIntentStatus, PaymentMethodType } from "@prisma/client";
import { BadRequestException, NotFoundException } from "@nestjs/common";
import {
  PaymentIntentSnapshot,
  PaymentReferenceType,
  PaymentService as PaymentServiceEnum,
  ProviderMethod,
  ProviderPaymentStatus,
} from "@edr/types";

describe("PaymentsService", () => {
  let service: PaymentsService;
  let prisma: PrismaService;
  let seatsService: SeatsService;
  let ticketsService: TicketsService;
  let eventEmitter: EventEmitter2;

  const mockPrisma: Record<string, any> = {
    booking: {
      findUnique: jest.fn(),
      update: jest.fn(),
    },
    paymentIntent: {
      findUnique: jest.fn(),
      findUniqueOrThrow: jest.fn(),
      upsert: jest.fn(),
      update: jest.fn(),
      create: jest.fn(),
    },
    walletAccount: {
      findUnique: jest.fn(),
      update: jest.fn(),
    },
    walletLedgerEntry: {
      create: jest.fn(),
    },
    loyaltyAccount: {
      findUnique: jest.fn(),
      update: jest.fn(),
    },
    loyaltyLedgerEntry: {
      create: jest.fn(),
    },
    $transaction: jest.fn((callback: (tx: any) => any) => callback(mockPrisma)),
  };

  const mockSeatsService = {
    confirmSeats: jest.fn(),
    releaseSeats: jest.fn(),
  };

  const mockTicketsService = {
    generate: jest.fn(),
  };

  const mockEventEmitter = {
    emit: jest.fn(),
  };

  const mockPaymentClient = {
    initiate: jest.fn(),
    getIntentByReference: jest.fn(),
  };

  const requiresActionSnapshot = (
    provider: ProviderMethod,
  ): PaymentIntentSnapshot => ({
    intentId: "remote-intent-1",
    service: PaymentServiceEnum.PASSENGER,
    referenceType: PaymentReferenceType.BOOKING,
    referenceId: "booking-1",
    merchantOrderId: "PSG-MERCH-123",
    provider,
    status: ProviderPaymentStatus.REQUIRES_ACTION,
    amountMinor: 50000,
    currency: "ETB",
    clientAction: { type: "REDIRECT", url: "https://provider.example/pay" },
  });

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PaymentsService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: SeatsService, useValue: mockSeatsService },
        { provide: TicketsService, useValue: mockTicketsService },
        { provide: EventEmitter2, useValue: mockEventEmitter },
        { provide: PaymentClientService, useValue: mockPaymentClient },
      ],
    }).compile();

    service = module.get<PaymentsService>(PaymentsService);
    prisma = module.get<PrismaService>(PrismaService);
    seatsService = module.get<SeatsService>(SeatsService);
    ticketsService = module.get<TicketsService>(TicketsService);
    eventEmitter = module.get<EventEmitter2>(EventEmitter2);

    jest.clearAllMocks();
    mockPaymentClient.getIntentByReference.mockResolvedValue(null);
  });

  describe("initiatePayment", () => {
    const mockBooking = {
      id: "booking-1",
      bookingRef: "EDR123456",
      passengerId: "passenger-1",
      totalMinor: 50000,
      currency: "ETB",
      status: "PENDING_PAYMENT",
      seats: [{ id: "seat-1", seatId: "seat-id-1" }],
    };

    it("should throw NotFoundException if booking not found", async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(null);

      await expect(
        service.initiatePayment({
          bookingId: "invalid",
          method: "TELEBIRR" as any,
        }),
      ).rejects.toThrow(NotFoundException);
    });

    it("should throw BadRequestException if booking not payable", async () => {
      mockPrisma.booking.findUnique.mockResolvedValue({
        ...mockBooking,
        status: "CONFIRMED",
      });

      await expect(
        service.initiatePayment({
          bookingId: "booking-1",
          method: "TELEBIRR" as any,
        }),
      ).rejects.toThrow(BadRequestException);
    });

    it("should initiate a provider payment through the payment microservice", async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      mockPaymentClient.initiate.mockResolvedValue(
        requiresActionSnapshot(ProviderMethod.TELEBIRR),
      );
      mockPrisma.paymentIntent.upsert.mockResolvedValue({
        id: "intent-1",
        status: PaymentIntentStatus.REQUIRES_ACTION,
        merchantOrderId: "PSG-MERCH-123",
        clientAction: { type: "REDIRECT", url: "https://provider.example/pay" },
      });

      const result = await service.initiatePayment({
        bookingId: "booking-1",
        method: "TELEBIRR" as any,
      });

      expect(result.status).toBe(PaymentIntentStatus.REQUIRES_ACTION);
      expect(result.clientAction?.url).toBe("https://provider.example/pay");
      expect(mockPaymentClient.initiate).toHaveBeenCalledWith(
        expect.objectContaining({
          service: PaymentServiceEnum.PASSENGER,
          referenceType: PaymentReferenceType.BOOKING,
          referenceId: "booking-1",
          orderRef: "EDR123456",
          amountMinor: 50000,
          currency: "ETB",
          provider: "TELEBIRR",
        }),
      );
      // Snapshot mirrored into the local projection.
      expect(mockPrisma.paymentIntent.upsert).toHaveBeenCalledWith(
        expect.objectContaining({ where: { bookingId: "booking-1" } }),
      );
    });

    it("should finalize the booking when the service reports an already-paid intent", async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      mockPaymentClient.initiate.mockResolvedValue({
        ...requiresActionSnapshot(ProviderMethod.WAAFI),
        status: ProviderPaymentStatus.SUCCEEDED,
        providerTxnId: "TXN-1",
        paidAt: new Date().toISOString(),
      });
      // Projection clamps SUCCEEDED to PROCESSING; finalizePaymentSuccess flips it.
      mockPrisma.paymentIntent.upsert.mockResolvedValue({
        id: "intent-1",
        bookingId: "booking-1",
        status: PaymentIntentStatus.PROCESSING,
      });
      mockPrisma.paymentIntent.findUnique.mockResolvedValue({
        id: "intent-1",
        bookingId: "booking-1",
        status: PaymentIntentStatus.PROCESSING,
      });
      mockPrisma.paymentIntent.findUniqueOrThrow.mockResolvedValue({
        id: "intent-1",
        status: PaymentIntentStatus.SUCCEEDED,
        merchantOrderId: "PSG-MERCH-123",
      });
      mockPrisma.loyaltyAccount.findUnique.mockResolvedValue(null);

      const result = await service.initiatePayment({
        bookingId: "booking-1",
        method: "WAAFI" as any,
      });

      expect(result.status).toBe(PaymentIntentStatus.SUCCEEDED);
      expect(mockTicketsService.generate).toHaveBeenCalledWith("booking-1");
    });

    it("should initiate wallet payment and debit successfully", async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      // First call: existing-intent check (none); second call: finalize loads the new intent.
      mockPrisma.paymentIntent.findUnique
        .mockResolvedValueOnce(null)
        .mockResolvedValue({
          id: "intent-1",
          bookingId: "booking-1",
          status: PaymentIntentStatus.PROCESSING,
        });
      mockPrisma.walletAccount.findUnique.mockResolvedValue({
        id: "wallet-1",
        passengerId: "passenger-1",
        balanceMinor: 100000,
      });
      mockPrisma.paymentIntent.upsert.mockResolvedValue({
        id: "intent-1",
        status: PaymentIntentStatus.PROCESSING,
      });
      mockPrisma.paymentIntent.findUniqueOrThrow.mockResolvedValue({
        id: "intent-1",
        status: PaymentIntentStatus.SUCCEEDED,
        bookingId: "booking-1",
      });
      mockPrisma.loyaltyAccount.findUnique.mockResolvedValue({
        id: "loyalty-1",
        pointsBalance: 100,
      });

      const result = await service.initiatePayment({
        bookingId: "booking-1",
        method: "WALLET" as any,
      });

      expect(result.status).toBe(PaymentIntentStatus.SUCCEEDED);
      expect(mockSeatsService.confirmSeats).toHaveBeenCalled();
      expect(mockTicketsService.generate).toHaveBeenCalled();
      expect(mockPaymentClient.initiate).not.toHaveBeenCalled();
    });

    it("should fail wallet payment with insufficient balance", async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      mockPrisma.paymentIntent.findUnique.mockResolvedValue(null);
      mockPrisma.walletAccount.findUnique.mockResolvedValue({
        id: "wallet-1",
        passengerId: "passenger-1",
        balanceMinor: 10000, // Less than booking total
      });
      mockPrisma.paymentIntent.upsert.mockResolvedValue({
        id: "intent-1",
        status: PaymentIntentStatus.FAILED,
        failureCode: "INSUFFICIENT_BALANCE",
      });

      const result = await service.initiatePayment({
        bookingId: "booking-1",
        method: "WALLET" as any,
      });

      expect(result.status).toBe(PaymentIntentStatus.FAILED);
    });
  });

  describe("finalizePaymentSuccess", () => {
    it("should finalize payment and issue ticket", async () => {
      const mockIntent = {
        id: "intent-1",
        bookingId: "booking-1",
        status: PaymentIntentStatus.PROCESSING,
      };
      const mockBooking = {
        id: "booking-1",
        passengerId: "passenger-1",
        totalMinor: 50000,
        seats: [{ seatId: "seat-1" }],
      };

      mockPrisma.paymentIntent.findUnique.mockResolvedValue(mockIntent);
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      mockPrisma.loyaltyAccount.findUnique.mockResolvedValue({
        id: "loyalty-1",
        pointsBalance: 100,
      });

      const result = await service.finalizePaymentSuccess({
        intentId: "intent-1",
        providerTxnId: "TXN-123",
      });

      expect(result.alreadyFinalized).toBe(false);
      expect(mockSeatsService.confirmSeats).toHaveBeenCalledWith(["seat-1"]);
      expect(mockTicketsService.generate).toHaveBeenCalledWith("booking-1");
      expect(mockEventEmitter.emit).toHaveBeenCalledWith("payment.succeeded", {
        booking: mockBooking,
      });
    });

    it("should return alreadyFinalized if payment already succeeded", async () => {
      mockPrisma.paymentIntent.findUnique.mockResolvedValue({
        id: "intent-1",
        status: PaymentIntentStatus.SUCCEEDED,
      });

      const result = await service.finalizePaymentSuccess({
        intentId: "intent-1",
      });

      expect(result.alreadyFinalized).toBe(true);
    });
  });

  describe("getIntentByBookingId", () => {
    it("should return the cached local intent when the payment service has none", async () => {
      const mockIntent = {
        id: "intent-1",
        bookingId: "booking-1",
        status: PaymentIntentStatus.SUCCEEDED,
        method: PaymentMethodType.TELEBIRR,
        paidAt: new Date(),
        merchantOrderId: "MERCH-123",
        updatedAt: new Date(),
      };

      mockPrisma.paymentIntent.findUnique.mockResolvedValue(mockIntent);
      mockPaymentClient.getIntentByReference.mockResolvedValue(null);

      const result = await service.getIntentByBookingId("booking-1");

      expect(result.intentId).toBe("intent-1");
      expect(result.status).toBe(PaymentIntentStatus.SUCCEEDED);
    });

    it("should mirror a payment-service snapshot into the local projection", async () => {
      mockPrisma.paymentIntent.findUnique.mockResolvedValue(null);
      mockPaymentClient.getIntentByReference.mockResolvedValue(
        requiresActionSnapshot(ProviderMethod.WAAFI),
      );
      mockPrisma.paymentIntent.upsert.mockResolvedValue({
        id: "intent-1",
        bookingId: "booking-1",
        status: PaymentIntentStatus.REQUIRES_ACTION,
        merchantOrderId: "PSG-MERCH-123",
        clientAction: { type: "REDIRECT", url: "https://provider.example/pay" },
      });

      const result = await service.getIntentByBookingId("booking-1");

      expect(mockPaymentClient.getIntentByReference).toHaveBeenCalledWith(
        PaymentReferenceType.BOOKING,
        "booking-1",
      );
      expect(result.status).toBe(PaymentIntentStatus.REQUIRES_ACTION);
      expect(result.clientAction?.url).toBe("https://provider.example/pay");
    });

    it("should throw NotFoundException if intent not found anywhere", async () => {
      mockPrisma.paymentIntent.findUnique.mockResolvedValue(null);
      mockPaymentClient.getIntentByReference.mockResolvedValue(null);

      await expect(service.getIntentByBookingId("invalid")).rejects.toThrow(
        NotFoundException,
      );
    });
  });
});
