import { describe, it, beforeEach, expect, vi } from 'vitest'; import { Session } from '@core/racing/domain/entities/Session'; import { RaceEvent } from '@core/racing/domain/entities/RaceEvent'; import { SessionType } from '@core/racing/domain/value-objects/SessionType'; import { MainRaceCompletedEvent } from '@core/racing/domain/events/MainRaceCompleted'; import { RaceEventStewardingClosedEvent } from '@core/racing/domain/events/RaceEventStewardingClosed'; import { SendPerformanceSummaryUseCase } from '@core/racing/application/use-cases/SendPerformanceSummaryUseCase'; import { SendFinalResultsUseCase } from '@core/racing/application/use-cases/SendFinalResultsUseCase'; import { CloseRaceEventStewardingUseCase } from '@core/racing/application/use-cases/CloseRaceEventStewardingUseCase'; import { InMemoryRaceEventRepository } from '@core/racing/infrastructure/repositories/InMemoryRaceEventRepository'; import { InMemorySessionRepository } from '@core/racing/infrastructure/repositories/InMemorySessionRepository'; // Mock notification service const mockNotificationService = { sendNotification: vi.fn(), }; // Test data builders const createTestSession = (overrides: Partial<{ id: string; raceEventId: string; sessionType: SessionType; status: 'scheduled' | 'running' | 'completed'; scheduledAt: Date; }> = {}) => { return Session.create({ id: overrides.id ?? 'session-1', raceEventId: overrides.raceEventId ?? 'race-event-1', scheduledAt: overrides.scheduledAt ?? new Date(), track: 'Monza', car: 'F1 Car', sessionType: overrides.sessionType ?? SessionType.main(), status: overrides.status ?? 'scheduled', }); }; const createTestRaceEvent = (overrides: Partial<{ id: string; seasonId: string; leagueId: string; name: string; sessions: Session[]; status: 'scheduled' | 'in_progress' | 'awaiting_stewarding' | 'closed'; stewardingClosesAt: Date; }> = {}) => { const sessions = overrides.sessions ?? [ createTestSession({ id: 'practice-1', sessionType: SessionType.practice() }), createTestSession({ id: 'qualifying-1', sessionType: SessionType.qualifying() }), createTestSession({ id: 'main-1', sessionType: SessionType.main() }), ]; return RaceEvent.create({ id: overrides.id ?? 'race-event-1', seasonId: overrides.seasonId ?? 'season-1', leagueId: overrides.leagueId ?? 'league-1', name: overrides.name ?? 'Monza Grand Prix', sessions, status: overrides.status ?? 'scheduled', stewardingClosesAt: overrides.stewardingClosesAt, }); }; describe('Race Event Performance Summary Notifications', () => { let raceEventRepository: InMemoryRaceEventRepository; let sessionRepository: InMemorySessionRepository; let sendPerformanceSummaryUseCase: SendPerformanceSummaryUseCase; let sendFinalResultsUseCase: SendFinalResultsUseCase; let closeStewardingUseCase: CloseRaceEventStewardingUseCase; beforeEach(() => { raceEventRepository = new InMemoryRaceEventRepository(); sessionRepository = new InMemorySessionRepository(); sendPerformanceSummaryUseCase = new SendPerformanceSummaryUseCase( mockNotificationService as any, raceEventRepository as any, {} as any // Mock result repository ); sendFinalResultsUseCase = new SendFinalResultsUseCase( mockNotificationService as any, raceEventRepository as any, {} as any // Mock result repository ); closeStewardingUseCase = new CloseRaceEventStewardingUseCase( raceEventRepository as any, {} as any // Mock domain event publisher ); vi.clearAllMocks(); }); describe('Performance Summary After Main Race Completion', () => { it('should send performance summary notification when main race completes', async () => { // Given const raceEvent = createTestRaceEvent(); await raceEventRepository.create(raceEvent); const mainRaceCompletedEvent = new MainRaceCompletedEvent({ raceEventId: raceEvent.id, sessionId: 'main-1', leagueId: raceEvent.leagueId, seasonId: raceEvent.seasonId, completedAt: new Date(), driverIds: ['driver-1', 'driver-2'], }); // When await sendPerformanceSummaryUseCase.execute(mainRaceCompletedEvent); // Then expect(mockNotificationService.sendNotification).toHaveBeenCalledTimes(2); expect(mockNotificationService.sendNotification).toHaveBeenCalledWith( expect.objectContaining({ recipientId: 'driver-1', type: 'race_performance_summary', urgency: 'modal', title: expect.stringContaining('Race Complete'), }) ); }); it('should calculate provisional rating changes correctly', async () => { // Given const raceEvent = createTestRaceEvent(); await raceEventRepository.create(raceEvent); // Mock result repository to return position data const mockResultRepository = { findByRaceId: vi.fn().mockResolvedValue([ { driverId: 'driver-1', position: 1, incidents: 0, getPositionChange: () => 0 }, ]), }; const useCase = new SendPerformanceSummaryUseCase( mockNotificationService as any, raceEventRepository as any, mockResultRepository as any ); const event = new MainRaceCompletedEvent({ raceEventId: raceEvent.id, sessionId: 'main-1', leagueId: raceEvent.leagueId, seasonId: raceEvent.seasonId, completedAt: new Date(), driverIds: ['driver-1'], }); // When await useCase.execute(event); // Then expect(mockNotificationService.sendNotification).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ provisionalRatingChange: 25, // P1 with 0 incidents = +25 }), }) ); }); }); describe('Final Results After Stewarding Closes', () => { it('should send final results notification when stewarding closes', async () => { // Given const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding' }); await raceEventRepository.create(raceEvent); const stewardingClosedEvent = new RaceEventStewardingClosedEvent({ raceEventId: raceEvent.id, leagueId: raceEvent.leagueId, seasonId: raceEvent.seasonId, closedAt: new Date(), driverIds: ['driver-1', 'driver-2'], hadPenaltiesApplied: false, }); // When await sendFinalResultsUseCase.execute(stewardingClosedEvent); // Then expect(mockNotificationService.sendNotification).toHaveBeenCalledTimes(2); expect(mockNotificationService.sendNotification).toHaveBeenCalledWith( expect.objectContaining({ recipientId: 'driver-1', type: 'race_final_results', urgency: 'modal', title: expect.stringContaining('Final Results'), }) ); }); }); describe('Stewarding Window Management', () => { it('should close expired stewarding windows', async () => { // Given const pastDate = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding', stewardingClosesAt: pastDate, }); await raceEventRepository.create(raceEvent); // When await closeStewardingUseCase.execute({}); // Then const updatedEvent = await raceEventRepository.findById(raceEvent.id); expect(updatedEvent?.status).toBe('closed'); }); it('should not close unexpired stewarding windows', async () => { // Given const futureDate = new Date(Date.now() + 25 * 60 * 60 * 1000); // 25 hours from now const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding', stewardingClosesAt: futureDate, }); await raceEventRepository.create(raceEvent); // When await closeStewardingUseCase.execute({}); // Then const updatedEvent = await raceEventRepository.findById(raceEvent.id); expect(updatedEvent?.status).toBe('awaiting_stewarding'); }); }); describe('Race Event Lifecycle', () => { it('should transition from scheduled to in_progress when sessions start', () => { // Given const raceEvent = createTestRaceEvent({ status: 'scheduled' }); // When const startedEvent = raceEvent.start(); // Then expect(startedEvent.status).toBe('in_progress'); }); it('should transition to awaiting_stewarding when main race completes', () => { // Given const raceEvent = createTestRaceEvent({ status: 'in_progress' }); // When const completedEvent = raceEvent.completeMainRace(); // Then expect(completedEvent.status).toBe('awaiting_stewarding'); }); it('should transition to closed when stewarding closes', () => { // Given const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding' }); // When const closedEvent = raceEvent.closeStewarding(); // Then expect(closedEvent.status).toBe('closed'); }); }); describe('Session Type Behavior', () => { it('should identify main race sessions correctly', () => { // Given const mainSession = createTestSession({ sessionType: SessionType.main() }); const practiceSession = createTestSession({ sessionType: SessionType.practice() }); // Then expect(mainSession.countsForPoints()).toBe(true); expect(practiceSession.countsForPoints()).toBe(false); }); it('should identify qualifying sessions correctly', () => { // Given const qualiSession = createTestSession({ sessionType: SessionType.qualifying() }); const mainSession = createTestSession({ sessionType: SessionType.main() }); // Then expect(qualiSession.determinesGrid()).toBe(true); expect(mainSession.determinesGrid()).toBe(false); }); }); });