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 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( '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('Supprimer la séance')); 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('Supprimer la séance')); 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() { return WorkoutTemplate( metadata: _metadata('template-1'), name: 'Séance jambes', 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 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> 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 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 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 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 {} }