import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest'; import { RequestProtestDefenseUseCase, type RequestProtestDefenseInput, type RequestProtestDefenseResult, type RequestProtestDefenseErrorCode, } from './RequestProtestDefenseUseCase'; import type { IProtestRepository } from '../../domain/repositories/IProtestRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; 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'; describe('RequestProtestDefenseUseCase', () => { let useCase: RequestProtestDefenseUseCase; let protestRepository: { findById: Mock; update: Mock }; let raceRepository: { findById: Mock }; let membershipRepository: { getMembership: Mock }; let logger: Logger; let output: UseCaseOutputPort & { present: Mock }; beforeEach(() => { protestRepository = { findById: vi.fn(), update: vi.fn() }; raceRepository = { 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 RequestProtestDefenseUseCase( protestRepository as unknown as IProtestRepository, raceRepository as unknown as IRaceRepository, membershipRepository as unknown as ILeagueMembershipRepository, logger, output, ); }); const createInput = (overrides: Partial = {}): RequestProtestDefenseInput => ({ protestId: 'protest-1', stewardId: 'steward-1', ...overrides, }); const unwrapError = ( result: Result>, ): ApplicationErrorCode => { expect(result.isErr()).toBe(true); return result.unwrapErr(); }; it('should return protest not found error', async () => { protestRepository.findById.mockResolvedValue(null); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('PROTEST_NOT_FOUND'); expect(error.details?.message).toBe('Protest not found'); expect(output.present).not.toHaveBeenCalled(); }); it('should return race not found error', async () => { const mockProtest = { raceId: 'race-1', accusedDriverId: 'driver-1', id: 'protest-1', canRequestDefense: vi.fn().mockReturnValue(true), }; protestRepository.findById.mockResolvedValue(mockProtest); raceRepository.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 not found'); expect(output.present).not.toHaveBeenCalled(); }); it('should return insufficient permissions error', async () => { const mockProtest = { raceId: 'race-1', accusedDriverId: 'driver-1', id: 'protest-1', canRequestDefense: vi.fn().mockReturnValue(true), }; const mockRace = { leagueId: 'league-1' }; protestRepository.findById.mockResolvedValue(mockProtest); raceRepository.findById.mockResolvedValue(mockRace); 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 request defense'); expect(output.present).not.toHaveBeenCalled(); }); it('should return defense cannot be requested error', async () => { const mockProtest = { raceId: 'race-1', accusedDriverId: 'driver-1', id: 'protest-1', canRequestDefense: vi.fn().mockReturnValue(false), }; const mockRace = { leagueId: 'league-1' }; const mockMembership = { role: 'steward' }; protestRepository.findById.mockResolvedValue(mockProtest); raceRepository.findById.mockResolvedValue(mockRace); membershipRepository.getMembership.mockResolvedValue(mockMembership); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('DEFENSE_CANNOT_BE_REQUESTED'); expect(error.details?.message).toBe('Defense cannot be requested for this protest'); expect(output.present).not.toHaveBeenCalled(); }); it('should request defense successfully', async () => { const updatedProtest = {}; const mockProtest = { raceId: 'race-1', accusedDriverId: 'driver-1', id: 'protest-1', canRequestDefense: vi.fn().mockReturnValue(true), requestDefense: vi.fn().mockReturnValue(updatedProtest), }; const mockRace = { leagueId: 'league-1' }; const mockMembership = { role: 'steward' }; protestRepository.findById.mockResolvedValue(mockProtest); raceRepository.findById.mockResolvedValue(mockRace); membershipRepository.getMembership.mockResolvedValue(mockMembership); protestRepository.update.mockResolvedValue(undefined); const result = await useCase.execute(createInput()); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(protestRepository.update).toHaveBeenCalledWith(updatedProtest); expect(output.present).toHaveBeenCalledTimes(1); const presented = output.present.mock.calls[0][0] as RequestProtestDefenseResult; expect(presented).toEqual({ leagueId: 'league-1', protestId: 'protest-1', accusedDriverId: 'driver-1', status: 'defense_requested', }); }); it('should wrap repository errors into REPOSITORY_ERROR and not present output', async () => { const mockError = new Error('Repository failed'); protestRepository.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 failed'); expect(output.present).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled(); }); });