import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RaceResultsService } from './RaceResultsService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceResultsDetailPresenter } from '../../presenters/RaceResultsDetailPresenter';
import { RaceWithSOFPresenter } from '../../presenters/RaceWithSOFPresenter';
import { ImportRaceResultsPresenter } from '../../presenters/ImportRaceResultsPresenter';
import type { RaceResultsDetailDto, RaceWithSOFDto, ImportRaceResultsSummaryDto } from '../../dtos';
import type { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel';
import type { RaceWithSOFViewModel } from '../../presenters/RaceWithSOFPresenter';
import type { ImportRaceResultsSummaryViewModel } from '../../presenters/ImportRaceResultsPresenter';

describe('RaceResultsService', () => {
  let service: RaceResultsService;
  let mockApiClient: RacesApiClient;
  let mockResultsDetailPresenter: RaceResultsDetailPresenter;
  let mockSOFPresenter: RaceWithSOFPresenter;
  let mockImportPresenter: ImportRaceResultsPresenter;

  beforeEach(() => {
    mockApiClient = {
      getResultsDetail: vi.fn(),
      getWithSOF: vi.fn(),
      importResults: vi.fn(),
    } as unknown as RacesApiClient;

    mockResultsDetailPresenter = {
      present: vi.fn(),
    } as unknown as RaceResultsDetailPresenter;

    mockSOFPresenter = {
      present: vi.fn(),
    } as unknown as RaceWithSOFPresenter;

    mockImportPresenter = {
      present: vi.fn(),
    } as unknown as ImportRaceResultsPresenter;

    service = new RaceResultsService(
      mockApiClient,
      mockResultsDetailPresenter,
      mockSOFPresenter,
      mockImportPresenter
    );
  });

  describe('getResultsDetail', () => {
    it('should fetch race results detail and transform via presenter', async () => {
      // Arrange
      const raceId = 'race-123';
      const currentUserId = 'user-456';
      const mockDto: Partial<RaceResultsDetailDto> = {
        raceId,
        results: [],
      };
      const mockViewModel: Partial<RaceResultsDetailViewModel> = {
        raceId,
        results: [],
      };

      vi.mocked(mockApiClient.getResultsDetail).mockResolvedValue(mockDto as RaceResultsDetailDto);
      vi.mocked(mockResultsDetailPresenter.present).mockReturnValue(mockViewModel as RaceResultsDetailViewModel);

      // Act
      const result = await service.getResultsDetail(raceId, currentUserId);

      // Assert
      expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId);
      expect(mockResultsDetailPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId);
      expect(result).toEqual(mockViewModel);
    });

    it('should fetch race results detail without currentUserId', async () => {
      // Arrange
      const raceId = 'race-123';
      const mockDto: Partial<RaceResultsDetailDto> = {
        raceId,
        results: [],
      };
      const mockViewModel: Partial<RaceResultsDetailViewModel> = {
        raceId,
        results: [],
      };

      vi.mocked(mockApiClient.getResultsDetail).mockResolvedValue(mockDto as RaceResultsDetailDto);
      vi.mocked(mockResultsDetailPresenter.present).mockReturnValue(mockViewModel as RaceResultsDetailViewModel);

      // Act
      const result = await service.getResultsDetail(raceId);

      // Assert
      expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId);
      expect(mockResultsDetailPresenter.present).toHaveBeenCalledWith(mockDto, undefined);
      expect(result).toEqual(mockViewModel);
    });

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

      // Act & Assert
      await expect(service.getResultsDetail(raceId)).rejects.toThrow('API Error');
      expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId);
      expect(mockResultsDetailPresenter.present).not.toHaveBeenCalled();
    });
  });

  describe('getWithSOF', () => {
    it('should fetch race with SOF and transform via presenter', async () => {
      // Arrange
      const raceId = 'race-123';
      const mockDto: RaceWithSOFDto = {
        id: raceId,
        track: 'Spa-Francorchamps',
        strengthOfField: 2500,
      };
      const mockViewModel: RaceWithSOFViewModel = {
        id: raceId,
        track: 'Spa-Francorchamps',
        strengthOfField: 2500,
      };

      vi.mocked(mockApiClient.getWithSOF).mockResolvedValue(mockDto);
      vi.mocked(mockSOFPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getWithSOF(raceId);

      // Assert
      expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId);
      expect(mockSOFPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle null strengthOfField', async () => {
      // Arrange
      const raceId = 'race-123';
      const mockDto: RaceWithSOFDto = {
        id: raceId,
        track: 'Spa-Francorchamps',
        strengthOfField: null,
      };
      const mockViewModel: RaceWithSOFViewModel = {
        id: raceId,
        track: 'Spa-Francorchamps',
        strengthOfField: null,
      };

      vi.mocked(mockApiClient.getWithSOF).mockResolvedValue(mockDto);
      vi.mocked(mockSOFPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getWithSOF(raceId);

      // Assert
      expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId);
      expect(mockSOFPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const raceId = 'race-123';
      const error = new Error('SOF calculation failed');
      vi.mocked(mockApiClient.getWithSOF).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getWithSOF(raceId)).rejects.toThrow('SOF calculation failed');
      expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId);
      expect(mockSOFPresenter.present).not.toHaveBeenCalled();
    });
  });

  describe('importResults', () => {
    it('should import race results and transform via presenter', async () => {
      // Arrange
      const raceId = 'race-123';
      const input = {
        sessionId: 'session-456',
        results: [
          { position: 1, driverId: 'driver-1', finishTime: 120000 },
          { position: 2, driverId: 'driver-2', finishTime: 121000 },
        ],
      };
      const mockDto: ImportRaceResultsSummaryDto = {
        success: true,
        raceId,
        driversProcessed: 2,
        resultsRecorded: 2,
      };
      const mockViewModel: ImportRaceResultsSummaryViewModel = {
        success: true,
        raceId,
        driversProcessed: 2,
        resultsRecorded: 2,
      };

      vi.mocked(mockApiClient.importResults).mockResolvedValue(mockDto);
      vi.mocked(mockImportPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.importResults(raceId, input);

      // Assert
      expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input);
      expect(mockImportPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle import with errors', async () => {
      // Arrange
      const raceId = 'race-123';
      const input = { sessionId: 'session-456', results: [] };
      const mockDto: ImportRaceResultsSummaryDto = {
        success: false,
        raceId,
        driversProcessed: 5,
        resultsRecorded: 3,
        errors: ['Driver not found: driver-99', 'Invalid time for driver-88'],
      };
      const mockViewModel: ImportRaceResultsSummaryViewModel = {
        success: false,
        raceId,
        driversProcessed: 5,
        resultsRecorded: 3,
        errors: ['Driver not found: driver-99', 'Invalid time for driver-88'],
      };

      vi.mocked(mockApiClient.importResults).mockResolvedValue(mockDto);
      vi.mocked(mockImportPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.importResults(raceId, input);

      // Assert
      expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input);
      expect(mockImportPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
      expect(result.errors).toHaveLength(2);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const raceId = 'race-123';
      const input = { sessionId: 'session-456', results: [] };
      const error = new Error('Import failed');
      vi.mocked(mockApiClient.importResults).mockRejectedValue(error);

      // Act & Assert
      await expect(service.importResults(raceId, input)).rejects.toThrow('Import failed');
      expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input);
      expect(mockImportPresenter.present).not.toHaveBeenCalled();
    });
  });
});