import { Test, TestingModule } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PaymentsService } from '../payments.service';
import { Payment, PaymentMethod, PaymentStatus } from '../entities';
import { CreatePaymentDto, UpdatePaymentDto } from '../dto';

describe('PaymentsService', () => {
  let service: PaymentsService;
  let paymentRepository: Repository<Payment>;
  let paymentMethodRepository: Repository<PaymentMethod>;

  const mockPayment = {
    id: 'uuid-1',
    tenantId: 'tenant-1',
    invoiceId: 'invoice-1',
    paymentMethodId: 'method-1',
    amount: 1000,
    currency: 'USD',
    status: PaymentStatus.PENDING,
    paymentDate: new Date('2024-01-15'),
    reference: 'REF-001',
    notes: 'Payment for invoice #001',
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockPaymentMethod = {
    id: 'method-1',
    tenantId: 'tenant-1',
    name: 'Bank Transfer',
    type: 'BANK_TRANSFER',
    isActive: true,
    config: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PaymentsService,
        {
          provide: getRepositoryToken(Payment),
          useValue: {
            findOne: jest.fn(),
            find: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
            remove: jest.fn(),
            createQueryBuilder: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(PaymentMethod),
          useValue: {
            findOne: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<PaymentsService>(PaymentsService);
    paymentRepository = module.get<Repository<Payment>>(getRepositoryToken(Payment));
    paymentMethodRepository = module.get<Repository<PaymentMethod>>(getRepositoryToken(PaymentMethod));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('create', () => {
    it('should create a new payment successfully', async () => {
      const dto: CreatePaymentDto = {
        invoiceId: 'invoice-1',
        paymentMethodId: 'method-1',
        amount: 1000,
        currency: 'USD',
        paymentDate: new Date('2024-01-15'),
        reference: 'REF-001',
        notes: 'Payment for invoice #001',
      };

      jest.spyOn(paymentMethodRepository, 'findOne').mockResolvedValue(mockPaymentMethod as any);
      jest.spyOn(paymentRepository, 'create').mockReturnValue(mockPayment as any);
      jest.spyOn(paymentRepository, 'save').mockResolvedValue(mockPayment);

      const result = await service.create(dto);

      expect(paymentMethodRepository.findOne).toHaveBeenCalledWith({
        where: { id: dto.paymentMethodId },
      });
      expect(paymentRepository.create).toHaveBeenCalled();
      expect(paymentRepository.save).toHaveBeenCalled();
      expect(result).toEqual(mockPayment);
    });

    it('should throw error if payment method not found', async () => {
      const dto: CreatePaymentDto = {
        invoiceId: 'invoice-1',
        paymentMethodId: 'invalid-method',
        amount: 1000,
        currency: 'USD',
      };

      jest.spyOn(paymentMethodRepository, 'findOne').mockResolvedValue(null);

      await expect(service.create(dto)).rejects.toThrow('Payment method not found');
    });

    it('should throw error if payment method is inactive', async () => {
      const inactiveMethod = { ...mockPaymentMethod, isActive: false };
      const dto: CreatePaymentDto = {
        invoiceId: 'invoice-1',
        paymentMethodId: 'method-1',
        amount: 1000,
        currency: 'USD',
      };

      jest.spyOn(paymentMethodRepository, 'findOne').mockResolvedValue(inactiveMethod as any);

      await expect(service.create(dto)).rejects.toThrow('Payment method is not active');
    });
  });

  describe('findById', () => {
    it('should find payment by id', async () => {
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);

      const result = await service.findById('uuid-1');

      expect(paymentRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'uuid-1' },
        relations: ['paymentMethod', 'invoice'],
      });
      expect(result).toEqual(mockPayment);
    });

    it('should return null if payment not found', async () => {
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(null);

      const result = await service.findById('invalid-id');

      expect(result).toBeNull();
    });
  });

  describe('findByInvoice', () => {
    it('should find payments by invoice', async () => {
      const mockPayments = [mockPayment, { ...mockPayment, id: 'uuid-2' }];
      jest.spyOn(paymentRepository, 'find').mockResolvedValue(mockPayments as any);

      const result = await service.findByInvoice('invoice-1');

      expect(paymentRepository.find).toHaveBeenCalledWith({
        where: { invoiceId: 'invoice-1' },
        relations: ['paymentMethod'],
        order: { createdAt: 'DESC' },
      });
      expect(result).toEqual(mockPayments);
    });
  });

  describe('update', () => {
    it('should update payment successfully', async () => {
      const dto: UpdatePaymentDto = {
        status: PaymentStatus.COMPLETED,
        notes: 'Payment completed',
      };

      const updatedPayment = { ...mockPayment, status: PaymentStatus.COMPLETED };

      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
      jest.spyOn(paymentRepository, 'save').mockResolvedValue(updatedPayment as any);

      const result = await service.update('uuid-1', dto);

      expect(paymentRepository.findOne).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });
      expect(paymentRepository.save).toHaveBeenCalled();
      expect(result.status).toBe(PaymentStatus.COMPLETED);
    });

    it('should throw error if payment not found', async () => {
      const dto: UpdatePaymentDto = { status: PaymentStatus.COMPLETED };

      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(null);

      await expect(service.update('invalid-id', dto)).rejects.toThrow('Payment not found');
    });
  });

  describe('updateStatus', () => {
    it('should update payment status', async () => {
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
      jest.spyOn(paymentRepository, 'save').mockResolvedValue({
        ...mockPayment,
        status: PaymentStatus.COMPLETED,
      } as any);

      const result = await service.updateStatus('uuid-1', PaymentStatus.COMPLETED);

      expect(result.status).toBe(PaymentStatus.COMPLETED);
    });
  });

  describe('delete', () => {
    it('should delete payment successfully', async () => {
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
      jest.spyOn(paymentRepository, 'remove').mockResolvedValue(undefined);

      await service.delete('uuid-1');

      expect(paymentRepository.remove).toHaveBeenCalledWith(mockPayment);
    });

    it('should throw error if payment not found', async () => {
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(null);

      await expect(service.delete('invalid-id')).rejects.toThrow('Payment not found');
    });

    it('should throw error if payment is completed', async () => {
      const completedPayment = { ...mockPayment, status: PaymentStatus.COMPLETED };
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(completedPayment as any);

      await expect(service.delete('uuid-1')).rejects.toThrow('Cannot delete completed payment');
    });
  });

  describe('getTotalPaid', () => {
    it('should get total paid for invoice', async () => {
      jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue({
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        select: jest.fn().mockReturnThis(),
        getRawOne: jest.fn().mockResolvedValue({ total: 1500 }),
      } as any);

      const result = await service.getTotalPaid('invoice-1');

      expect(result).toBe(1500);
    });

    it('should return 0 if no payments found', async () => {
      jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue({
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        select: jest.fn().mockReturnThis(),
        getRawOne: jest.fn().mockResolvedValue(null),
      } as any);

      const result = await service.getTotalPaid('invoice-1');

      expect(result).toBe(0);
    });
  });

  describe('processRefund', () => {
    it('should process refund successfully', async () => {
      const refundAmount = 500;
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);
      jest.spyOn(paymentRepository, 'save').mockResolvedValue({
        ...mockPayment,
        status: PaymentStatus.REFUNDED,
        refundedAmount: refundAmount,
      } as any);

      const result = await service.processRefund('uuid-1', refundAmount, 'Customer request');

      expect(result.status).toBe(PaymentStatus.REFUNDED);
      expect(result.refundedAmount).toBe(refundAmount);
    });

    it('should throw error if refund amount exceeds payment amount', async () => {
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);

      await expect(service.processRefund('uuid-1', 1500, 'Over refund')).rejects.toThrow('Refund amount cannot exceed payment amount');
    });

    it('should throw error if payment is not completed', async () => {
      jest.spyOn(paymentRepository, 'findOne').mockResolvedValue(mockPayment as any);

      await expect(service.processRefund('uuid-1', 500, 'Refund pending payment')).rejects.toThrow('Can only refund completed payments');
    });
  });

  describe('getPaymentsByDateRange', () => {
    it('should get payments by date range', async () => {
      const startDate = new Date('2024-01-01');
      const endDate = new Date('2024-01-31');
      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        leftJoinAndSelect: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([mockPayment]),
      };

      jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any);

      const result = await service.getPaymentsByDateRange('tenant-1', startDate, endDate);

      expect(paymentRepository.createQueryBuilder).toHaveBeenCalledWith('payment');
      expect(mockQueryBuilder.where).toHaveBeenCalledWith('payment.tenantId = :tenantId', { tenantId: 'tenant-1' });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('payment.paymentDate >= :startDate', { startDate });
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('payment.paymentDate <= :endDate', { endDate });
      expect(result).toEqual([mockPayment]);
    });
  });

  describe('getPaymentSummary', () => {
    it('should get payment summary for tenant', async () => {
      const mockSummary = {
        totalPayments: 10,
        totalAmount: 10000,
        completedPayments: 8,
        completedAmount: 8500,
        pendingPayments: 2,
        pendingAmount: 1500,
      };

      jest.spyOn(paymentRepository, 'createQueryBuilder').mockReturnValue({
        where: jest.fn().mockReturnThis(),
        select: jest.fn().mockReturnThis(),
        getRawOne: jest.fn().mockResolvedValue({ total: 10, amount: 10000 }),
      } as any);

      const result = await service.getPaymentSummary('tenant-1', new Date('2024-01-01'), new Date('2024-01-31'));

      expect(result.totalPayments).toBe(10);
      expect(result.totalAmount).toBe(10000);
    });
  });
});
