import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DriverService } from './DriverService';
import type { DriversApiClient } from '../../api/drivers/DriversApiClient';
import type { DriversLeaderboardPresenter } from '../../presenters/DriversLeaderboardPresenter';
import type { DriverPresenter } from '../../presenters/DriverPresenter';
import type { CompleteOnboardingPresenter } from '../../presenters/CompleteOnboardingPresenter';
import type { DriversLeaderboardDto, CompleteOnboardingOutputDto, DriverDto, CompleteOnboardingInputDto } from '../../dtos';
import type { DriverLeaderboardViewModel, DriverViewModel, CompleteOnboardingViewModel } from '../../view-models';

describe('DriverService', () => {
  let service: DriverService;
  let mockApiClient: DriversApiClient;
  let mockLeaderboardPresenter: DriversLeaderboardPresenter;
  let mockDriverPresenter: DriverPresenter;
  let mockOnboardingPresenter: CompleteOnboardingPresenter;

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

    mockLeaderboardPresenter = {
      present: vi.fn(),
    } as unknown as DriversLeaderboardPresenter;

    mockDriverPresenter = {
      present: vi.fn(),
    } as unknown as DriverPresenter;

    mockOnboardingPresenter = {
      present: vi.fn(),
    } as unknown as CompleteOnboardingPresenter;

    service = new DriverService(
      mockApiClient,
      mockLeaderboardPresenter,
      mockDriverPresenter,
      mockOnboardingPresenter
    );
  });

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

  describe('getDriverLeaderboard', () => {
    it('should fetch leaderboard from API and transform via presenter', async () => {
      // Arrange
      const mockDto: DriversLeaderboardDto = {
        drivers: [
          {
            id: 'driver-1',
            name: 'John Doe',
            rating: 2500,
            races: 50,
            wins: 10,
            isActive: true,
          },
          {
            id: 'driver-2',
            name: 'Jane Smith',
            rating: 2300,
            races: 40,
            wins: 8,
            isActive: true,
          },
        ],
      };

      const mockViewModel = {
        drivers: [
          {
            id: 'driver-1',
            name: 'John Doe',
            rating: 2500,
            races: 50,
            wins: 10,
            isActive: true,
          },
          {
            id: 'driver-2',
            name: 'Jane Smith',
            rating: 2300,
            races: 40,
            wins: 8,
            isActive: true,
          },
        ],
      } as DriverLeaderboardViewModel;

      vi.mocked(mockApiClient.getLeaderboard).mockResolvedValue(mockDto);
      vi.mocked(mockLeaderboardPresenter.present).mockReturnValue(mockViewModel);

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

      // Assert
      expect(mockApiClient.getLeaderboard).toHaveBeenCalled();
      expect(mockLeaderboardPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle empty leaderboard', async () => {
      // Arrange
      const mockDto: DriversLeaderboardDto = {
        drivers: [],
      };

      const mockViewModel = {
        drivers: [],
      } as DriverLeaderboardViewModel;

      vi.mocked(mockApiClient.getLeaderboard).mockResolvedValue(mockDto);
      vi.mocked(mockLeaderboardPresenter.present).mockReturnValue(mockViewModel);

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

      // Assert
      expect(mockApiClient.getLeaderboard).toHaveBeenCalled();
      expect(mockLeaderboardPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

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

      // Act & Assert
      await expect(service.getDriverLeaderboard()).rejects.toThrow('Leaderboard fetch failed');
      expect(mockApiClient.getLeaderboard).toHaveBeenCalled();
      expect(mockLeaderboardPresenter.present).not.toHaveBeenCalled();
    });
  });

  describe('completeDriverOnboarding', () => {
    it('should complete onboarding and transform via presenter', async () => {
      // Arrange
      const input: CompleteOnboardingInputDto = {
        iracingId: '123456',
        displayName: 'John Doe',
      };

      const mockDto: CompleteOnboardingOutputDto = {
        driverId: 'driver-123',
        success: true,
      };

      const mockViewModel: CompleteOnboardingViewModel = {
        driverId: 'driver-123',
        success: true,
      };

      vi.mocked(mockApiClient.completeOnboarding).mockResolvedValue(mockDto);
      vi.mocked(mockOnboardingPresenter.present).mockReturnValue(mockViewModel);

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

      // Assert
      expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input);
      expect(mockOnboardingPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle onboarding failure', async () => {
      // Arrange
      const input: CompleteOnboardingInputDto = {
        iracingId: '123456',
        displayName: 'John Doe',
      };

      const mockDto: CompleteOnboardingOutputDto = {
        driverId: '',
        success: false,
      };

      const mockViewModel: CompleteOnboardingViewModel = {
        driverId: '',
        success: false,
      };

      vi.mocked(mockApiClient.completeOnboarding).mockResolvedValue(mockDto);
      vi.mocked(mockOnboardingPresenter.present).mockReturnValue(mockViewModel);

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

      // Assert
      expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input);
      expect(mockOnboardingPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
      expect(result.success).toBe(false);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const input: CompleteOnboardingInputDto = {
        iracingId: '123456',
        displayName: 'John Doe',
      };
      const error = new Error('Onboarding failed');
      vi.mocked(mockApiClient.completeOnboarding).mockRejectedValue(error);

      // Act & Assert
      await expect(service.completeDriverOnboarding(input)).rejects.toThrow('Onboarding failed');
      expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input);
      expect(mockOnboardingPresenter.present).not.toHaveBeenCalled();
    });
  });

  describe('getCurrentDriver', () => {
    it('should fetch current driver and transform via presenter', async () => {
      // Arrange
      const mockDto: DriverDto = {
        id: 'driver-123',
        name: 'John Doe',
        avatarUrl: 'https://example.com/avatar.jpg',
        iracingId: '123456',
        rating: 2500,
      };

      const mockViewModel: DriverViewModel = {
        id: 'driver-123',
        name: 'John Doe',
        avatarUrl: 'https://example.com/avatar.jpg',
        iracingId: '123456',
        rating: 2500,
      };

      vi.mocked(mockApiClient.getCurrent).mockResolvedValue(mockDto);
      vi.mocked(mockDriverPresenter.present).mockReturnValue(mockViewModel);

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

      // Assert
      expect(mockApiClient.getCurrent).toHaveBeenCalled();
      expect(mockDriverPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should return null when no current driver', async () => {
      // Arrange
      vi.mocked(mockApiClient.getCurrent).mockResolvedValue(null);

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

      // Assert
      expect(mockApiClient.getCurrent).toHaveBeenCalled();
      expect(mockDriverPresenter.present).not.toHaveBeenCalled();
      expect(result).toBeNull();
    });

    it('should handle driver without optional fields', async () => {
      // Arrange
      const mockDto: DriverDto = {
        id: 'driver-123',
        name: 'John Doe',
      };

      const mockViewModel: DriverViewModel = {
        id: 'driver-123',
        name: 'John Doe',
      };

      vi.mocked(mockApiClient.getCurrent).mockResolvedValue(mockDto);
      vi.mocked(mockDriverPresenter.present).mockReturnValue(mockViewModel);

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

      // Assert
      expect(mockApiClient.getCurrent).toHaveBeenCalled();
      expect(mockDriverPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

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

      // Act & Assert
      await expect(service.getCurrentDriver()).rejects.toThrow('Failed to fetch current driver');
      expect(mockApiClient.getCurrent).toHaveBeenCalled();
      expect(mockDriverPresenter.present).not.toHaveBeenCalled();
    });
  });
});