import 'dart:convert'; import 'dart:typed_data'; 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/profile_screen.dart'; import 'package:share_plus/share_plus.dart'; void main() { testWidgets('le profil déconnecté invite sans bloquer le reste', ( tester, ) async { final harness = _AuthHarness(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); expect(find.text('Compte GameTime'), findsOneWidget); expect(find.text('Créer un compte'), findsOneWidget); expect(find.text('Se connecter'), findsOneWidget); expect(find.byType(AlertDialog), findsNothing); }); testWidgets('la connexion invalide affiche une erreur inline', ( tester, ) async { final harness = _AuthHarness() ..remote.loginFailure = RemoteAuthFailure.invalidCredentials; await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); await tester.tap(find.text('Se connecter')); await tester.pumpAndSettle(); expect(find.text('Tu peux revenir à GameTime.'), findsNothing); await tester.enterText( find.widgetWithText(TextFormField, 'Email'), 'a@b.fr', ); await tester.enterText( find.widgetWithText(TextFormField, 'Mot de passe'), 'password1', ); await tester.tap(find.widgetWithText(FilledButton, 'Se connecter')); await tester.pumpAndSettle(); expect(find.text('Email ou mot de passe incorrect.'), findsOneWidget); expect(find.byType(AlertDialog), findsNothing); }); testWidgets('une erreur serveur affiche le message dédié', (tester) async { final harness = _AuthHarness() ..remote.loginFailure = RemoteAuthFailure.server; await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); await tester.tap(find.text('Se connecter')); await tester.pumpAndSettle(); await tester.enterText( find.widgetWithText(TextFormField, 'Email'), 'a@b.fr', ); await tester.enterText( find.widgetWithText(TextFormField, 'Mot de passe'), 'password1', ); await tester.tap(find.widgetWithText(FilledButton, 'Se connecter')); await tester.pumpAndSettle(); expect( find.text('Serveur indisponible pour le moment. Réessaie plus tard.'), findsOneWidget, ); expect(find.byType(AlertDialog), findsNothing); }); testWidgets('la connexion réussie affiche le profil connecté', ( tester, ) async { final harness = _AuthHarness(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); await tester.tap(find.text('Se connecter')); await tester.pumpAndSettle(); await tester.enterText( find.widgetWithText(TextFormField, 'Email'), 'alex@example.com', ); await tester.enterText( find.widgetWithText(TextFormField, 'Mot de passe'), 'password1', ); await tester.tap(find.widgetWithText(FilledButton, 'Se connecter')); await tester.pumpAndSettle(); expect(find.text('alex@example.com'), findsOneWidget); expect(find.text('Synchronisation'), findsOneWidget); expect(find.text('Partages reçus'), findsOneWidget); expect(harness.syncRemote.pullCalls, 1); }); testWidgets('la déconnexion repasse au profil déconnecté', (tester) async { final harness = _AuthHarness( initialSession: UserAccountSession( id: 'account-1', serverUserId: 'server-user-1', email: 'alex@example.com', displayName: 'Alex', isLoggedIn: true, createdAt: DateTime.utc(2026, 7, 17), updatedAt: DateTime.utc(2026, 7, 17), ), initialToken: 'token-1', ); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); expect(find.text('Alex'), findsOneWidget); await tester.tap(find.text('Se déconnecter')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Se déconnecter')); await tester.pumpAndSettle(); expect(harness.tokenStore.token, isNull); expect(harness.accountRepository.session?.isLoggedIn, isFalse); expect(find.text('Compte GameTime'), findsOneWidget); }); testWidgets( 'le bouton de synchronisation lance une synchronisation manuelle', (tester) async { final harness = _AuthHarness.connected(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); expect(find.text('Synchroniser maintenant'), findsOneWidget); await tester.tap(find.text('Synchroniser maintenant')); await tester.pump(); await tester.pump(); await tester.pump(); expect(harness.syncRemote.pullCalls, 1); }, ); testWidgets('un souci de synchronisation reste neutre dans le profil', ( tester, ) async { final harness = _AuthHarness.connected()..syncRemote.throwOnPull = true; await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); await tester.tap(find.text('Synchroniser maintenant')); await tester.pump(); await tester.pump(); await tester.pump(); expect(harness.syncMetadata.metadata.status, OnlineSyncStatus.failure); expect(find.text('Synchronisation en attente'), findsOneWidget); expect(find.textContaining('Erreur'), findsNothing); expect(find.byType(AlertDialog), findsNothing); }); testWidgets('le statut se met à jour après une synchronisation réussie', ( tester, ) async { final harness = _AuthHarness.connected(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); expect(find.text('Synchronisation en attente'), findsOneWidget); await tester.tap(find.text('Synchroniser maintenant')); await tester.pump(); await tester.pump(); await tester.pump(); expect(find.textContaining('Dernière synchro :'), findsOneWidget); }); testWidgets('partages reçus ouvre la boîte de réception', (tester) async { final harness = _AuthHarness.connected(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); await tester.tap(find.text('Partages reçus')); await tester.pumpAndSettle(); expect(find.text('Aucun partage reçu'), findsOneWidget); }); testWidgets( 'la section Sauvegarde locale est visible déconnecté et connecté', (tester) async { final harness = _AuthHarness(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, ), ), ); await tester.pump(); expect(find.text('Sauvegarde locale'), findsOneWidget); expect(find.text('Exporter mes données'), findsOneWidget); expect(find.text('Importer des données'), findsOneWidget); final connectedHarness = _AuthHarness.connected(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: connectedHarness.useCases, syncUseCases: connectedHarness.syncUseCases, shareUseCases: connectedHarness.shareUseCases, dataExportUseCase: connectedHarness.dataExportUseCase, dataImportUseCase: connectedHarness.dataImportUseCase, ), ), ); await tester.pump(); expect(find.text('Sauvegarde locale'), findsOneWidget); expect(find.text('Exporter mes données'), findsOneWidget); expect(find.text('Importer des données'), findsOneWidget); }, ); testWidgets('export réussi affiche le message de sauvegarde exportée', ( tester, ) async { final harness = _AuthHarness(); final exporter = _FakeBackupFileExporter(status: ShareResultStatus.success); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, backupFileExporter: exporter, ), ), ); await tester.pump(); await tester.tap(find.text('Exporter mes données')); await tester.pumpAndSettle(); expect(find.text('Sauvegarde exportée.'), findsOneWidget); expect(exporter.exportedFileName, isNotNull); expect(exporter.exportedFileName, endsWith('.gametime')); }); testWidgets('un échec d’export affiche un message neutre', (tester) async { final harness = _AuthHarness(); final exporter = _FakeBackupFileExporter()..throwOnExport = true; await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, backupFileExporter: exporter, ), ), ); await tester.pump(); await tester.tap(find.text('Exporter mes données')); await tester.pumpAndSettle(); expect(find.text('Export impossible pour le moment.'), findsOneWidget); }); testWidgets('import fusion réussie affiche les données importées', ( tester, ) async { final harness = _AuthHarness()..backupRepository.hasData = true; final bytes = const LocalBackupCodec().encode( LocalDataExportSnapshot( exportedAt: DateTime.utc(2026, 7, 22, 10, 15), appSchemaVersion: 19, originDeviceId: 'device-1', mediaAssets: const [], exercises: const [], programs: const [], workoutTemplates: const [], workoutHistories: const [], mediaFiles: const [], ), ); final picker = _FakeBackupFilePicker( file: LocalBackupPickedFile( fileName: 'gametime-sauvegarde-2026-07-22.gametime', bytes: bytes, ), ); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, backupFilePicker: picker, ), ), ); await tester.pump(); await tester.tap(find.text('Importer des données')); await tester.pumpAndSettle(); expect(find.text('Importer cette sauvegarde ?'), findsOneWidget); expect(find.text('Fusionner'), findsOneWidget); expect(find.text('Remplacer tout'), findsOneWidget); await tester.tap(find.text('Fusionner')); await tester.pumpAndSettle(); expect(find.text('Données importées.'), findsOneWidget); expect(harness.backupRepository.appliedModes, [ LocalBackupImportMode.merge, ]); }); testWidgets('remplacer tout exige une seconde confirmation destructive', ( tester, ) async { final harness = _AuthHarness()..backupRepository.hasData = true; final bytes = const LocalBackupCodec().encode( LocalDataExportSnapshot( exportedAt: DateTime.utc(2026, 7, 22, 10, 15), appSchemaVersion: 19, originDeviceId: 'device-1', mediaAssets: const [], exercises: const [], programs: const [], workoutTemplates: const [], workoutHistories: const [], mediaFiles: const [], ), ); final picker = _FakeBackupFilePicker( file: LocalBackupPickedFile( fileName: 'gametime-sauvegarde-2026-07-22.gametime', bytes: bytes, ), ); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, backupFilePicker: picker, ), ), ); await tester.pump(); await tester.tap(find.text('Importer des données')); await tester.pumpAndSettle(); await tester.tap(find.text('Remplacer tout')); await tester.pumpAndSettle(); expect(find.text('Remplacer toutes les données locales ?'), findsOneWidget); expect(harness.backupRepository.appliedModes, isEmpty); await tester.tap(find.widgetWithText(TextButton, 'Annuler')); await tester.pumpAndSettle(); expect(harness.backupRepository.appliedModes, isEmpty); await tester.tap(find.text('Importer des données')); await tester.pumpAndSettle(); await tester.tap(find.text('Remplacer tout')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Remplacer tout')); await tester.pumpAndSettle(); expect(find.text('Sauvegarde restaurée.'), findsOneWidget); expect(harness.backupRepository.appliedModes, [ LocalBackupImportMode.replaceAll, ]); }); testWidgets('un fichier invalide affiche le message dédié', (tester) async { final harness = _AuthHarness(); final picker = _FakeBackupFilePicker( file: LocalBackupPickedFile( fileName: 'notes.txt', bytes: Uint8List.fromList(utf8.encode('{"kind":"something.else"}')), ), ); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, backupFilePicker: picker, ), ), ); await tester.pump(); await tester.tap(find.text('Importer des données')); await tester.pumpAndSettle(); expect(find.text('Fichier invalide'), findsOneWidget); expect( find.text('Ce fichier n’est pas une sauvegarde GameTime valide.'), findsOneWidget, ); }); testWidgets('une séance active bloque l’import', (tester) async { final harness = _AuthHarness() ..backupRepository.hasOpenSession = true ..backupRepository.hasData = true; final bytes = const LocalBackupCodec().encode( LocalDataExportSnapshot( exportedAt: DateTime.utc(2026, 7, 22, 10, 15), appSchemaVersion: 19, originDeviceId: 'device-1', mediaAssets: const [], exercises: const [], programs: const [], workoutTemplates: const [], workoutHistories: const [], mediaFiles: const [], ), ); final picker = _FakeBackupFilePicker( file: LocalBackupPickedFile( fileName: 'gametime-sauvegarde-2026-07-22.gametime', bytes: bytes, ), ); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, backupFilePicker: picker, ), ), ); await tester.pump(); await tester.tap(find.text('Importer des données')); await tester.pumpAndSettle(); expect(find.text('Importer cette sauvegarde ?'), findsOneWidget); await tester.tap(find.text('Fusionner')); await tester.pumpAndSettle(); expect(find.text('Séance en cours'), findsOneWidget); expect( find.text( 'Termine ou sauvegarde ta séance avant d’importer des données.', ), findsOneWidget, ); }); testWidgets('annuler la sélection de fichier reste silencieux', ( tester, ) async { final harness = _AuthHarness(); final picker = _FakeBackupFilePicker(); await tester.pumpWidget( MaterialApp( home: ProfileScreen( authUseCases: harness.useCases, syncUseCases: harness.syncUseCases, shareUseCases: harness.shareUseCases, dataExportUseCase: harness.dataExportUseCase, dataImportUseCase: harness.dataImportUseCase, backupFilePicker: picker, ), ), ); await tester.pump(); await tester.tap(find.text('Importer des données')); await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsNothing); expect(find.byType(SnackBar), findsNothing); }); } final class _AuthHarness { _AuthHarness({UserAccountSession? initialSession, String? initialToken}) : tokenStore = _FakeAuthTokenStore(initialToken), accountRepository = _FakeOnlineAccountRepository(initialSession), remote = _FakeRemoteAuthApi(), syncRemote = _FakeRemoteSyncApi(), syncMetadata = _FakeSyncMetadataRepository(), syncMappings = _FakeRemoteResourceMappingRepository(), syncChanges = _FakeLocalSyncChangeRepository(), shareRemote = _FakeRemoteShareApi(), shareInbox = _FakeShareInboxRepository(), pendingShares = _FakePendingShareActionRepository(), programRepository = _FakeProgramRepository(), templateRepository = _FakeWorkoutTemplateRepository(), backupRepository = _FakeLocalDataBackupRepository(), backupMediaStore = _FakeLocalBackupMediaStore() { useCases = AuthUseCases( tokenStore: tokenStore, accountRepository: accountRepository, remoteAuthApi: remote, clock: const _FakeClock(), ids: _FakeIds(), ); syncUseCases = SyncUseCases( tokenStore: tokenStore, remoteSyncApi: syncRemote, metadataRepository: syncMetadata, mappingRepository: syncMappings, localChanges: syncChanges, clock: const _FakeClock(), deviceId: 'device-1', ); shareUseCases = ShareUseCases( tokenStore: tokenStore, remoteShareApi: shareRemote, inboxRepository: shareInbox, pendingActionRepository: pendingShares, localChanges: syncChanges, programRepository: programRepository, templateRepository: templateRepository, clock: const _FakeClock(), ids: _FakeIds(), originDeviceId: 'device-1', ); dataExportUseCase = DataExportUseCase( repository: backupRepository, mediaStore: backupMediaStore, clock: const _FakeClock(), ); dataImportUseCase = DataImportUseCase( repository: backupRepository, mediaStore: backupMediaStore, clock: const _FakeClock(), ); } factory _AuthHarness.connected() { return _AuthHarness( initialSession: UserAccountSession( id: 'account-1', serverUserId: 'server-user-1', email: 'alex@example.com', displayName: 'Alex', isLoggedIn: true, createdAt: DateTime.utc(2026, 7, 17), updatedAt: DateTime.utc(2026, 7, 17), ), initialToken: 'token-1', ); } final _FakeAuthTokenStore tokenStore; final _FakeOnlineAccountRepository accountRepository; final _FakeRemoteAuthApi remote; final _FakeRemoteSyncApi syncRemote; final _FakeSyncMetadataRepository syncMetadata; final _FakeRemoteResourceMappingRepository syncMappings; final _FakeLocalSyncChangeRepository syncChanges; final _FakeRemoteShareApi shareRemote; final _FakeShareInboxRepository shareInbox; final _FakePendingShareActionRepository pendingShares; final _FakeProgramRepository programRepository; final _FakeWorkoutTemplateRepository templateRepository; final _FakeLocalDataBackupRepository backupRepository; final _FakeLocalBackupMediaStore backupMediaStore; late final AuthUseCases useCases; late final SyncUseCases syncUseCases; late final ShareUseCases shareUseCases; late final DataExportUseCase dataExportUseCase; late final DataImportUseCase dataImportUseCase; } final class _FakeClock implements Clock { const _FakeClock(); @override DateTime now() => DateTime.utc(2026, 7, 17, 12); } final class _FakeIds implements IdGenerator { var _next = 0; @override String newId() { _next += 1; return 'local-account-$_next'; } } 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 _FakeOnlineAccountRepository implements OnlineAccountRepository { _FakeOnlineAccountRepository(this.session); UserAccountSession? session; @override Future clearSession() async { session = null; } @override Future currentSession() async => session; @override Future saveSession(UserAccountSession session) async { this.session = session; } } final class _FakeRemoteAuthApi implements RemoteAuthApi { RemoteAuthFailure? loginFailure; RemoteAuthFailure? registerFailure; @override Future login({ required String email, required String password, }) async { final failure = loginFailure; if (failure != null) { throw RemoteAuthException(failure); } return RemoteAuthResult( userId: 'server-user-1', email: email.trim(), token: 'token-1', expiresAt: DateTime.utc(2026, 7, 18), ); } @override Future logout(String token) async {} @override Future register({ required String email, required String password, String? displayName, }) async { final failure = registerFailure; if (failure != null) { throw RemoteAuthException(failure); } return RemoteAuthResult( userId: 'server-user-1', email: email.trim(), token: 'token-1', expiresAt: DateTime.utc(2026, 7, 18), ); } } final class _FakeRemoteSyncApi implements RemoteSyncApi { var pullCalls = 0; var pushCalls = 0; var throwOnPull = false; @override Future push({ required String deviceId, required List items, required String token, }) async { pushCalls += 1; return const RemoteSyncPushResult(serverCursor: null, results: []); } @override Future pull({ required String? since, required String token, }) async { pullCalls += 1; if (throwOnPull) { throw Exception('temporary unavailable'); } return const RemoteSyncPullResult(serverCursor: 'cursor-1', items: []); } } final class _FakeSyncMetadataRepository implements SyncMetadataRepository { var metadata = const SyncMetadataSnapshot(); @override Future read() async => metadata; @override Future save(SyncMetadataSnapshot metadata) async { this.metadata = metadata; } } final class _FakeRemoteResourceMappingRepository implements RemoteResourceMappingRepository { final _mappings = {}; @override Future find({ required SyncResourceType resourceType, required String clientId, }) async { return _mappings['${resourceType.name}:$clientId']; } @override Future save(RemoteResourceMapping mapping) async { _mappings['${mapping.resourceType.name}:${mapping.clientId}'] = mapping; } } 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 {} } final class _FakeRemoteShareApi implements RemoteShareApi { @override Future acceptShare(String shareId, String token) async { return RemoteSyncedItem( resourceType: SyncResourceType.program, clientId: 'program-remote', serverId: 'program-server', schemaVersion: 1, clientUpdatedAt: DateTime.utc(2026, 7, 17), serverUpdatedAt: DateTime.utc(2026, 7, 17), deletedAt: null, payload: const {'name': 'Programme partagé'}, ); } @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 { return const RemoteShareSendResult( shareId: 'share-1', recipientUserIds: ['recipient-1'], unresolvedEmails: [], ); } } final class _FakeShareInboxRepository implements ShareInboxRepository { final items = []; @override Future findByShareId(String shareId) async { return items.cast().firstWhere( (item) => item?.shareId == shareId, orElse: () => null, ); } @override Future> listAll() async => List.of(items); @override Future markStatus( String shareId, ShareInboxStatus status, DateTime respondedAt, ) async { final index = items.indexWhere((item) => item.shareId == shareId); if (index == -1) return; items[index] = items[index].copyWith( status: status, respondedAt: respondedAt, ); } @override Future upsert(ShareInboxItem item) async { items.removeWhere((existing) => existing.shareId == item.shareId); items.add(item); } @override Future upsertAll(List items) async { for (final item in items) { await upsert(item); } } } final class _FakePendingShareActionRepository implements PendingShareActionRepository { final actions = []; @override Future add(PendingShareAction action) async { actions.add(action); } @override Future> listPending() async => actions; @override Future markFailed(String id, DateTime attemptedAt) async {} @override Future markSucceeded(String id, DateTime attemptedAt) 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 _FakeWorkoutTemplateRepository implements WorkoutTemplateRepository { @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future replaceComposition( WorkoutTemplate template, DateTime deletedAt, ) async {} @override Future save(WorkoutTemplate template) async {} @override Future saveOverride(WorkoutTemplateExerciseOverride override) async {} @override Future saveProgram(WorkoutTemplateProgram program) async {} } final class _FakeLocalDataBackupRepository implements LocalDataBackupRepository { var hasOpenSession = false; var hasData = false; var throwOnApply = false; final appliedModes = []; @override Future applyImportSnapshot({ required LocalDataExportSnapshot snapshot, required LocalBackupImportMode mode, required DateTime importedAt, }) async { if (throwOnApply) { throw Exception('transaction failed'); } appliedModes.add(mode); return const LocalBackupImportResult( insertedCount: 1, updatedCount: 0, ignoredOlderCount: 0, deletedByReplaceCount: 0, missingMediaCount: 0, ); } @override Future hasAnyUserData() async => hasData; @override Future hasOpenActiveWorkoutSession() async => hasOpenSession; @override Future readExportSnapshot( DateTime exportedAt, ) async { return LocalDataExportSnapshot( exportedAt: exportedAt, appSchemaVersion: 19, originDeviceId: 'device-1', mediaAssets: const [], exercises: const [], programs: const [], workoutTemplates: const [], workoutHistories: const [], mediaFiles: const [], ); } } final class _FakeLocalBackupMediaStore implements LocalBackupMediaStore { @override Future> readEmbeddableFiles( List mediaAssets, ) async => const []; @override Future> restoreEmbeddedFiles( List files, ) async => const {}; } final class _FakeBackupFileExporter implements LocalBackupFileExporter { _FakeBackupFileExporter({this.status = ShareResultStatus.success}); ShareResultStatus status; String? exportedFileName; Uint8List? exportedBytes; var throwOnExport = false; @override Future exportFile({ required String fileName, required Uint8List bytes, }) async { if (throwOnExport) { throw Exception('share failed'); } exportedFileName = fileName; exportedBytes = bytes; return status; } } final class _FakeBackupFilePicker implements LocalBackupFilePicker { _FakeBackupFilePicker({this.file}); LocalBackupPickedFile? file; @override Future pickFile() async => file; }