import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:gametime/application/application.dart'; import 'package:gametime/domain/domain.dart'; void main() { test('Exercise requires at least one active measure', () { expect( () => Exercise( metadata: _metadata('exercise-1'), name: 'Push up', hasTimeMeasure: false, hasRepsMeasure: false, hasScoreMeasure: false, ), throwsA(isA()), ); }); test( 'Exercise use case rejects active measures without default targets', () async { final useCase = _exerciseUseCase(_FakeExerciseRepository()); await expectLater( useCase.create( name: 'Squat', hasTimeMeasure: true, hasRepsMeasure: false, hasScoreMeasure: false, ), throwsA(isA()), ); await expectLater( useCase.create( name: 'Pompes', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, ), throwsA(isA()), ); await expectLater( useCase.create( name: 'Charge', hasTimeMeasure: false, hasRepsMeasure: false, hasScoreMeasure: true, scoreLabel: 'Charge', scoreUnit: 'kg', ), throwsA(isA()), ); }, ); test('Exercise use case rejects non-positive default targets', () async { final useCase = _exerciseUseCase(_FakeExerciseRepository()); await expectLater( useCase.create( name: 'Squat', hasTimeMeasure: true, hasRepsMeasure: false, hasScoreMeasure: false, defaultTargetTimeSeconds: 0, ), throwsA(isA()), ); await expectLater( useCase.create( name: 'Pompes', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: -1, ), throwsA(isA()), ); await expectLater( useCase.create( name: 'Charge', hasTimeMeasure: false, hasRepsMeasure: false, hasScoreMeasure: true, scoreLabel: 'Charge', scoreUnit: 'kg', defaultTargetScore: 0, ), throwsA(isA()), ); }); test( 'Exercise use case accepts stopwatch score without default target', () async { final repository = _FakeExerciseRepository(); final useCase = _exerciseUseCase(repository); final exercise = await useCase.create( name: 'Sprint', hasTimeMeasure: false, hasRepsMeasure: false, hasScoreMeasure: true, scoreInputMode: ScoreInputMode.stopwatch, scoreLabel: 'Temps réalisé', ); expect(exercise.defaultTargetScoreTimeMs, isNull); expect(repository.saved.single.metadata.id, exercise.metadata.id); }, ); test( 'Exercise use case rejects non-positive stopwatch score default target', () async { final useCase = _exerciseUseCase(_FakeExerciseRepository()); await expectLater( useCase.create( name: 'Sprint', hasTimeMeasure: false, hasRepsMeasure: false, hasScoreMeasure: true, scoreInputMode: ScoreInputMode.stopwatch, scoreLabel: 'Temps réalisé', defaultTargetScoreTimeMs: 0, ), throwsA(isA()), ); }, ); test('Exercise update validates default targets too', () async { final repository = _FakeExerciseRepository() ..exercise = Exercise( metadata: _metadata('exercise-1'), name: 'Squat', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 10, ); final useCase = _exerciseUseCase(repository); await expectLater( useCase.update( id: 'exercise-1', name: 'Squat', hasTimeMeasure: true, hasRepsMeasure: false, hasScoreMeasure: false, ), throwsA(isA()), ); }); test('Exercise image gallery rejects a sixth image', () async { final repository = _FakeExerciseRepository() ..exercise = Exercise( metadata: _metadata('exercise-1'), name: 'Squat', imageMediaIds: const [ 'media-1', 'media-2', 'media-3', 'media-4', 'media-5', ], hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 10, ); final useCase = _exerciseUseCase(repository); await expectLater( useCase.addImage(exerciseId: 'exercise-1', mediaAssetId: 'media-6'), throwsA(isA()), ); }); test('Exercise image gallery reorders images', () async { final repository = _FakeExerciseRepository() ..exercise = Exercise( metadata: _metadata('exercise-1'), name: 'Squat', imageMediaIds: const ['media-1', 'media-2', 'media-3'], hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 10, ); final useCase = _exerciseUseCase(repository); final updated = await useCase.reorderImages( exerciseId: 'exercise-1', orderedMediaIds: const ['media-3', 'media-1', 'media-2'], ); expect(updated.imageMediaIds, ['media-3', 'media-1', 'media-2']); expect(repository.saved.single.imageMediaIds, updated.imageMediaIds); }); test('Exercise image gallery removes an image', () async { final repository = _FakeExerciseRepository() ..exercise = Exercise( metadata: _metadata('exercise-1'), name: 'Squat', imageMediaIds: const ['media-1', 'media-2', 'media-3'], hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 10, ); final useCase = _exerciseUseCase(repository); final updated = await useCase.removeImage( exerciseId: 'exercise-1', mediaAssetId: 'media-2', ); expect(updated.imageMediaIds, ['media-1', 'media-3']); expect(updated.imageMediaId, 'media-1'); }); test('delete exercise removes it from referencing programs', () async { final exerciseRepository = _FakeExerciseRepository() ..exercise = Exercise( metadata: _metadata('exercise-1'), name: 'Squat', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 10, ); final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Jambes', defaultRestSeconds: 60, exercises: [ _programExercise( id: 'program-exercise-1', programId: 'program-1', sourceExerciseId: 'exercise-1', position: 0, ), _programExercise( id: 'program-exercise-2', programId: 'program-1', sourceExerciseId: 'exercise-2', position: 1, ), ], ), ); final useCase = _exerciseUseCase( exerciseRepository, programRepository: programRepository, ); final deleted = await useCase.delete('exercise-1'); expect(deleted.metadata.deletedAt, isNotNull); expect(exerciseRepository.listActive(), completion(isEmpty)); expect(programRepository.programs.single.exercises, hasLength(1)); expect( programRepository.programs.single.exercises.single.sourceExerciseId, 'exercise-2', ); expect(programRepository.programs.single.exercises.single.position, 0); }); test( 'delete program removes it from referencing workout templates', () async { final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Jambes', defaultRestSeconds: 60, ), ); final templateRepository = _FakeWorkoutTemplateRepository() ..templates.add( WorkoutTemplate( metadata: _metadata('template-1'), name: 'Séance', programs: [ _templateProgram( id: 'template-program-1', workoutTemplateId: 'template-1', sourceProgramId: 'program-1', position: 0, ), _templateProgram( id: 'template-program-2', workoutTemplateId: 'template-1', sourceProgramId: 'program-2', position: 1, ), ], overrides: [ WorkoutTemplateExerciseOverride( metadata: _metadata('override-1'), workoutTemplateProgramId: 'template-program-1', snapshotProgramExerciseId: 'exercise-snapshot-1', targetRepsOverride: 8, ), ], ), ); final useCase = ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); final deleted = await useCase.delete('program-1'); expect(deleted.metadata.deletedAt, isNotNull); expect(programRepository.listActive(), completion(isEmpty)); final template = templateRepository.templates.single; expect(template.programs, hasLength(1)); expect(template.programs.single.sourceProgramId, 'program-2'); expect(template.programs.single.position, 0); expect(template.overrides, isEmpty); }, ); test('delete workout template hides it from active list', () async { final templateRepository = _FakeWorkoutTemplateRepository() ..templates.add( WorkoutTemplate(metadata: _metadata('template-1'), name: 'Séance'), ); final useCase = WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); final deleted = await useCase.delete('template-1'); expect(deleted.metadata.deletedAt, isNotNull); expect(templateRepository.listActive(), completion(isEmpty)); }); test('history stays unchanged after deleting its source template', () async { final templateRepository = _FakeWorkoutTemplateRepository() ..templates.add( WorkoutTemplate(metadata: _metadata('template-1'), name: 'Séance'), ); final history = WorkoutHistory( metadata: _metadata('history-1'), sourceWorkoutTemplateId: 'template-1', nameSnapshot: 'Séance historique', startedAt: DateTime.utc(2026, 7, 17, 10), endedAt: DateTime.utc(2026, 7, 17, 11), totalActiveMs: 3600000, completed: true, historySnapshotJson: '{"name":"Séance historique"}', ); final useCase = WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); await useCase.delete('template-1'); expect(history.sourceWorkoutTemplateId, 'template-1'); expect(history.nameSnapshot, 'Séance historique'); expect(history.historySnapshotJson, '{"name":"Séance historique"}'); }); test('Workout template use case rejects active measure overrides', () async { final useCase = WorkoutTemplateUseCases( templateRepository: _FakeWorkoutTemplateRepository(), programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); await expectLater( useCase.overrideExerciseTargets( workoutTemplateProgramId: 'template-program-1', snapshotProgramExerciseId: 'exercise-snapshot-1', attemptedMeasureOverride: {WorkoutMeasure.reps}, ), throwsA(isA()), ); }); test( 'Workout template use case rejects target overrides for inactive measures', () async { final useCase = WorkoutTemplateUseCases( templateRepository: _FakeWorkoutTemplateRepository(), programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); await expectLater( useCase.saveConfigured( name: 'Séance tir', programs: [ WorkoutTemplateProgramConfig( clientKey: 'program-1', programNameSnapshot: 'Programme tir', defaultRestSecondsSnapshot: 30, programSnapshotJson: jsonEncode({ 'exercises': [ { 'id': 'exercise-snapshot-1', 'exerciseNameSnapshot': 'Lancers francs', 'setsCount': 3, 'timeEnabled': true, 'repsEnabled': false, 'scoreEnabled': false, 'targetTimeSeconds': 60, 'targetReps': null, 'targetScore': null, }, ], }), ), ], overrides: const [ WorkoutTemplateExerciseOverrideConfig( workoutTemplateProgramClientKey: 'program-1', snapshotProgramExerciseId: 'exercise-snapshot-1', targetRepsOverride: 10, ), ], ), throwsA(isA()), ); }, ); test('Active session elapsed time is based on persisted timestamps', () { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), status: ActiveWorkoutStatus.running, startedAt: clock.now(), lastPersistedAt: clock.now(), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: '{}', ); clock.value = clock.value.add(const Duration(seconds: 30)); final paused = session.pause(clock.now()); expect(paused.elapsedActiveMs, 30000); clock.value = clock.value.add(const Duration(minutes: 5)); final resumed = paused.resume(clock.now()); expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 30000); clock.value = clock.value.add(const Duration(seconds: 15)); expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 45000); }); test('adjustRestSeconds persists adjusted rest duration', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository() ..restStates['rest-1'] = ActiveRestState( metadata: _metadata('rest-1'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now(), ); final useCase = _activeUseCase(repository, clock); final adjusted = await useCase.adjustRestSeconds( restStateId: 'rest-1', deltaSeconds: -75, ); expect(adjusted.adjustedRestSeconds, 0); expect(repository.restStates['rest-1']!.adjustedRestSeconds, 0); }); test('skipRest persists skippedAt timestamp', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository() ..restStates['rest-1'] = ActiveRestState( metadata: _metadata('rest-1'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now(), ); clock.value = clock.value.add(const Duration(seconds: 10)); final useCase = _activeUseCase(repository, clock); final skipped = await useCase.skipRest(restStateId: 'rest-1'); expect(skipped.skippedAt, clock.now()); expect(repository.restStates['rest-1']!.skippedAt, clock.now()); }); test('findActiveRest returns latest non-ended non-skipped rest', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository() ..restStates['ended'] = ActiveRestState( metadata: _metadata('ended'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now(), endedAt: clock.now().add(const Duration(seconds: 60)), ) ..restStates['older-active'] = ActiveRestState( metadata: _metadata('older-active'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 1, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now().add(const Duration(seconds: 30)), ) ..restStates['latest-active'] = ActiveRestState( metadata: _metadata('latest-active'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 2, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now().add(const Duration(seconds: 90)), ) ..restStates['skipped'] = ActiveRestState( metadata: _metadata('skipped'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 3, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now().add(const Duration(seconds: 120)), skippedAt: clock.now().add(const Duration(seconds: 125)), ); final useCase = _activeUseCase(repository, clock); final active = await useCase.findActiveRest(sessionId: 'session-1'); expect(active?.metadata.id, 'latest-active'); }); test('upsertSetResultAtPosition does not move session cursor', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = _sessionWithSnapshot( currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 1, ); final repository = _FakeActiveSessionRepository()..session = session; final useCase = _activeUseCase(repository, clock); await useCase.upsertSetResultAtPosition( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, status: SetResultStatus.completed, actualReps: 12, ); expect(repository.session?.currentProgramIndex, 0); expect(repository.session?.currentExerciseIndex, 0); expect(repository.session?.currentSetIndex, 1); expect(repository.results.single.actualReps, 12); }); test( 'upsertSetResultAtPosition preserves existing id and createdAt', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = _sessionWithSnapshot( currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 1, ); final existingCreatedAt = DateTime.utc(2026, 7, 17, 10); final existing = ActiveSetResult( metadata: EntityMetadata( id: 'result-1', createdAt: existingCreatedAt, updatedAt: existingCreatedAt, originDeviceId: 'device-1', ), activeWorkoutSessionId: session.metadata.id, programSnapshotId: 'program-snapshot-1', exerciseSnapshotId: 'exercise-snapshot-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, actualReps: 8, ); final repository = _FakeActiveSessionRepository() ..session = session ..results.add(existing); final useCase = _activeUseCase(repository, clock); final updated = await useCase.upsertSetResultAtPosition( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, status: SetResultStatus.completed, actualReps: 12, ); expect(updated.metadata.id, 'result-1'); expect(updated.metadata.createdAt, existingCreatedAt); expect(updated.actualReps, 12); }, ); test( 'listSetResults returns pending, skipped and completed statuses', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = _sessionWithSnapshot( currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 2, setsCount: 3, ); final repository = _FakeActiveSessionRepository() ..session = session ..results.addAll([ ActiveSetResult( metadata: _metadata('completed'), activeWorkoutSessionId: session.metadata.id, programSnapshotId: 'program-snapshot-1', exerciseSnapshotId: 'exercise-snapshot-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, actualReps: 10, status: SetResultStatus.completed, ), ActiveSetResult( metadata: _metadata('skipped'), activeWorkoutSessionId: session.metadata.id, programSnapshotId: 'program-snapshot-1', exerciseSnapshotId: 'exercise-snapshot-1', programIndex: 0, exerciseIndex: 0, setIndex: 1, status: SetResultStatus.skipped, ), ]); final useCase = _activeUseCase(repository, clock); final states = await useCase.listSetResults(session.metadata.id); expect(states.map((state) => state.status), [ SetPositionStatus.completed, SetPositionStatus.skipped, SetPositionStatus.pending, ]); }, ); test( 'score stopwatch start stop resume accumulates elapsed duration', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = _sessionWithSnapshot( currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, ); final repository = _FakeActiveSessionRepository()..session = session; final useCase = _activeUseCase(repository, clock); await useCase.startScoreStopwatch( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, ); clock.value = clock.value.add(const Duration(seconds: 5)); final stopped = await useCase.stopScoreStopwatch( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, ); clock.value = clock.value.add(const Duration(seconds: 10)); await useCase.resumeScoreStopwatch( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, ); clock.value = clock.value.add(const Duration(seconds: 3)); final stoppedAgain = await useCase.stopScoreStopwatch( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, ); expect(stopped.accumulatedMs, 5000); expect(stoppedAgain.accumulatedMs, 8000); }, ); test('score stopwatch state survives use case reconstruction', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = _sessionWithSnapshot( currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, ); final repository = _FakeActiveSessionRepository()..session = session; await _activeUseCase(repository, clock).startScoreStopwatch( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, ); clock.value = clock.value.add(const Duration(seconds: 4)); final reconstructed = _activeUseCase(repository, clock); final stopped = await reconstructed.stopScoreStopwatch( sessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, ); expect(stopped.accumulatedMs, 4000); }); test('score result enforces manual xor stopwatch values', () { expect( () => ActiveSetResult( metadata: _metadata('result-1'), activeWorkoutSessionId: 'session-1', programSnapshotId: 'program-1', exerciseSnapshotId: 'exercise-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, actualScore: 12, actualScoreTimeMs: 12000, ), throwsA(isA()), ); expect( () => ActiveSetResult( metadata: _metadata('result-2'), activeWorkoutSessionId: 'session-1', programSnapshotId: 'program-1', exerciseSnapshotId: 'exercise-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, actualScore: 12, scoreInputModeSnapshot: ScoreInputMode.stopwatch, ), throwsA(isA()), ); expect( () => ActiveSetResult( metadata: _metadata('result-3'), activeWorkoutSessionId: 'session-1', programSnapshotId: 'program-1', exerciseSnapshotId: 'exercise-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, actualScoreTimeMs: 12000, scoreInputModeSnapshot: ScoreInputMode.manual, ), throwsA(isA()), ); }); } EntityMetadata _metadata(String id) { return EntityMetadata( id: id, createdAt: DateTime.utc(2026, 7, 17), updatedAt: DateTime.utc(2026, 7, 17), originDeviceId: 'device-1', ); } final class _FakeClock implements Clock { _FakeClock(this.value); DateTime value; @override DateTime now() => value; } final class _FakeIds implements IdGenerator { var _next = 0; @override String newId() { _next += 1; return 'id-$_next'; } } ExerciseUseCases _exerciseUseCase( _FakeExerciseRepository repository, { _FakeProgramRepository? programRepository, }) { return ExerciseUseCases( repository: repository, programRepository: programRepository ?? _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); } ProgramExercise _programExercise({ required String id, required String programId, required String sourceExerciseId, required int position, }) { return ProgramExercise( metadata: _metadata(id), programId: programId, sourceExerciseId: sourceExerciseId, position: position, exerciseNameSnapshot: 'Exercise $id', availableTimeSnapshot: false, availableRepsSnapshot: true, availableScoreSnapshot: false, setsCount: 3, timeEnabled: false, repsEnabled: true, scoreEnabled: false, targetReps: 10, ); } WorkoutTemplateProgram _templateProgram({ required String id, required String workoutTemplateId, required String sourceProgramId, required int position, }) { return WorkoutTemplateProgram( metadata: _metadata(id), workoutTemplateId: workoutTemplateId, sourceProgramId: sourceProgramId, position: position, programNameSnapshot: 'Program $id', defaultRestSecondsSnapshot: 60, programSnapshotJson: '{"exercises":[]}', ); } final class _FakeExerciseRepository implements ExerciseRepository { Exercise? exercise; final saved = []; @override Future findById(String id) async { return exercise?.metadata.id == id ? exercise : null; } @override Future isReferencedByProgram(String id) async => false; @override Future> listActive() async => exercise == null || exercise!.metadata.deletedAt != null ? const [] : [exercise!]; @override Future save(Exercise exercise) async { this.exercise = exercise; saved.add(exercise); } } final class _FakeWorkoutTemplateRepository implements WorkoutTemplateRepository { final templates = []; @override Future findById(String id) async { for (final template in templates) { if (template.metadata.id == id && template.metadata.deletedAt == null) { return template; } } return null; } @override Future> listActive() async { return templates .where((template) => template.metadata.deletedAt == null) .toList(); } @override Future replaceComposition( WorkoutTemplate template, DateTime deletedAt, ) async { await save(template); } @override Future save(WorkoutTemplate template) async { templates.removeWhere((saved) => saved.metadata.id == template.metadata.id); templates.add(template); } @override Future saveOverride(WorkoutTemplateExerciseOverride override) async {} @override Future saveProgram(WorkoutTemplateProgram program) async {} } final class _FakeProgramRepository implements ProgramRepository { final programs = []; @override Future findById(String id) async { for (final program in programs) { if (program.metadata.id == id && program.metadata.deletedAt == null) { return program; } } return null; } @override Future> listActive() async { return programs .where((program) => program.metadata.deletedAt == null) .toList(); } @override Future replaceExercises(Program program, DateTime deletedAt) async { await save(program); } @override Future save(Program program) async { programs.removeWhere((saved) => saved.metadata.id == program.metadata.id); programs.add(program); } @override Future saveExercise(ProgramExercise exercise) async { final index = programs.indexWhere( (program) => program.metadata.id == exercise.programId, ); if (index == -1) { return; } final program = programs[index]; programs[index] = program.copyWith( exercises: [ ...program.exercises.where( (saved) => saved.metadata.id != exercise.metadata.id, ), exercise, ], ); } } ActiveWorkoutSessionUseCases _activeUseCase( _FakeActiveSessionRepository repository, _FakeClock clock, ) { return ActiveWorkoutSessionUseCases( sessionRepository: repository, templateRepository: _FakeWorkoutTemplateRepository(), clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ); } final class _FakeActiveSessionRepository implements ActiveSessionRepository { ActiveWorkoutSession? session; final results = []; final restStates = {}; final scoreStopwatchStates = {}; @override Future findById(String id) async { return session?.metadata.id == id ? session : null; } @override Future findOpen() async => session; @override Future findRestStateById(String id) async => restStates[id]; @override Future findScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return scoreStopwatchStates.values .where( (state) => state.activeWorkoutSessionId == sessionId && state.programIndex == programIndex && state.exerciseIndex == exerciseIndex && state.setIndex == setIndex && state.metadata.deletedAt == null, ) .firstOrNull; } @override Future> listRestStates(String sessionId) async { return restStates.values .where((rest) => rest.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listScoreStopwatchStates( String sessionId, ) async { return scoreStopwatchStates.values .where( (state) => state.activeWorkoutSessionId == sessionId && state.metadata.deletedAt == null, ) .toList(); } @override Future> listSetResults(String sessionId) async { return results .where((result) => result.activeWorkoutSessionId == sessionId) .toList(); } @override Future save(ActiveWorkoutSession session) async { this.session = session; } @override Future saveRestState(ActiveRestState restState) async { restStates[restState.metadata.id] = restState; } @override Future saveScoreStopwatchState(ActiveScoreStopwatchState state) async { scoreStopwatchStates[state.metadata.id] = state; } @override Future deleteScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }) async { final state = await findScoreStopwatchState( sessionId: sessionId, programIndex: programIndex, exerciseIndex: exerciseIndex, setIndex: setIndex, ); if (state == null) { return; } scoreStopwatchStates.remove(state.metadata.id); } @override Future saveSetResult(ActiveSetResult result) async { results.removeWhere( (existing) => existing.activeWorkoutSessionId == result.activeWorkoutSessionId && existing.programIndex == result.programIndex && existing.exerciseIndex == result.exerciseIndex && existing.setIndex == result.setIndex, ); results.add(result); } } ActiveWorkoutSession _sessionWithSnapshot({ required int currentProgramIndex, required int currentExerciseIndex, required int currentSetIndex, int setsCount = 2, }) { return ActiveWorkoutSession( metadata: _metadata('session-1'), status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: currentProgramIndex, currentExerciseIndex: currentExerciseIndex, currentSetIndex: currentSetIndex, resolvedTemplateSnapshotJson: jsonEncode({ 'programs': [ { 'id': 'program-snapshot-1', 'programNameSnapshot': 'Programme', 'programSnapshotJson': jsonEncode({ 'exercises': [ { 'id': 'exercise-snapshot-1', 'exerciseNameSnapshot': 'Squat', 'setsCount': setsCount, 'timeEnabled': false, 'repsEnabled': true, 'scoreEnabled': false, 'targetReps': 10, }, ], }), }, ], }), ); }