import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import { SendFinalResultsUseCase, type SendFinalResultsInput, type SendFinalResultsResult, type SendFinalResultsErrorCode, } from './SendFinalResultsUseCase'; import type { NotificationService } from '../../../notifications/application/ports/NotificationService'; import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { Result } from '@core/shared/application/Result'; import type { Logger } from '@core/shared/application/Logger'; const unwrapError = ( result: Result>, ): ApplicationErrorCode => { expect(result.isErr()).toBe(true); return result.unwrapErr(); }; describe('SendFinalResultsUseCase', () => { let notificationService: { sendNotification: Mock }; let raceEventRepository: { findById: Mock }; let resultRepository: { findByRaceId: Mock }; let leagueRepository: { findById: Mock }; let membershipRepository: { getMembership: Mock }; let logger: Logger; let output: UseCaseOutputPort & { present: Mock }; let useCase: SendFinalResultsUseCase; beforeEach(() => { notificationService = { sendNotification: vi.fn() }; raceEventRepository = { findById: vi.fn() }; resultRepository = { findByRaceId: vi.fn() }; leagueRepository = { findById: vi.fn() }; membershipRepository = { getMembership: vi.fn() }; logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; output = { present: vi.fn() } as unknown as UseCaseOutputPort & { present: Mock }; useCase = new SendFinalResultsUseCase( notificationService as unknown as NotificationService, raceEventRepository as unknown as IRaceEventRepository, resultRepository as unknown as IResultRepository, leagueRepository as unknown as ILeagueRepository, membershipRepository as unknown as ILeagueMembershipRepository, logger, output, ); }); const createInput = (overrides: Partial = {}): SendFinalResultsInput => ({ leagueId: 'league-1', raceId: 'race-1', triggeredById: 'user-1', ...overrides, }); it('sends final results notifications to all participating drivers and presents result', async () => { const mockRaceEvent = { id: 'race-1', leagueId: 'league-1', name: 'Test Race', status: 'closed', getMainRaceSession: vi.fn().mockReturnValue({ id: 'session-1' }), }; const mockLeague = { id: 'league-1' }; const mockMembership = { role: 'steward' }; leagueRepository.findById.mockResolvedValue(mockLeague); raceEventRepository.findById.mockResolvedValue(mockRaceEvent); membershipRepository.getMembership.mockResolvedValue(mockMembership); const mockResults = [ { driverId: 'driver-1', position: 1, incidents: 0, getPositionChange: vi.fn().mockReturnValue(2), }, { driverId: 'driver-2', position: 2, incidents: 1, getPositionChange: vi.fn().mockReturnValue(-1), }, ]; resultRepository.findByRaceId.mockResolvedValue(mockResults); const input = createInput(); const result = await useCase.execute(input); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(leagueRepository.findById).toHaveBeenCalledWith('league-1'); expect(raceEventRepository.findById).toHaveBeenCalledWith('race-1'); expect(resultRepository.findByRaceId).toHaveBeenCalledWith('session-1'); expect(notificationService.sendNotification).toHaveBeenCalledTimes(2); expect(output.present).toHaveBeenCalledTimes(1); const presented = output.present.mock.calls[0][0] as SendFinalResultsResult; expect(presented).toEqual({ leagueId: 'league-1', raceId: 'race-1', notificationsSent: 2, }); }); it('returns LEAGUE_NOT_FOUND when league does not exist', async () => { leagueRepository.findById.mockResolvedValue(null); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('LEAGUE_NOT_FOUND'); expect(error.details?.message).toBe('League not found'); expect(output.present).not.toHaveBeenCalled(); }); it('returns RACE_NOT_FOUND when race event does not exist', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue(null); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('RACE_NOT_FOUND'); expect(error.details?.message).toBe('Race event not found'); expect(output.present).not.toHaveBeenCalled(); }); it('returns INSUFFICIENT_PERMISSIONS when user is not steward or higher', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue({ id: 'race-1', leagueId: 'league-1', status: 'closed' }); membershipRepository.getMembership.mockResolvedValue(null); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('INSUFFICIENT_PERMISSIONS'); expect(error.details?.message).toBe('Insufficient permissions to send final results'); expect(output.present).not.toHaveBeenCalled(); }); it('returns RESULTS_NOT_FINAL when race is not closed', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue({ id: 'race-1', leagueId: 'league-1', name: 'Test Race', status: 'in_progress', getMainRaceSession: vi.fn(), }); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('RESULTS_NOT_FINAL'); expect(error.details?.message).toBe('Race results are not in a final state'); expect(output.present).not.toHaveBeenCalled(); }); it('returns RESULTS_NOT_FINAL when main race session is missing', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue({ id: 'race-1', leagueId: 'league-1', name: 'Test Race', status: 'closed', getMainRaceSession: vi.fn().mockReturnValue(undefined), }); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('RESULTS_NOT_FINAL'); expect(error.details?.message).toBe('Main race session not found for race event'); expect(output.present).not.toHaveBeenCalled(); }); it('wraps repository errors into REPOSITORY_ERROR and does not present output', async () => { const mockError = new Error('Repository failure'); leagueRepository.findById.mockRejectedValue(mockError); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('REPOSITORY_ERROR'); expect(error.details?.message).toBe('Repository failure'); expect(output.present).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled(); }); });