import 'dart:async'; import 'sync_models.dart'; /// Explicit cloud confirmation states representing the exact verification status of a remote ride. enum CloudConfirmationStatus { /// Firestore metadata and corresponding R2 bundle are consistently verified. confirmed, /// Required Firestore metadata or R2 object is absent. missing, /// Ride identity, owner, bundle reference, size, or checksum does not match. inconsistent, /// The current user cannot access or verify the ride. unauthorized, /// Network outage, timeout, rate limit, or transient backend failure. temporarilyUnverifiable, } /// Explicit verification report for a cloud ride. class CloudConfirmationResult { final CloudConfirmationStatus status; final String? message; final String? remoteRideId; final String? ownerUid; final String? objectKey; final int? sizeBytes; final String? sha256; const CloudConfirmationResult({ required this.status, this.message, this.remoteRideId, this.ownerUid, this.objectKey, this.sizeBytes, this.sha256, }); const CloudConfirmationResult.confirmed({ required String remoteRideId, String? ownerUid, String? objectKey, int? sizeBytes, String? sha256, String? message, }) : this( status: CloudConfirmationStatus.confirmed, remoteRideId: remoteRideId, ownerUid: ownerUid, objectKey: objectKey, sizeBytes: sizeBytes, sha256: sha256, message: message, ); const CloudConfirmationResult.missing({ required String remoteRideId, String? message, }) : this( status: CloudConfirmationStatus.missing, remoteRideId: remoteRideId, message: message, ); const CloudConfirmationResult.inconsistent({ required String remoteRideId, String? ownerUid, String? objectKey, String? message, }) : this( status: CloudConfirmationStatus.inconsistent, remoteRideId: remoteRideId, ownerUid: ownerUid, objectKey: objectKey, message: message, ); const CloudConfirmationResult.unauthorized({ required String remoteRideId, String? ownerUid, String? message, }) : this( status: CloudConfirmationStatus.unauthorized, remoteRideId: remoteRideId, ownerUid: ownerUid, message: message, ); const CloudConfirmationResult.temporarilyUnverifiable({ required String remoteRideId, String? message, }) : this( status: CloudConfirmationStatus.temporarilyUnverifiable, remoteRideId: remoteRideId, message: message, ); bool get isConfirmed => status == CloudConfirmationStatus.confirmed; bool get isMissing => status == CloudConfirmationStatus.missing; bool get isInconsistent => status == CloudConfirmationStatus.inconsistent; bool get isUnauthorized => status == CloudConfirmationStatus.unauthorized; bool get isTemporarilyUnverifiable => status == CloudConfirmationStatus.temporarilyUnverifiable; @override String toString() => 'CloudConfirmationResult(status: $status, ride: $remoteRideId, owner: $ownerUid, message: $message)'; } /// Abstract contract for synchronization providers (e.g. Firebase, GitHub, Supabase). /// Implementations must not leak provider-specific SDK types into the core sync engine. abstract class SyncProvider { /// Unique identifier of the provider (e.g. 'firebase', 'github', 'fake') String get providerId; /// Whether the provider backend and network are currently available Future isAvailable(); /// Whether this provider requires an active user authentication credential Future requiresAuthentication(); /// Performs the upload of the local ride referenced by [item]. /// Must return a deterministic [SyncResult]. Future uploadRide(SyncItem item); /// Checks if the ride with [remoteRideId] is already confirmed uploaded in the cloud. /// Backward compatible wrapper around [verifyCloudConfirmation]. Future isRideConfirmedInCloud(String remoteRideId) async { final result = await verifyCloudConfirmation(remoteRideId); return result.isConfirmed; } /// Deeply verifies whether [remoteRideId] is confirmed and recoverable in the cloud. /// Default implementation returns [CloudConfirmationStatus.missing]. Future verifyCloudConfirmation( String remoteRideId, ) async => CloudConfirmationResult.missing(remoteRideId: remoteRideId); } /// Simulated behavior modes for testing the synchronization engine deterministically enum FakeSyncBehavior { success, temporaryFailure, permanentFailure, authRequired, offline, } /// Deterministic, in-memory fake synchronization provider for testing class FakeSyncProvider implements SyncProvider { @override final String providerId; FakeSyncBehavior behavior; int delayMs; final List uploadedItems = []; final Map uploadAttemptsPerRide = {}; final Set confirmedRemoteRideIds = {}; final Map confirmationResults = {}; FakeSyncProvider({ this.providerId = 'fake', this.behavior = FakeSyncBehavior.success, this.delayMs = 0, }); @override Future isAvailable() async { if (behavior == FakeSyncBehavior.offline) return false; return true; } @override Future requiresAuthentication() async { return behavior == FakeSyncBehavior.authRequired; } @override Future isRideConfirmedInCloud(String remoteRideId) async { final result = await verifyCloudConfirmation(remoteRideId); return result.isConfirmed; } @override Future verifyCloudConfirmation( String remoteRideId, ) async { if (confirmationResults.containsKey(remoteRideId)) { return confirmationResults[remoteRideId]!; } if (confirmedRemoteRideIds.contains(remoteRideId)) { return CloudConfirmationResult.confirmed(remoteRideId: remoteRideId); } if (behavior == FakeSyncBehavior.offline || behavior == FakeSyncBehavior.temporaryFailure) { return CloudConfirmationResult.temporarilyUnverifiable( remoteRideId: remoteRideId, message: 'Fake provider backend unavailable', ); } if (behavior == FakeSyncBehavior.authRequired) { return CloudConfirmationResult.unauthorized( remoteRideId: remoteRideId, message: 'Fake provider requires authentication', ); } return CloudConfirmationResult.missing( remoteRideId: remoteRideId, message: 'Ride not found in fake provider', ); } @override Future uploadRide(SyncItem item) async { if (delayMs > 0) { await Future.delayed(Duration(milliseconds: delayMs)); } uploadAttemptsPerRide[item.localRideId] = (uploadAttemptsPerRide[item.localRideId] ?? 0) + 1; switch (behavior) { case FakeSyncBehavior.success: uploadedItems.add(item); final remoteId = 'remote_${item.localRideId}_${DateTime.now().millisecondsSinceEpoch}'; confirmedRemoteRideIds.add(remoteId); confirmationResults[remoteId] = CloudConfirmationResult.confirmed( remoteRideId: remoteId, ); return SyncResult.success(remoteId: remoteId); case FakeSyncBehavior.temporaryFailure: return SyncResult.temporaryFailure( 'Simulated temporary network/server error (HTTP 503)', ); case FakeSyncBehavior.permanentFailure: return SyncResult.permanentFailure( 'Simulated unrecoverable payload or validation error (HTTP 422)', ); case FakeSyncBehavior.authRequired: return SyncResult.authRequired( message: 'Simulated authentication expired or required (HTTP 401)', ); case FakeSyncBehavior.offline: return SyncResult.temporaryFailure( 'Simulated offline network unreachable', ); } } void reset() { uploadedItems.clear(); uploadAttemptsPerRide.clear(); confirmedRemoteRideIds.clear(); confirmationResults.clear(); } }