import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/services/sync/sync_manager.dart'; import 'package:motorcycle_dash/services/sync/sync_models.dart'; import 'package:motorcycle_dash/services/sync/sync_outbox.dart'; import 'package:motorcycle_dash/services/sync/sync_provider.dart'; class _CountingProvider implements SyncProvider { _CountingProvider({this.result = _Result.success}); static const _Result success = _Result.success; static const _Result authRequired = _Result.authRequired; static const _Result failure = _Result.failure; _Result result; int uploadCount = 0; @override String get providerId => 'firebase'; @override Future isAvailable() async => true; @override Future requiresAuthentication() async => result == _Result.authRequired; @override Future uploadRide(SyncItem item) async { uploadCount++; switch (result) { case _Result.success: return SyncResult.success(remoteId: 'remote_${item.localRideId}'); case _Result.authRequired: return SyncResult.authRequired(message: 'Authentication required'); case _Result.failure: return SyncResult.permanentFailure('Upload rejected'); } } } enum _Result { success, authRequired, failure } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('MotorSignal offline-first sync contract', () { test('same ride uploaded twice is idempotent', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final outbox = SyncOutbox(prefs, storageNamespace: 'rider_a'); final provider = _CountingProvider(); final manager = SyncManager(outbox, initialProviders: [provider], defaultProviderId: 'firebase'); await manager.enqueueRide(42, autoProcess: false); await manager.processQueue(); expect(outbox.getItem(42, 'firebase')?.status, SyncStatus.synced); await manager.enqueueRide(42, autoProcess: false); await manager.processQueue(); expect(provider.uploadCount, 1); expect(outbox.count, 1); expect(outbox.getItem(42, 'firebase')?.remoteId, 'remote_42'); }); test('User A and User B have isolated persisted sync state', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final userA = SyncOutbox(prefs, storageNamespace: 'uid_user_a'); await userA.enqueue(7, 'firebase'); await userA.update(userA.getItem(7, 'firebase')!.copyWith(status: SyncStatus.synced)); final userB = SyncOutbox(prefs, storageNamespace: 'uid_user_b'); expect(userB.count, 0); expect(userB.getItem(7, 'firebase'), isNull); final userAReloaded = SyncOutbox(prefs, storageNamespace: 'uid_user_a'); expect(userAReloaded.getItem(7, 'firebase')?.status, SyncStatus.synced); }); test('signed-out upload is rejected before provider network upload', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final outbox = SyncOutbox(prefs, storageNamespace: 'local'); final provider = _CountingProvider(result: _Result.authRequired); final manager = SyncManager(outbox, initialProviders: [provider], defaultProviderId: 'firebase'); await manager.enqueueRide(9, autoProcess: false); await manager.processQueue(); expect(outbox.getItem(9, 'firebase')?.status, SyncStatus.authRequired); expect(provider.uploadCount, 0); }); test('failed upload remains unsynced and retains failure state', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final outbox = SyncOutbox(prefs, storageNamespace: 'uid_user_a'); final provider = _CountingProvider(result: _Result.failure); final manager = SyncManager(outbox, initialProviders: [provider], defaultProviderId: 'firebase'); await manager.enqueueRide(11, autoProcess: false); await manager.processQueue(); final item = outbox.getItem(11, 'firebase'); expect(item?.status, SyncStatus.failed); expect(item?.status, isNot(SyncStatus.synced)); expect(item?.lastError, 'Upload rejected'); }); test('successful sync survives app restart', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final firstOutbox = SyncOutbox(prefs, storageNamespace: 'uid_restart'); final provider = _CountingProvider(); final manager = SyncManager(firstOutbox, initialProviders: [provider], defaultProviderId: 'firebase'); await manager.enqueueRide(13, autoProcess: false); await manager.processQueue(); expect(firstOutbox.getItem(13, 'firebase')?.status, SyncStatus.synced); // Fresh outbox/manager instances represent a process restart. final restartedOutbox = SyncOutbox(prefs, storageNamespace: 'uid_restart'); expect(restartedOutbox.getItem(13, 'firebase')?.status, SyncStatus.synced); expect(restartedOutbox.getItem(13, 'firebase')?.remoteId, 'remote_13'); }); }); }