import { Test, TestingModule } from '@nestjs/testing';
import { PaymentsService } from './payments.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 { TelebirrProvider } from './providers/telebirr.provider';
import { CbeBirrProvider } from './providers/cbe-birr.provider';
import { EBirrProvider } from './providers/ebirr.provider';
import { CardProvider } from './providers/card.provider';
import { PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
import { BadRequestException, NotFoundException } from '@nestjs/common';

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 mockTelebirrProvider = {
    method: PaymentMethodType.TELEBIRR,
    initiate: jest.fn(),
    queryStatus: jest.fn(),
  };

  const mockCbeBirrProvider = {
    method: PaymentMethodType.CBE_BIRR,
    initiate: jest.fn(),
    queryStatus: jest.fn(),
  };

  const mockEBirrProvider = {
    method: PaymentMethodType.EBIRR,
    initiate: jest.fn(),
    queryStatus: jest.fn(),
  };

  const mockCardProvider = {
    method: PaymentMethodType.CARD,
    initiate: jest.fn(),
    queryStatus: jest.fn(),
  };

  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: TelebirrProvider, useValue: mockTelebirrProvider },
        { provide: CbeBirrProvider, useValue: mockCbeBirrProvider },
        { provide: EBirrProvider, useValue: mockEBirrProvider },
        { provide: CardProvider, useValue: mockCardProvider },
      ],
    }).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();
  });

  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 Telebirr payment successfully', async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      mockPrisma.paymentIntent.findUnique.mockResolvedValue(null);
      mockTelebirrProvider.initiate.mockResolvedValue({
        providerOrderId: 'TB-ORDER-123',
        clientAction: { type: 'REDIRECT', url: 'https://telebirr.com/pay' },
        expiresAt: new Date(),
        rawInitiation: {},
      });
      mockPrisma.paymentIntent.upsert.mockResolvedValue({
        id: 'intent-1',
        status: PaymentIntentStatus.REQUIRES_ACTION,
        merchantOrderId: 'MERCH-123',
        clientAction: { type: 'REDIRECT', url: 'https://telebirr.com/pay' },
      });

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

      expect(result.status).toBe(PaymentIntentStatus.REQUIRES_ACTION);
      expect(mockTelebirrProvider.initiate).toHaveBeenCalled();
    });

    it('should initiate CBE Birr payment successfully', async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      mockPrisma.paymentIntent.findUnique.mockResolvedValue(null);
      mockCbeBirrProvider.initiate.mockResolvedValue({
        providerOrderId: 'CBE-ORDER-123',
        clientAction: { type: 'REDIRECT', url: 'https://cbe.com/pay' },
        expiresAt: new Date(),
        rawInitiation: {},
      });
      mockPrisma.paymentIntent.upsert.mockResolvedValue({
        id: 'intent-1',
        status: PaymentIntentStatus.REQUIRES_ACTION,
        merchantOrderId: 'MERCH-123',
        clientAction: { type: 'REDIRECT', url: 'https://cbe.com/pay' },
      });

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

      expect(result.status).toBe(PaymentIntentStatus.REQUIRES_ACTION);
      expect(mockCbeBirrProvider.initiate).toHaveBeenCalled();
    });

    it('should initiate wallet payment and debit successfully', async () => {
      mockPrisma.booking.findUnique.mockResolvedValue(mockBooking);
      mockPrisma.paymentIntent.findUnique.mockResolvedValue(null);
      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();
    });

    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 intent status', 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);

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

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

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

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