import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:gametime/application/application.dart'; import 'package:gametime/domain/domain.dart'; import 'package:gametime/presentation/presentation.dart'; void main() { testWidgets('la liste affiche le badge exemple des séances seedées', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(isExample: true), ]); final activeRepository = _FakeActiveSessionRepository(); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); expect(find.text('Séance jambes'), findsOneWidget); expect(find.text('Exemple'), findsOneWidget); expect(find.byTooltip('Actions'), findsOneWidget); expect(find.text('Lancer'), findsOneWidget); }); testWidgets('la liste séances vide conserve le message dédié', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository(const []); final activeRepository = _FakeActiveSessionRepository(); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); expect(find.text('Aucune séance'), findsOneWidget); expect( find.text( 'Crée une séance-modèle pour lancer ton entraînement plus vite.', ), findsOneWidget, ); }); testWidgets('une séance sans programme ne peut pas être enregistrée', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository(const []); final programRepository = _FakeProgramRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateFormScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), ), ), ); expect( find.text('Ajoute au moins un programme pour enregistrer cette séance.'), findsOneWidget, ); expect( tester .widget( find.widgetWithText(FilledButton, 'Enregistrer'), ) .onPressed, isNull, ); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Vide'); await tester.tap(find.text('Enregistrer')); await tester.pump(); expect(templateRepository.saved, isEmpty); }); testWidgets('le lancement est bloqué si aucun exercice ne peut être joué', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ WorkoutTemplate( metadata: _metadata('template-empty'), name: 'Séance vide', programs: [ WorkoutTemplateProgram( metadata: _metadata('template-program-empty'), workoutTemplateId: 'template-empty', position: 0, programNameSnapshot: 'Programme vide', defaultRestSecondsSnapshot: 60, programSnapshotJson: jsonEncode({'exercises': const []}), ), ], ), ]); final activeRepository = _FakeActiveSessionRepository(); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.text('Lancer')); await tester.pump(); expect( find.text( 'Cette séance ne contient aucun exercice. Ajoute un programme avec ' 'au moins un exercice avant de la lancer.', ), findsOneWidget, ); expect(activeRepository.session, isNull); }); testWidgets('filtre les séances par tag et peut effacer les filtres', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(tags: const ['match', 'intense']), WorkoutTemplate( metadata: _metadata('template-2'), name: 'Séance routine', tags: const ['routine'], ), ]); final activeRepository = _FakeActiveSessionRepository(); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.widgetWithText(FilterChip, 'match')); await tester.pump(); expect(find.text('Séance jambes'), findsOneWidget); expect(find.text('Séance routine'), findsNothing); expect(find.text('Effacer les filtres'), findsOneWidget); await tester.tap(find.text('Effacer les filtres')); await tester.pump(); expect(find.text('Séance jambes'), findsOneWidget); expect(find.text('Séance routine'), findsOneWidget); }); testWidgets('dupliquer une séance ouvre la copie en édition', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(tags: const ['match']), ]); final activeRepository = _FakeActiveSessionRepository(); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.byTooltip('Actions')); await tester.pumpAndSettle(); await tester.tap(find.text('Dupliquer')); await tester.pumpAndSettle(); expect(find.text('Modifier la séance'), findsOneWidget); expect(find.text('Copie de Séance jambes'), findsOneWidget); expect( find.text('Séance dupliquée. Ajuste la copie avant de la lancer.'), findsOneWidget, ); expect(find.widgetWithText(Chip, 'match'), findsOneWidget); }); testWidgets('modifier une séance exemple conserve sa provenance', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(isExample: true), ]); final programRepository = _FakeProgramRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateFormScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), template: templateRepository.templates.single, ), ), ); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Editée'); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); expect(templateRepository.templates.single.name, 'Editée'); expect(templateRepository.templates.single.isExample, isTrue); }); testWidgets( 'la vue programme de séance ne permet pas de changer les mesures actives', (tester) async { final program = WorkoutTemplateProgramDraft( clientKey: 'program-1', programNameSnapshot: 'Jambes', defaultRestSecondsSnapshot: 60, programSnapshotJson: '{}', exercises: [ WorkoutTemplateExerciseDraft( snapshotExerciseId: 'exercise-1', exerciseNameSnapshot: 'Squat', setsCount: 3, timeEnabled: true, repsEnabled: true, scoreEnabled: false, targetTimeSeconds: 45, targetReps: 10, ), ], ); await tester.pumpWidget( MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)), ); expect(find.byType(CheckboxListTile), findsNothing); expect(find.text('Mesures à suivre'), findsNothing); await tester.enterText(find.byType(TextFormField).first, '5'); await tester.pump(); expect(program.exercises.single.setsCountOverride, 5); expect(find.text('5 séries · Temps + Répétitions'), findsOneWidget); }, ); testWidgets( 'personnaliser l’enchaînement automatique transmet l’override séance', (tester) async { final program = WorkoutTemplateProgramDraft( clientKey: 'program-1', programNameSnapshot: 'Circuit', defaultRestSecondsSnapshot: 60, programSnapshotJson: '{}', exercises: [ WorkoutTemplateExerciseDraft( snapshotExerciseId: 'exercise-1', exerciseNameSnapshot: 'Navettes', setsCount: 3, timeEnabled: true, repsEnabled: false, scoreEnabled: false, exerciseStepsSnapshot: [ _step( id: 'step-1', position: 0, name: 'Course', type: ExerciseStepType.time, ), _step( id: 'step-2', position: 1, name: 'Retour', type: ExerciseStepType.time, ), ], autoStartNextTimedStepSnapshot: true, targetTimeSeconds: 30, ), ], ); await tester.binding.setSurfaceSize(const Size(400, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)), ); await tester.tap( find.widgetWithText( SwitchListTile, 'Enchaîner automatiquement les chronos consécutifs', ), ); await tester.pump(); expect( find.textContaining('Personnalisé pour cette séance'), findsOneWidget, ); final override = program.toOverrides().single; expect(override.autoStartNextTimedStepOverride, isFalse); }, ); testWidgets( 'revenir au réglage du programme remet l’override séance à null', (tester) async { final program = WorkoutTemplateProgramDraft( clientKey: 'program-1', programNameSnapshot: 'Circuit', defaultRestSecondsSnapshot: 60, programSnapshotJson: '{}', exercises: [ WorkoutTemplateExerciseDraft( snapshotExerciseId: 'exercise-1', exerciseNameSnapshot: 'Navettes', setsCount: 3, timeEnabled: true, repsEnabled: false, scoreEnabled: false, exerciseStepsSnapshot: [ _step( id: 'step-1', position: 0, name: 'Course', type: ExerciseStepType.time, ), ], autoStartNextTimedStepSnapshot: false, autoStartNextTimedStepOverride: true, targetTimeSeconds: 30, ), ], ); await tester.binding.setSurfaceSize(const Size(400, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)), ); expect( find.textContaining('Personnalisé pour cette séance'), findsOneWidget, ); await tester.tap(find.text('Revenir au réglage du programme')); await tester.pump(); expect(find.textContaining('Réglage du programme'), findsOneWidget); expect(program.toOverrides(), isEmpty); }, ); testWidgets( 'lancer une séance alors qu’une autre est en cours affiche un message', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(), ]); final activeRepository = _FakeActiveSessionRepository() ..session = _activeSession(); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.text('Lancer')); await tester.pump(); await tester.pump(); expect( find.textContaining('Une séance est déjà en cours.'), findsOneWidget, ); expect(activeRepository.saveCount, 0); }, ); testWidgets('une session legacy invalide ne bloque pas le lancement', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(), ]); final activeRepository = _FakeActiveSessionRepository() ..session = ActiveWorkoutSession( metadata: _metadata('legacy-session'), sourceWorkoutTemplateId: 'template-legacy', status: ActiveWorkoutStatus.paused, startedAt: DateTime.utc(2026, 7, 17, 9), lastPersistedAt: DateTime.utc(2026, 7, 17, 9), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: jsonEncode({ 'name': 'Séance vide', 'programs': [ { 'id': 'template-program-empty', 'programNameSnapshot': 'Programme vide', 'programSnapshotJson': jsonEncode({'exercises': const []}), }, ], 'overrides': const [], }), ); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.text('Lancer')); await tester.pumpAndSettle(); expect(find.textContaining('Une séance est déjà en cours.'), findsNothing); expect(find.byType(WorkoutExecutionScreen), findsOneWidget); expect(activeRepository.session?.sourceWorkoutTemplateId, 'template-1'); }); testWidgets( 'supprimer une séance demande confirmation puis la retire de la liste', (tester) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(), ]); final activeRepository = _FakeActiveSessionRepository(); final programRepository = _FakeProgramRepository(); final historyRepository = _FakeWorkoutHistoryRepository(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateListScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), activeUseCases: ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), closeUseCase: CloseWorkoutSessionUseCase( sessionRepository: activeRepository, historyRepository: historyRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), historyUseCases: WorkoutHistoryUseCases( repository: historyRepository, clock: clock, ), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.byTooltip('Actions')); await tester.pumpAndSettle(); await tester.tap(find.text('Supprimer')); await tester.pumpAndSettle(); expect(find.text('Supprimer Séance jambes ?'), findsOneWidget); expect(find.text('Cette action est irréversible.'), findsOneWidget); await tester.tap(find.text('Annuler')); await tester.pumpAndSettle(); expect(find.text('Séance jambes'), findsOneWidget); expect(templateRepository.saved, isEmpty); await tester.tap(find.byTooltip('Actions')); await tester.pumpAndSettle(); await tester.tap(find.text('Supprimer')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Supprimer')); await tester.pumpAndSettle(); expect(find.text('Séance jambes'), findsNothing); expect(templateRepository.saved.single.metadata.deletedAt, isNotNull); }, ); testWidgets('partager une séance ouvre le formulaire de partage', ( tester, ) async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final templateRepository = _FakeWorkoutTemplateRepository([ _workoutTemplate(), ]); final programRepository = _FakeProgramRepository(); final remoteShareApi = _FakeRemoteShareApi(); await tester.pumpWidget( MaterialApp( home: WorkoutTemplateFormScreen( workoutTemplateUseCases: WorkoutTemplateUseCases( templateRepository: templateRepository, programRepository: programRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), programUseCases: ProgramUseCases( programRepository: programRepository, exerciseRepository: _FakeExerciseRepository(), templateRepository: templateRepository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ), shareUseCases: _shareUseCases( templateRepository: templateRepository, programRepository: programRepository, remoteShareApi: remoteShareApi, ), template: templateRepository.templates.single, ), ), ); await tester.tap(find.byTooltip('Partager')); await tester.pumpAndSettle(); expect(find.text('Partager la séance'), findsOneWidget); await tester.enterText( find.widgetWithText(TextFormField, 'Emails destinataires'), 'coach@example.com', ); await tester.tap(find.text('Envoyer le partage')); await tester.pump(); await tester.pump(); expect(remoteShareApi.lastResourceType, ShareResourceType.workoutTemplate); expect(remoteShareApi.lastRecipientEmails, ['coach@example.com']); }); } WorkoutTemplate _workoutTemplate({ bool isExample = false, List tags = const [], }) { return WorkoutTemplate( metadata: _metadata('template-1'), name: 'Séance jambes', isExample: isExample, tags: tags, programs: [ WorkoutTemplateProgram( metadata: _metadata('template-program-1'), workoutTemplateId: 'template-1', position: 0, programNameSnapshot: 'Programme jambes', defaultRestSecondsSnapshot: 60, programSnapshotJson: _programSnapshotJson(), ), ], ); } ActiveWorkoutSession _activeSession() { return ActiveWorkoutSession( metadata: _metadata('session-1'), sourceWorkoutTemplateId: 'template-active', status: ActiveWorkoutStatus.running, startedAt: DateTime.utc(2026, 7, 17, 12), lastPersistedAt: DateTime.utc(2026, 7, 17, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: jsonEncode({ 'name': 'Séance active', 'programs': [ { 'id': 'template-program-1', 'programNameSnapshot': 'Programme jambes', 'programSnapshotJson': _programSnapshotJson(), }, ], 'overrides': const [], }), ); } String _programSnapshotJson() { return jsonEncode({ 'exercises': [ { 'id': 'exercise-1', 'exerciseNameSnapshot': 'Squat', 'setsCount': 3, 'timeEnabled': true, 'repsEnabled': false, 'scoreEnabled': false, 'restSecondsOverride': 60, }, ], }); } EntityMetadata _metadata(String id) { return EntityMetadata( id: id, createdAt: DateTime.utc(2026, 7, 17), updatedAt: DateTime.utc(2026, 7, 17), originDeviceId: 'device-1', ); } ExerciseStep _step({ required String id, required int position, required String name, required ExerciseStepType type, }) { return ExerciseStep( id: id, position: position, name: name, type: type, defaultTargetValue: type == ExerciseStepType.time ? 30 : 5, ); } final class _FakeClock implements Clock { const _FakeClock(this.value); final DateTime value; @override DateTime now() => value; } final class _FakeIds implements IdGenerator { var _next = 0; @override String newId() { _next += 1; return 'id-$_next'; } } final class _FakeWorkoutTemplateRepository implements WorkoutTemplateRepository { _FakeWorkoutTemplateRepository(this.templates); final List templates; final saved = []; @override Future findById(String id) async { return templates.cast().firstWhere( (template) => template?.metadata.id == id, orElse: () => null, ); } @override Future> listActive() async { return templates .where((template) => template.metadata.deletedAt == null) .toList(); } @override Future replaceComposition( WorkoutTemplate template, DateTime deletedAt, ) async { templates.removeWhere((item) => item.metadata.id == template.metadata.id); templates.add(template); } @override Future save(WorkoutTemplate template) async { saved.add(template); templates.removeWhere((item) => item.metadata.id == template.metadata.id); templates.add(template); } @override Future saveOverride(WorkoutTemplateExerciseOverride override) async {} @override Future saveProgram(WorkoutTemplateProgram program) async {} } final class _FakeActiveSessionRepository implements ActiveSessionRepository { ActiveWorkoutSession? session; var saveCount = 0; @override Future findById(String id) async { return session?.metadata.id == id ? session : null; } @override Future findOpen() async => session; @override Future findRestStateById(String id) async => null; @override Future findScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return null; } @override Future findManualScoreState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return null; } @override Future findSetTimerState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return null; } @override Future findExerciseStepProgressState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return null; } @override Future> listRestStates(String sessionId) async { return const []; } @override Future> listScoreStopwatchStates( String sessionId, ) async { return const []; } @override Future> listManualScoreStates( String sessionId, ) async { return const []; } @override Future> listSetTimerStates(String sessionId) async { return const []; } @override Future> listSetResults(String sessionId) async { return const []; } @override Future> listExerciseStepProgressStates( String sessionId, ) async { return const []; } @override Future> listExerciseStepResults( String sessionId, ) async { return const []; } @override Future save(ActiveWorkoutSession session) async { saveCount += 1; this.session = session; } @override Future saveRestState(ActiveRestState restState) async {} @override Future saveScoreStopwatchState(ActiveScoreStopwatchState state) async {} @override Future saveManualScoreState(ActiveManualScoreState state) async {} @override Future saveSetTimerState(ActiveSetTimerState state) async {} @override Future saveExerciseStepProgressState( ActiveExerciseStepProgressState state, ) async {} @override Future saveExerciseStepResult(ActiveExerciseStepResult result) async {} @override Future deleteScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }) async {} @override Future deleteManualScoreState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }) async {} @override Future saveSetResult(ActiveSetResult result) async {} } final class _FakeProgramRepository implements ProgramRepository { @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future replaceExercises(Program program, DateTime deletedAt) async {} @override Future save(Program program) async {} @override Future saveExercise(ProgramExercise exercise) async {} } final class _FakeExerciseRepository implements ExerciseRepository { @override Future findById(String id) async => null; @override Future isReferencedByProgram(String id) async => false; @override Future> listActive() async => const []; @override Future save(Exercise exercise) async {} } final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository { @override Future delete(String id, DateTime deletedAt) async {} @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future save(WorkoutHistory history) async {} @override Future patchHeartRateSummary({ required String historyId, required double averageHeartRateBpm, required int maxHeartRateBpm, required DateTime patchedAt, }) async {} @override Future saveSetResult(WorkoutHistorySetResult result) async {} @override Future saveStepResult(WorkoutHistoryStepResult result) async {} } ShareUseCases _shareUseCases({ required _FakeWorkoutTemplateRepository templateRepository, required _FakeProgramRepository programRepository, _FakeRemoteShareApi? remoteShareApi, }) { return ShareUseCases( tokenStore: _FakeAuthTokenStore('token-1'), remoteShareApi: remoteShareApi ?? _FakeRemoteShareApi(), inboxRepository: _FakeShareInboxRepository(), pendingActionRepository: _FakePendingShareActionRepository(), localChanges: _FakeLocalSyncChangeRepository(), programRepository: programRepository, templateRepository: templateRepository, clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); } final class _FakeAuthTokenStore implements AuthTokenStore { _FakeAuthTokenStore([this.token]); String? token; @override Future clearToken() async { token = null; } @override Future readToken() async => token; @override Future saveToken(String token, DateTime expiresAt) async { this.token = token; } } final class _FakeRemoteShareApi implements RemoteShareApi { ShareResourceType? lastResourceType; List lastRecipientEmails = const []; @override Future acceptShare(String shareId, String token) async { return RemoteSyncedItem( resourceType: SyncResourceType.workoutTemplate, clientId: 'template-remote', serverId: 'template-server', schemaVersion: 1, clientUpdatedAt: DateTime.utc(2026, 7, 17), serverUpdatedAt: DateTime.utc(2026, 7, 17), deletedAt: null, payload: const {'name': 'Séance partagée'}, ); } @override Future declineShare(String shareId, String token) async {} @override Future> fetchInbox(String token) async => const []; @override Future revokeShare(String shareId, String token) async {} @override Future sendShare({ required ShareResourceType resourceType, required Map payload, required List recipientEmails, required String token, }) async { lastResourceType = resourceType; lastRecipientEmails = recipientEmails; return const RemoteShareSendResult( shareId: 'share-1', recipientUserIds: ['recipient-1'], unresolvedEmails: [], ); } } final class _FakeShareInboxRepository implements ShareInboxRepository { @override Future findByShareId(String shareId) async => null; @override Future> listAll() async => const []; @override Future markStatus( String shareId, ShareInboxStatus status, DateTime respondedAt, ) async {} @override Future upsert(ShareInboxItem item) async {} @override Future upsertAll(List items) async {} } final class _FakePendingShareActionRepository implements PendingShareActionRepository { @override Future add(PendingShareAction action) async {} @override Future> listPending() async => const []; @override Future markFailed(String id, DateTime attemptedAt) async {} @override Future markSucceeded(String id, DateTime attemptedAt) async {} } final class _FakeLocalSyncChangeRepository implements LocalSyncChangeRepository { @override Future applyRemoteItem(RemoteSyncedItem item) async => true; @override Future> listPendingChanges() async => const []; @override Future markChangesSynced( List changeLogIds, DateTime syncedAt, ) async {} }