import { describe, it, expect, vi, Mock } from 'vitest'; import { Season } from '@core/racing/domain/entities/season/Season'; import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository'; import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import { CreateSeasonForLeagueUseCase, type CreateSeasonForLeagueInput, type CreateSeasonForLeagueResult, } from '@core/racing/application/use-cases/CreateSeasonForLeagueUseCase'; import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO'; import type { UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { Result } from '@core/shared/application/Result'; function createLeagueConfigFormModel(overrides?: Partial): LeagueConfigFormModel { return { basics: { name: 'Test League', visibility: 'ranked', gameId: 'iracing', ...overrides?.basics, }, structure: { mode: 'solo', maxDrivers: 30, ...overrides?.structure, }, championships: { enableDriverChampionship: true, enableTeamChampionship: false, enableNationsChampionship: false, enableTrophyChampionship: false, ...overrides?.championships, }, scoring: { patternId: 'sprint-main-driver', customScoringEnabled: false, ...overrides?.scoring, }, dropPolicy: { strategy: 'bestNResults', n: 3, ...overrides?.dropPolicy, }, timings: { qualifyingMinutes: 10, mainRaceMinutes: 30, sessionCount: 8, seasonStartDate: '2025-01-01', raceStartTime: '20:00', timezoneId: 'UTC', recurrenceStrategy: 'weekly', weekdays: ['Mon'], ...overrides?.timings, }, stewarding: { decisionMode: 'steward_vote', requiredVotes: 3, requireDefense: true, defenseTimeLimit: 24, voteTimeLimit: 24, protestDeadlineHours: 48, stewardingClosesHours: 72, notifyAccusedOnProtest: true, notifyOnVoteRequired: true, ...overrides?.stewarding, }, ...overrides, }; } type CreateSeasonErrorCode = ApplicationErrorCode<'LEAGUE_NOT_FOUND' | 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'>; describe('CreateSeasonForLeagueUseCase', () => { const mockLeagueFindById = vi.fn(); const mockLeagueRepo: ILeagueRepository = { findById: mockLeagueFindById, findAll: vi.fn(), findByOwnerId: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn(), exists: vi.fn(), searchByName: vi.fn(), }; const mockSeasonFindById = vi.fn(); const mockSeasonAdd = vi.fn(); const mockSeasonRepo: ISeasonRepository = { findById: mockSeasonFindById, findByLeagueId: vi.fn(), create: vi.fn(), add: mockSeasonAdd, update: vi.fn(), listByLeague: vi.fn(), listActiveByLeague: vi.fn(), }; let output: { present: Mock } & UseCaseOutputPort; beforeEach(() => { vi.clearAllMocks(); output = { present: vi.fn() } as unknown as typeof output; }); it('creates a planned Season for an existing league with config-derived props', async () => { mockLeagueFindById.mockResolvedValue({ id: 'league-1' }); mockSeasonAdd.mockResolvedValue(undefined); const useCase = new CreateSeasonForLeagueUseCase(mockLeagueRepo, mockSeasonRepo, output); const config = createLeagueConfigFormModel({ basics: { name: 'League With Config', visibility: 'ranked', gameId: 'iracing', }, scoring: { patternId: 'club-default', customScoringEnabled: true, }, dropPolicy: { strategy: 'dropWorstN', n: 2, }, // Intentionally omit seasonStartDate / raceStartTime to avoid schedule derivation, // focusing this test on scoring/drop/stewarding/maxDrivers mapping. timings: { qualifyingMinutes: 10, mainRaceMinutes: 30, sessionCount: 8, }, }); const command: CreateSeasonForLeagueInput = { leagueId: 'league-1', name: 'Season from Config', gameId: 'iracing', config, }; const result: Result = await useCase.execute(command); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); const presented = output.present.mock.calls[0][0] as CreateSeasonForLeagueResult; expect(presented.season).toBeInstanceOf(Season); expect(presented.league.id).toBe('league-1'); }); it('clones configuration from a source season when sourceSeasonId is provided', async () => { const sourceSeason = Season.create({ id: 'source-season', leagueId: 'league-1', gameId: 'iracing', name: 'Source Season', status: 'planned', }).withMaxDrivers(40); mockLeagueFindById.mockResolvedValue({ id: 'league-1' }); mockSeasonFindById.mockResolvedValue(sourceSeason); mockSeasonAdd.mockResolvedValue(undefined); const useCase = new CreateSeasonForLeagueUseCase(mockLeagueRepo, mockSeasonRepo, output); const command: CreateSeasonForLeagueInput = { leagueId: 'league-1', name: 'Cloned Season', gameId: 'iracing', sourceSeasonId: 'source-season', }; const result: Result = await useCase.execute(command); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); const presented = output.present.mock.calls[0][0] as CreateSeasonForLeagueResult; expect(presented.season.maxDrivers).toBe(40); }); it('returns error when league not found and does not call output', async () => { mockLeagueFindById.mockResolvedValue(null); const useCase = new CreateSeasonForLeagueUseCase(mockLeagueRepo, mockSeasonRepo, output); const command: CreateSeasonForLeagueInput = { leagueId: 'missing-league', name: 'Any', gameId: 'iracing', }; const result: Result = await useCase.execute(command); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('LEAGUE_NOT_FOUND'); expect(error.details?.message).toBe('League not found: missing-league'); expect(output.present).not.toHaveBeenCalled(); }); it('returns validation error when source season is missing and does not call output', async () => { mockLeagueFindById.mockResolvedValue({ id: 'league-1' }); mockSeasonFindById.mockResolvedValue(undefined); const useCase = new CreateSeasonForLeagueUseCase(mockLeagueRepo, mockSeasonRepo, output); const command: CreateSeasonForLeagueInput = { leagueId: 'league-1', name: 'Cloned Season', gameId: 'iracing', sourceSeasonId: 'missing-source', }; const result: Result = await useCase.execute(command); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('VALIDATION_ERROR'); expect(error.details?.message).toBe('Source Season not found: missing-source'); expect(output.present).not.toHaveBeenCalled(); }); })