import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DriverRegistrationService } from './DriverRegistrationService';
import type { DriversApiClient } from '../../api/drivers/DriversApiClient';
import type { DriverRegistrationStatusPresenter } from '../../presenters/DriverRegistrationStatusPresenter';
import type { DriverRegistrationStatusDto } from '../../dtos';
import type { DriverRegistrationStatusViewModel } from '../../view-models';

describe('DriverRegistrationService', () => {
  let service: DriverRegistrationService;
  let mockApiClient: DriversApiClient;
  let mockStatusPresenter: DriverRegistrationStatusPresenter;

  beforeEach(() => {
    mockApiClient = {
      getRegistrationStatus: vi.fn(),
    } as unknown as DriversApiClient;

    mockStatusPresenter = {
      present: vi.fn(),
    } as unknown as DriverRegistrationStatusPresenter;

    service = new DriverRegistrationService(
      mockApiClient,
      mockStatusPresenter
    );
  });

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

  describe('getDriverRegistrationStatus', () => {
    it('should fetch registration status from API and transform via presenter', async () => {
      // Arrange
      const driverId = 'driver-123';
      const raceId = 'race-456';

      const mockDto: DriverRegistrationStatusDto = {
        isRegistered: true,
        raceId: 'race-456',
        driverId: 'driver-123',
      };

      const mockViewModel = {
        isRegistered: true,
        raceId: 'race-456',
        driverId: 'driver-123',
        statusMessage: 'Registered for this race',
        statusColor: 'green',
        statusBadgeVariant: 'success',
        registrationButtonText: 'Withdraw',
        canRegister: false,
      } as DriverRegistrationStatusViewModel;

      vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto);
      vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getDriverRegistrationStatus(driverId, raceId);

      // Assert
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
      expect(mockStatusPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle unregistered driver status', async () => {
      // Arrange
      const driverId = 'driver-789';
      const raceId = 'race-101';

      const mockDto: DriverRegistrationStatusDto = {
        isRegistered: false,
        raceId: 'race-101',
        driverId: 'driver-789',
      };

      const mockViewModel = {
        isRegistered: false,
        raceId: 'race-101',
        driverId: 'driver-789',
        statusMessage: 'Not registered',
        statusColor: 'red',
        statusBadgeVariant: 'warning',
        registrationButtonText: 'Register',
        canRegister: true,
      } as DriverRegistrationStatusViewModel;

      vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto);
      vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getDriverRegistrationStatus(driverId, raceId);

      // Assert
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
      expect(mockStatusPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
      expect(result.canRegister).toBe(true);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const driverId = 'driver-123';
      const raceId = 'race-456';
      const error = new Error('Failed to fetch registration status');
      
      vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getDriverRegistrationStatus(driverId, raceId))
        .rejects.toThrow('Failed to fetch registration status');
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
      expect(mockStatusPresenter.present).not.toHaveBeenCalled();
    });

    it('should handle network errors gracefully', async () => {
      // Arrange
      const driverId = 'driver-123';
      const raceId = 'race-456';
      const networkError = new Error('Network request failed');
      
      vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(networkError);

      // Act & Assert
      await expect(service.getDriverRegistrationStatus(driverId, raceId))
        .rejects.toThrow('Network request failed');
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
    });

    it('should handle API errors with proper error propagation', async () => {
      // Arrange
      const driverId = 'driver-123';
      const raceId = 'race-456';
      const apiError = new Error('Race not found');
      
      vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(apiError);

      // Act & Assert
      await expect(service.getDriverRegistrationStatus(driverId, raceId))
        .rejects.toThrow('Race not found');
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
      expect(mockStatusPresenter.present).not.toHaveBeenCalled();
    });

    it('should handle multiple consecutive calls correctly', async () => {
      // Arrange
      const driverId = 'driver-123';
      const raceId1 = 'race-456';
      const raceId2 = 'race-789';

      const mockDto1: DriverRegistrationStatusDto = {
        isRegistered: true,
        raceId: raceId1,
        driverId,
      };

      const mockDto2: DriverRegistrationStatusDto = {
        isRegistered: false,
        raceId: raceId2,
        driverId,
      };

      const mockViewModel1 = {
        isRegistered: true,
        raceId: raceId1,
        driverId,
      } as DriverRegistrationStatusViewModel;

      const mockViewModel2 = {
        isRegistered: false,
        raceId: raceId2,
        driverId,
      } as DriverRegistrationStatusViewModel;

      vi.mocked(mockApiClient.getRegistrationStatus)
        .mockResolvedValueOnce(mockDto1)
        .mockResolvedValueOnce(mockDto2);

      vi.mocked(mockStatusPresenter.present)
        .mockReturnValueOnce(mockViewModel1)
        .mockReturnValueOnce(mockViewModel2);

      // Act
      const result1 = await service.getDriverRegistrationStatus(driverId, raceId1);
      const result2 = await service.getDriverRegistrationStatus(driverId, raceId2);

      // Assert
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledTimes(2);
      expect(mockApiClient.getRegistrationStatus).toHaveBeenNthCalledWith(1, driverId, raceId1);
      expect(mockApiClient.getRegistrationStatus).toHaveBeenNthCalledWith(2, driverId, raceId2);
      expect(result1.isRegistered).toBe(true);
      expect(result2.isRegistered).toBe(false);
    });

    it('should handle different driver IDs for same race', async () => {
      // Arrange
      const driverId1 = 'driver-123';
      const driverId2 = 'driver-456';
      const raceId = 'race-789';

      const mockDto1: DriverRegistrationStatusDto = {
        isRegistered: true,
        raceId,
        driverId: driverId1,
      };

      const mockDto2: DriverRegistrationStatusDto = {
        isRegistered: false,
        raceId,
        driverId: driverId2,
      };

      const mockViewModel1 = {
        isRegistered: true,
        raceId,
        driverId: driverId1,
      } as DriverRegistrationStatusViewModel;

      const mockViewModel2 = {
        isRegistered: false,
        raceId,
        driverId: driverId2,
      } as DriverRegistrationStatusViewModel;

      vi.mocked(mockApiClient.getRegistrationStatus)
        .mockResolvedValueOnce(mockDto1)
        .mockResolvedValueOnce(mockDto2);

      vi.mocked(mockStatusPresenter.present)
        .mockReturnValueOnce(mockViewModel1)
        .mockReturnValueOnce(mockViewModel2);

      // Act
      const result1 = await service.getDriverRegistrationStatus(driverId1, raceId);
      const result2 = await service.getDriverRegistrationStatus(driverId2, raceId);

      // Assert
      expect(result1.driverId).toBe(driverId1);
      expect(result1.isRegistered).toBe(true);
      expect(result2.driverId).toBe(driverId2);
      expect(result2.isRegistered).toBe(false);
    });

    it('should handle unauthorized access errors', async () => {
      // Arrange
      const driverId = 'driver-123';
      const raceId = 'race-456';
      const authError = new Error('Unauthorized: Driver not found');
      
      vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(authError);

      // Act & Assert
      await expect(service.getDriverRegistrationStatus(driverId, raceId))
        .rejects.toThrow('Unauthorized: Driver not found');
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
      expect(mockStatusPresenter.present).not.toHaveBeenCalled();
    });

    it('should call presenter only after successful API response', async () => {
      // Arrange
      const driverId = 'driver-123';
      const raceId = 'race-456';

      const mockDto: DriverRegistrationStatusDto = {
        isRegistered: true,
        raceId: 'race-456',
        driverId: 'driver-123',
      };

      const mockViewModel = {
        isRegistered: true,
        raceId: 'race-456',
        driverId: 'driver-123',
      } as DriverRegistrationStatusViewModel;

      vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto);
      vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel);

      // Act
      await service.getDriverRegistrationStatus(driverId, raceId);

      // Assert - verify call order
      expect(mockApiClient.getRegistrationStatus).toHaveBeenCalled();
      expect(mockStatusPresenter.present).toHaveBeenCalledAfter(
        mockApiClient.getRegistrationStatus as any
      );
    });
  });
});