import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RaceService } from './RaceService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceDetailPresenter } from '../../presenters/RaceDetailPresenter';
import type { RaceDetailDto, RacesPageDataDto, RaceStatsDto } from '../../dtos';
import type { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';

describe('RaceService', () => {
  let mockApiClient: RacesApiClient;
  let mockPresenter: RaceDetailPresenter;
  let service: RaceService;

  beforeEach(() => {
    mockApiClient = {
      getDetail: vi.fn(),
      getPageData: vi.fn(),
      getTotal: vi.fn(),
    } as unknown as RacesApiClient;

    mockPresenter = {
      present: vi.fn(),
    } as unknown as RaceDetailPresenter;

    service = new RaceService(mockApiClient, mockPresenter);
  });

  describe('getRaceDetail', () => {
    it('should fetch race detail from API and transform via presenter', async () => {
      // Arrange
      const mockDto: RaceDetailDto = {
        race: {
          id: 'race-1',
          name: 'Test Race',
          scheduledTime: '2025-12-17T20:00:00Z',
          status: 'upcoming',
          trackName: 'Spa-Francorchamps',
          carClasses: ['GT3'],
        },
        league: null,
        entryList: [],
        registration: {
          isRegistered: false,
          canRegister: true,
        },
        userResult: null,
      };

      const mockViewModel: RaceDetailViewModel = {
        race: mockDto.race,
        league: mockDto.league,
        entryList: mockDto.entryList,
        registration: mockDto.registration,
        userResult: mockDto.userResult,
        isRegistered: false,
        canRegister: true,
        raceStatusDisplay: 'Upcoming',
        formattedScheduledTime: expect.any(String),
        entryCount: 0,
        hasResults: false,
        registrationStatusMessage: 'You can register for this race',
      } as unknown as RaceDetailViewModel;

      vi.mocked(mockApiClient.getDetail).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getRaceDetail('race-1', 'driver-1');

      // Assert
      expect(mockApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1');
      expect(mockApiClient.getDetail).toHaveBeenCalledTimes(1);
      expect(mockPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(mockPresenter.present).toHaveBeenCalledTimes(1);
      expect(result).toBe(mockViewModel);
    });

    it('should propagate API client errors', async () => {
      // Arrange
      const error = new Error('API Error: Race not found');
      vi.mocked(mockApiClient.getDetail).mockRejectedValue(error);

      // Act & Assert
      await expect(
        service.getRaceDetail('invalid-race', 'driver-1')
      ).rejects.toThrow('API Error: Race not found');
      
      expect(mockApiClient.getDetail).toHaveBeenCalledWith('invalid-race', 'driver-1');
      expect(mockPresenter.present).not.toHaveBeenCalled();
    });

    it('should propagate presenter errors', async () => {
      // Arrange
      const mockDto: RaceDetailDto = {
        race: null,
        league: null,
        entryList: [],
        registration: {
          isRegistered: false,
          canRegister: false,
        },
        userResult: null,
      };

      const error = new Error('Presenter Error: Invalid DTO structure');
      vi.mocked(mockApiClient.getDetail).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.present).mockImplementation(() => {
        throw error;
      });

      // Act & Assert
      await expect(
        service.getRaceDetail('race-1', 'driver-1')
      ).rejects.toThrow('Presenter Error: Invalid DTO structure');
      
      expect(mockApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1');
      expect(mockPresenter.present).toHaveBeenCalledWith(mockDto);
    });
  });

  describe('getRacesPageData', () => {
    it('should fetch races page data from API', async () => {
      // Arrange
      const mockPageData: RacesPageDataDto = {
        races: [
          {
            id: 'race-1',
            name: 'Test Race 1',
            scheduledTime: '2025-12-17T20:00:00Z',
            trackName: 'Spa-Francorchamps',
          },
          {
            id: 'race-2',
            name: 'Test Race 2',
            scheduledTime: '2025-12-18T20:00:00Z',
            trackName: 'Monza',
          },
        ],
        totalCount: 2,
      };

      vi.mocked(mockApiClient.getPageData).mockResolvedValue(mockPageData);

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

      // Assert
      expect(mockApiClient.getPageData).toHaveBeenCalledTimes(1);
      expect(result).toBe(mockPageData);
    });

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

      // Act & Assert
      await expect(service.getRacesPageData()).rejects.toThrow(
        'API Error: Failed to fetch page data'
      );
      
      expect(mockApiClient.getPageData).toHaveBeenCalledTimes(1);
    });
  });

  describe('getRacesTotal', () => {
    it('should fetch race statistics from API', async () => {
      // Arrange
      const mockStats: RaceStatsDto = {
        total: 42,
        upcoming: 10,
        live: 2,
        finished: 30,
      };

      vi.mocked(mockApiClient.getTotal).mockResolvedValue(mockStats);

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

      // Assert
      expect(mockApiClient.getTotal).toHaveBeenCalledTimes(1);
      expect(result).toBe(mockStats);
    });

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

      // Act & Assert
      await expect(service.getRacesTotal()).rejects.toThrow(
        'API Error: Failed to fetch statistics'
      );
      
      expect(mockApiClient.getTotal).toHaveBeenCalledTimes(1);
    });
  });

  describe('Constructor Dependency Injection', () => {
    it('should require apiClient and raceDetailPresenter', () => {
      // This test verifies the constructor signature
      expect(() => {
        new RaceService(mockApiClient, mockPresenter);
      }).not.toThrow();
    });

    it('should use injected dependencies', async () => {
      // Arrange
      const customApiClient = {
        getDetail: vi.fn().mockResolvedValue({
          race: null,
          league: null,
          entryList: [],
          registration: { isRegistered: false, canRegister: false },
          userResult: null,
        }),
        getPageData: vi.fn(),
        getTotal: vi.fn(),
      } as unknown as RacesApiClient;

      const customPresenter = {
        present: vi.fn().mockReturnValue({} as RaceDetailViewModel),
      } as unknown as RaceDetailPresenter;

      const customService = new RaceService(customApiClient, customPresenter);

      // Act
      await customService.getRaceDetail('race-1', 'driver-1');

      // Assert
      expect(customApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1');
      expect(customPresenter.present).toHaveBeenCalled();
    });
  });
});