import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PaymentService } from './PaymentService';
import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import type { PaymentListPresenter } from '../../presenters/PaymentListPresenter';
import type {
  GetPaymentsOutputDto,
  CreatePaymentInputDto,
  CreatePaymentOutputDto,
  GetMembershipFeesOutputDto,
  GetPrizesOutputDto,
  GetWalletOutputDto,
  PaymentDto,
  MembershipFeeDto,
  PrizeDto,
  WalletDto,
} from '../../dtos';
import type {
  PaymentViewModel,
  MembershipFeeViewModel,
  PrizeViewModel,
  WalletViewModel,
} from '../../view-models';

describe('PaymentService', () => {
  let service: PaymentService;
  let mockApiClient: PaymentsApiClient;
  let mockPaymentListPresenter: PaymentListPresenter;
  let mockPresentPayment: (dto: any) => PaymentViewModel;
  let mockPresentMembershipFee: (dto: any) => MembershipFeeViewModel;
  let mockPresentPrize: (dto: any) => PrizeViewModel;
  let mockPresentWallet: (dto: any) => WalletViewModel;

  beforeEach(() => {
    mockApiClient = {
      getPayments: vi.fn(),
      createPayment: vi.fn(),
      getMembershipFees: vi.fn(),
      getPrizes: vi.fn(),
      getWallet: vi.fn(),
    } as unknown as PaymentsApiClient;

    mockPaymentListPresenter = {
      present: vi.fn(),
    } as unknown as PaymentListPresenter;

    mockPresentPayment = vi.fn();
    mockPresentMembershipFee = vi.fn();
    mockPresentPrize = vi.fn();
    mockPresentWallet = vi.fn();

    service = new PaymentService(
      mockApiClient,
      mockPaymentListPresenter,
      mockPresentPayment,
      mockPresentMembershipFee,
      mockPresentPrize,
      mockPresentWallet
    );
  });

  describe('constructor', () => {
    it('should create instance with injected dependencies', () => {
      expect(service).toBeInstanceOf(PaymentService);
    });
  });

  describe('getPayments', () => {
    it('should fetch all payments and transform via presenter', async () => {
      // Arrange
      const mockDto: GetPaymentsOutputDto = {
        payments: [
          {
            id: 'payment-1',
            amount: 100,
            currency: 'USD',
            status: 'completed',
            createdAt: '2024-01-01',
          },
          {
            id: 'payment-2',
            amount: 200,
            currency: 'EUR',
            status: 'pending',
            createdAt: '2024-01-02',
          },
        ],
      };

      const mockViewModels: PaymentViewModel[] = [
        {
          id: 'payment-1',
          amount: 100,
          currency: 'USD',
          status: 'completed',
          createdAt: '2024-01-01',
        } as PaymentViewModel,
        {
          id: 'payment-2',
          amount: 200,
          currency: 'EUR',
          status: 'pending',
          createdAt: '2024-01-02',
        } as PaymentViewModel,
      ];

      vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto);
      vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels);

      // Act
      const result = await service.getPayments();

      // Assert
      expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, undefined);
      expect(mockPaymentListPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModels);
    });

    it('should filter payments by leagueId', async () => {
      // Arrange
      const leagueId = 'league-123';
      const mockDto: GetPaymentsOutputDto = { payments: [] };
      const mockViewModels: PaymentViewModel[] = [];

      vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto);
      vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels);

      // Act
      await service.getPayments(leagueId);

      // Assert
      expect(mockApiClient.getPayments).toHaveBeenCalledWith(leagueId, undefined);
    });

    it('should filter payments by driverId', async () => {
      // Arrange
      const driverId = 'driver-456';
      const mockDto: GetPaymentsOutputDto = { payments: [] };
      const mockViewModels: PaymentViewModel[] = [];

      vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto);
      vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels);

      // Act
      await service.getPayments(undefined, driverId);

      // Assert
      expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, driverId);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const error = new Error('Failed to fetch payments');
      vi.mocked(mockApiClient.getPayments).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getPayments()).rejects.toThrow('Failed to fetch payments');
      expect(mockApiClient.getPayments).toHaveBeenCalled();
      expect(mockPaymentListPresenter.present).not.toHaveBeenCalled();
    });
  });

  describe('getPayment', () => {
    it('should fetch single payment by ID', async () => {
      // Arrange
      const paymentId = 'payment-123';
      const mockDto: GetPaymentsOutputDto = {
        payments: [
          {
            id: paymentId,
            amount: 100,
            currency: 'USD',
            status: 'completed',
            createdAt: '2024-01-01',
          },
        ],
      };

      const mockViewModel: PaymentViewModel = {
        id: paymentId,
        amount: 100,
        currency: 'USD',
        status: 'completed',
        createdAt: '2024-01-01',
      } as PaymentViewModel;

      vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto);
      vi.mocked(mockPresentPayment).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getPayment(paymentId);

      // Assert
      expect(mockApiClient.getPayments).toHaveBeenCalled();
      expect(mockPresentPayment).toHaveBeenCalledWith(mockDto.payments[0]);
      expect(result).toEqual(mockViewModel);
    });

    it('should throw error when payment not found', async () => {
      // Arrange
      const paymentId = 'non-existent';
      const mockDto: GetPaymentsOutputDto = { payments: [] };

      vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto);

      // Act & Assert
      await expect(service.getPayment(paymentId)).rejects.toThrow(
        `Payment with ID ${paymentId} not found`
      );
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const error = new Error('API error');
      vi.mocked(mockApiClient.getPayments).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getPayment('payment-123')).rejects.toThrow('API error');
    });
  });

  describe('createPayment', () => {
    it('should create a new payment', async () => {
      // Arrange
      const input: CreatePaymentInputDto = {
        amount: 150,
        currency: 'USD',
        leagueId: 'league-123',
        driverId: 'driver-456',
      };

      const mockOutput: CreatePaymentOutputDto = {
        paymentId: 'payment-new',
        success: true,
      };

      vi.mocked(mockApiClient.createPayment).mockResolvedValue(mockOutput);

      // Act
      const result = await service.createPayment(input);

      // Assert
      expect(mockApiClient.createPayment).toHaveBeenCalledWith(input);
      expect(result).toEqual(mockOutput);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const input: CreatePaymentInputDto = {
        amount: 150,
        currency: 'USD',
      };
      const error = new Error('Payment creation failed');
      vi.mocked(mockApiClient.createPayment).mockRejectedValue(error);

      // Act & Assert
      await expect(service.createPayment(input)).rejects.toThrow('Payment creation failed');
    });
  });

  describe('getMembershipFees', () => {
    it('should fetch membership fees for a league', async () => {
      // Arrange
      const leagueId = 'league-123';
      const mockDto: GetMembershipFeesOutputDto = {
        fees: [
          {
            leagueId,
            amount: 50,
            currency: 'USD',
            period: 'monthly',
          },
        ],
        memberPayments: [],
      };

      const mockViewModel: MembershipFeeViewModel = {
        leagueId,
        amount: 50,
        currency: 'USD',
        period: 'monthly',
      } as MembershipFeeViewModel;

      vi.mocked(mockApiClient.getMembershipFees).mockResolvedValue(mockDto);
      vi.mocked(mockPresentMembershipFee).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getMembershipFees(leagueId);

      // Assert
      expect(mockApiClient.getMembershipFees).toHaveBeenCalledWith(leagueId);
      expect(mockPresentMembershipFee).toHaveBeenCalledWith(mockDto.fees[0]);
      expect(result).toEqual([mockViewModel]);
    });

    it('should handle empty fees list', async () => {
      // Arrange
      const leagueId = 'league-123';
      const mockDto: GetMembershipFeesOutputDto = {
        fees: [],
        memberPayments: [],
      };

      vi.mocked(mockApiClient.getMembershipFees).mockResolvedValue(mockDto);

      // Act
      const result = await service.getMembershipFees(leagueId);

      // Assert
      expect(result).toEqual([]);
      expect(mockPresentMembershipFee).not.toHaveBeenCalled();
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const error = new Error('Failed to fetch fees');
      vi.mocked(mockApiClient.getMembershipFees).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getMembershipFees('league-123')).rejects.toThrow('Failed to fetch fees');
    });
  });

  describe('getPrizes', () => {
    it('should fetch all prizes', async () => {
      // Arrange
      const mockDto: GetPrizesOutputDto = {
        prizes: [
          {
            id: 'prize-1',
            name: 'First Place',
            amount: 1000,
            currency: 'USD',
            position: 1,
          },
        ],
      };

      const mockViewModel: PrizeViewModel = {
        id: 'prize-1',
        name: 'First Place',
        amount: 1000,
        currency: 'USD',
        position: 1,
      } as PrizeViewModel;

      vi.mocked(mockApiClient.getPrizes).mockResolvedValue(mockDto);
      vi.mocked(mockPresentPrize).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getPrizes();

      // Assert
      expect(mockApiClient.getPrizes).toHaveBeenCalledWith(undefined, undefined);
      expect(mockPresentPrize).toHaveBeenCalledWith(mockDto.prizes[0]);
      expect(result).toEqual([mockViewModel]);
    });

    it('should filter prizes by leagueId and seasonId', async () => {
      // Arrange
      const leagueId = 'league-123';
      const seasonId = 'season-456';
      const mockDto: GetPrizesOutputDto = { prizes: [] };

      vi.mocked(mockApiClient.getPrizes).mockResolvedValue(mockDto);

      // Act
      await service.getPrizes(leagueId, seasonId);

      // Assert
      expect(mockApiClient.getPrizes).toHaveBeenCalledWith(leagueId, seasonId);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const error = new Error('Failed to fetch prizes');
      vi.mocked(mockApiClient.getPrizes).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getPrizes()).rejects.toThrow('Failed to fetch prizes');
    });
  });

  describe('getWallet', () => {
    it('should fetch wallet for a driver', async () => {
      // Arrange
      const driverId = 'driver-123';
      const mockDto: GetWalletOutputDto = {
        driverId,
        balance: 500,
        currency: 'USD',
        transactions: [],
      };

      const mockViewModel: WalletViewModel = {
        driverId,
        balance: 500,
        currency: 'USD',
        transactions: [],
      } as WalletViewModel;

      vi.mocked(mockApiClient.getWallet).mockResolvedValue(mockDto);
      vi.mocked(mockPresentWallet).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getWallet(driverId);

      // Assert
      expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId);
      expect(mockPresentWallet).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const error = new Error('Failed to fetch wallet');
      vi.mocked(mockApiClient.getWallet).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getWallet('driver-123')).rejects.toThrow('Failed to fetch wallet');
    });
  });

  describe('processPayment', () => {
    it('should process payment using createPayment', async () => {
      // Arrange
      const input: CreatePaymentInputDto = {
        amount: 100,
        currency: 'USD',
      };

      const mockOutput: CreatePaymentOutputDto = {
        paymentId: 'payment-123',
        success: true,
      };

      vi.mocked(mockApiClient.createPayment).mockResolvedValue(mockOutput);

      // Act
      const result = await service.processPayment(input);

      // Assert
      expect(mockApiClient.createPayment).toHaveBeenCalledWith(input);
      expect(result).toEqual(mockOutput);
    });

    it('should propagate errors', async () => {
      // Arrange
      const input: CreatePaymentInputDto = {
        amount: 100,
        currency: 'USD',
      };
      const error = new Error('Processing failed');
      vi.mocked(mockApiClient.createPayment).mockRejectedValue(error);

      // Act & Assert
      await expect(service.processPayment(input)).rejects.toThrow('Processing failed');
    });
  });

  describe('getPaymentHistory', () => {
    it('should fetch payment history for a driver', async () => {
      // Arrange
      const driverId = 'driver-123';
      const mockDto: GetPaymentsOutputDto = {
        payments: [
          {
            id: 'payment-1',
            amount: 100,
            currency: 'USD',
            status: 'completed',
            createdAt: '2024-01-01',
          },
        ],
      };

      const mockViewModels: PaymentViewModel[] = [
        {
          id: 'payment-1',
          amount: 100,
          currency: 'USD',
          status: 'completed',
          createdAt: '2024-01-01',
        } as PaymentViewModel,
      ];

      vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto);
      vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels);

      // Act
      const result = await service.getPaymentHistory(driverId);

      // Assert
      expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, driverId);
      expect(result).toEqual(mockViewModels);
    });

    it('should propagate errors', async () => {
      // Arrange
      const error = new Error('History fetch failed');
      vi.mocked(mockApiClient.getPayments).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getPaymentHistory('driver-123')).rejects.toThrow('History fetch failed');
    });
  });
});