import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_state.dart'; import 'package:motorcycle_dash/services/entitlements/feature_access_service.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_storage.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_providers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('EntitlementState domain rules', () { test('default constructor creates free tier', () { const state = EntitlementState(); expect(state.plan, MotorSignalPlan.free); expect(state.status, MotorSignalEntitlementStatus.free); expect(state.isPro, isFalse); expect(state.canCloudBackup, isTrue); expect(state.canUnlimitedCloudHistory, isFalse); expect(state.canAdvancedTelemetry, isFalse); expect(state.canExportCsv, isFalse); }); test('EntitlementState.free factory normalizes to valid free tier', () { const state = EntitlementState.free(); expect(state.plan, MotorSignalPlan.free); expect(state.status, MotorSignalEntitlementStatus.free); expect(state.verificationStatus, EntitlementVerificationStatus.verified); expect(state.isPro, isFalse); expect(state.productId, isNull); expect(state.expiresAt, isNull); }); test('Pro Monthly active exposes complete premium capability surface', () { final now = DateTime.now(); final expiry = now.add(const Duration(days: 30)); final state = EntitlementState.proMonthlyActive( productId: 'motorsignal_pro_monthly', purchaseTokenHash: 'abc123hash', expiresAt: expiry, lastVerifiedAt: now, ); expect(state.isPro, isTrue); expect(state.plan, MotorSignalPlan.proMonthly); expect(state.status, MotorSignalEntitlementStatus.proMonthlyActive); expect(state.verificationStatus, EntitlementVerificationStatus.verified); expect(state.source, PurchaseSource.googlePlay); expect(state.productId, 'motorsignal_pro_monthly'); expect(state.purchaseTokenHash, 'abc123hash'); expect(state.expiresAt, expiry); // Verify all Pro capabilities expect(state.canCloudBackup, isTrue); expect(state.canUnlimitedCloudHistory, isTrue); expect(state.canMultiDeviceRestore, isTrue); expect(state.canAdvancedTelemetry, isTrue); expect(state.canAdvancedAcceleration, isTrue); expect(state.canAdvancedBraking, isTrue); expect(state.canAdvancedLean, isTrue); expect(state.canAdvancedAnalytics, isTrue); expect(state.canUnlimitedHistory, isTrue); expect(state.canExportCsv, isTrue); expect(state.canExportPdf, isTrue); expect(state.canUseMultipleMotorcycles, isTrue); expect(state.canPlanTrips, isTrue); expect(state.canCreateGroupRides, isTrue); expect(state.canUseAdvancedGroupRideFeatures, isTrue); }); test('Pro Monthly Grace provides effective premium access', () { final state = EntitlementState( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyGrace, verificationStatus: EntitlementVerificationStatus.offlineGrace, source: PurchaseSource.offlineCache, productId: 'motorsignal_pro_monthly', ); expect(state.isPro, isTrue); expect(state.canAdvancedTelemetry, isTrue); expect(state.canExportCsv, isTrue); }); test('Pro Monthly Expired denies premium access', () { final state = EntitlementState( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyExpired, verificationStatus: EntitlementVerificationStatus.expired, ); expect(state.isPro, isFalse); expect(state.canCloudBackup, isTrue); expect(state.canAdvancedTelemetry, isFalse); expect(state.canExportCsv, isFalse); expect(state.canUnlimitedHistory, isFalse); }); test('Pro Monthly Account Hold denies premium access', () { final state = EntitlementState( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyAccountHold, ); expect(state.isPro, isFalse); }); test('Pro Lifetime exposes full capabilities without expiration', () { final state = EntitlementState.proLifetime( productId: 'motorsignal_pro_lifetime', purchaseTokenHash: 'lifetime_hash', ); expect(state.isPro, isTrue); expect(state.plan, MotorSignalPlan.proLifetime); expect(state.status, MotorSignalEntitlementStatus.proLifetime); expect(state.expiresAt, isNull); expect(state.canUnlimitedCloudHistory, isTrue); expect(state.canAdvancedLean, isTrue); expect(state.canPlanTrips, isTrue); }); test('Unknown or sync pending statuses do not grant Pro access', () { const unknownState = EntitlementState( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.unknown, ); expect(unknownState.isPro, isFalse); const pendingState = EntitlementState( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.syncPending, ); expect(pendingState.isPro, isFalse); }); }); group('EntitlementState normalization & invalid state prevention', () { test('normalizes contradictory free plan with pro status to pure free', () { final normalized = EntitlementState.normalized( plan: MotorSignalPlan.free, status: MotorSignalEntitlementStatus.proLifetime, productId: 'some_product', expiresAt: DateTime.now().add(const Duration(days: 10)), ); expect(normalized.plan, MotorSignalPlan.free); expect(normalized.status, MotorSignalEntitlementStatus.free); expect(normalized.isPro, isFalse); expect(normalized.productId, isNull); expect(normalized.expiresAt, isNull); }); test('normalizes revoked verification status to non-pro status', () { final normalized = EntitlementState.normalized( plan: MotorSignalPlan.proLifetime, status: MotorSignalEntitlementStatus.proLifetime, verificationStatus: EntitlementVerificationStatus.revoked, ); expect( normalized.verificationStatus, EntitlementVerificationStatus.revoked, ); expect(normalized.isPro, isFalse); }); test('normalizes expired subscription with past expiresAt date', () { final pastDate = DateTime.now().subtract(const Duration(days: 2)); final normalized = EntitlementState.normalized( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyActive, expiresAt: pastDate, ); expect(normalized.status, MotorSignalEntitlementStatus.proMonthlyExpired); expect( normalized.verificationStatus, EntitlementVerificationStatus.expired, ); expect(normalized.isPro, isFalse); }); test('normalizes Lifetime plan ensuring null expiration', () { final normalized = EntitlementState.normalized( plan: MotorSignalPlan.proLifetime, status: MotorSignalEntitlementStatus.proMonthlyActive, expiresAt: DateTime.now().add(const Duration(days: 365)), ); expect(normalized.plan, MotorSignalPlan.proLifetime); expect(normalized.status, MotorSignalEntitlementStatus.proLifetime); expect(normalized.expiresAt, isNull); expect(normalized.isPro, isTrue); }); }); group('FeatureAccessService exhaustive mapping', () { test('every MotorSignalFeature enum entry maps to explicit capability', () { const freeService = FeatureAccessService(EntitlementState.free()); final proService = FeatureAccessService(EntitlementState.proLifetime()); for (final feature in MotorSignalFeature.values) { // Must execute cleanly without exception final freeResult = freeService.canAccess(feature); final proResult = proService.canAccess(feature); if (feature == MotorSignalFeature.cloudBackup) { expect( freeResult, isTrue, reason: 'Cloud backup included in free tier', ); expect(proResult, isTrue); } else { expect( freeResult, isFalse, reason: '$feature should be denied on Free tier', ); expect( proResult, isTrue, reason: '$feature should be granted on Pro tier', ); } } }); }); group('Entitlement serialization & storage contract', () { test('toJson and fromJson perform lossless round trip', () { final original = EntitlementState( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyActive, verificationStatus: EntitlementVerificationStatus.verified, source: PurchaseSource.googlePlay, productId: 'motorsignal_pro_monthly', purchaseTokenHash: 'token_sha256_hash', expiresAt: DateTime.utc(2026, 10, 1), lastVerifiedAt: DateTime.utc(2026, 9, 1), ); final json = original.toJson(); final restored = EntitlementState.fromJson(json); expect(restored.plan, original.plan); expect(restored.status, original.status); expect(restored.verificationStatus, original.verificationStatus); expect(restored.source, original.source); expect(restored.productId, original.productId); expect(restored.purchaseTokenHash, original.purchaseTokenHash); expect(restored.expiresAt, original.expiresAt); expect(restored.lastVerifiedAt, original.lastVerifiedAt); expect(restored.isPro, isTrue); }); test('fromJson throws on unsupported future version', () { final invalidVersionJson = {'version': 99, 'plan': 'proMonthly'}; expect( () => EntitlementState.fromJson(invalidVersionJson), throwsA(isA()), ); }); test('InMemoryEntitlementStorage behaves according to contract', () async { final storage = InMemoryEntitlementStorage(); // Missing cache defaults to Free final initial = await storage.read(); expect(initial.plan, MotorSignalPlan.free); expect(initial.isPro, isFalse); // Write Pro Lifetime final proLifetime = EntitlementState.proLifetime(productId: 'lifetime'); await storage.write(proLifetime); final cached = await storage.read(); expect(cached.plan, MotorSignalPlan.proLifetime); expect(cached.isPro, isTrue); // Clear reverts to Free await storage.clear(); final cleared = await storage.read(); expect(cleared.plan, MotorSignalPlan.free); expect(cleared.isPro, isFalse); }); }); group('Riverpod Provider integration', () { test( 'featureAccessProvider reacts to entitlementProvider updates', () async { final container = ProviderContainer( overrides: [ entitlementStorageProvider.overrideWithValue( InMemoryEntitlementStorage(), ), ], ); addTearDown(container.dispose); // Default is Free expect( container .read(featureAccessProvider) .canAccess(MotorSignalFeature.csvExport), isFalse, ); // Update to Pro Lifetime await container .read(entitlementCoordinatorProvider) .applyVerifiedEntitlement(EntitlementState.proLifetime()); expect( container .read(featureAccessProvider) .canAccess(MotorSignalFeature.csvExport), isTrue, ); // Downgrade to Expired await container .read(entitlementCoordinatorProvider) .applyVerifiedEntitlement( const EntitlementState( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyExpired, ), ); expect( container .read(featureAccessProvider) .canAccess(MotorSignalFeature.csvExport), isFalse, ); }, ); }); group('Architectural boundaries & Rider safety invariant', () { test('entitlement domain requires zero Firebase or auth dependencies', () { // Constructs, normalizes, serializes, queries access with no Firebase context const state = EntitlementState.free(); const service = FeatureAccessService(state); expect(service.canAccess(MotorSignalFeature.cloudBackup), isTrue); }); }); }