import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/services/billing/billing_models.dart'; import 'package:motorcycle_dash/services/billing/billing_providers.dart'; import 'package:motorcycle_dash/services/billing/durable_verification_queue.dart'; import 'package:motorcycle_dash/services/billing/fake_billing_service.dart'; import 'package:motorcycle_dash/services/billing/play_billing_service.dart'; import 'package:motorcycle_dash/services/billing/product_catalog.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_coordinator.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_providers.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_state.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_storage.dart'; import 'package:motorcycle_dash/services/entitlements/feature_access_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('ProductCatalog rules', () { test('contains expected product IDs and correct classification', () { expect(ProductCatalog.proMonthly, 'motorsignal_pro_monthly'); expect(ProductCatalog.proLifetime, 'motorsignal_pro_lifetime'); expect(ProductCatalog.allProductIds, contains('motorsignal_pro_monthly')); expect( ProductCatalog.allProductIds, contains('motorsignal_pro_lifetime'), ); expect(ProductCatalog.isSubscription('motorsignal_pro_monthly'), isTrue); expect( ProductCatalog.isSubscription('motorsignal_pro_lifetime'), isFalse, ); expect(ProductCatalog.isLifetime('motorsignal_pro_lifetime'), isTrue); expect(ProductCatalog.isLifetime('motorsignal_pro_monthly'), isFalse); expect(ProductCatalog.isValidProduct('unknown_product'), isFalse); }); }); group('BillingService & FakeBillingService lifecycle', () { test('initializes and reports availability correctly', () async { final fakeBilling = FakeBillingService(); expect(await fakeBilling.initialize(), isTrue); expect(await fakeBilling.isAvailable(), isTrue); fakeBilling.available = false; expect(await fakeBilling.isAvailable(), isFalse); }); test('queryProducts returns localized product catalog', () async { final fakeBilling = FakeBillingService(); final products = await fakeBilling.queryProducts(); expect(products.length, 2); final monthly = products.firstWhere( (p) => p.id == ProductCatalog.proMonthly, ); expect(monthly.isSubscription, isTrue); expect(monthly.price, '\$4.99'); expect(monthly.rawPrice, 4.99); final lifetime = products.firstWhere( (p) => p.id == ProductCatalog.proLifetime, ); expect(lifetime.isSubscription, isFalse); expect(lifetime.price, '\$49.99'); expect(lifetime.rawPrice, 49.99); }); test('queryProducts handles store unavailable safely', () async { final fakeBilling = FakeBillingService()..available = false; final products = await fakeBilling.queryProducts(); expect(products, isEmpty); }); test('launchPurchaseFlow tracks requests and validates product', () async { final fakeBilling = FakeBillingService(); final success = await fakeBilling.launchPurchaseFlow( ProductCatalog.proMonthly, ); expect(success, isTrue); expect(fakeBilling.launchCount, 1); expect(fakeBilling.lastPurchasedProductId, ProductCatalog.proMonthly); final invalidSuccess = await fakeBilling.launchPurchaseFlow( 'invalid_sku', ); expect(invalidSuccess, isFalse); }); test('restorePurchases increments call count', () async { final fakeBilling = FakeBillingService(); await fakeBilling.restorePurchases(); expect(fakeBilling.restoreCount, 1); }); }); group('EntitlementCoordinator purchase lifecycle & verification boundary', () { late FakeBillingService billing; late InMemoryEntitlementStorage storage; late EntitlementCoordinator coordinator; setUp(() async { billing = FakeBillingService(); storage = InMemoryEntitlementStorage(); coordinator = EntitlementCoordinator( billingService: billing, storage: storage, ); await coordinator.initialize(); }); tearDown(() { coordinator.dispose(); billing.dispose(); }); test('initial state defaults to cached free tier', () { expect(coordinator.state.plan, MotorSignalPlan.free); expect(coordinator.state.isPro, isFalse); }); test( 'purchase observed transitions unverified state to syncPending (does NOT unlock Pro yet)', () async { final purchase = billing.createFakePurchase( productId: ProductCatalog.proMonthly, status: StorePurchaseStatus.purchased, ); billing.emitPurchases([purchase]); await Future.delayed(Duration.zero); // Critical Stage 4B-2 invariant: STORE PURCHASE OBSERVED != ENTITLEMENT VERIFIED expect( coordinator.state.status, MotorSignalEntitlementStatus.syncPending, ); expect( coordinator.state.isPro, isFalse, reason: 'Must not grant Pro before server verification', ); expect(coordinator.unverifiedPurchases.length, 1); expect( coordinator.unverifiedPurchases.first.purchaseId, purchase.purchaseId, ); }, ); test( 'pending store purchase transitions status to syncPending without granting access', () async { final pendingPurchase = billing.createFakePurchase( productId: ProductCatalog.proMonthly, status: StorePurchaseStatus.pending, ); billing.emitPurchases([pendingPurchase]); await Future.delayed(Duration.zero); expect( coordinator.state.status, MotorSignalEntitlementStatus.syncPending, ); expect(coordinator.state.isPro, isFalse); }, ); test( 'canceled or error purchase events do not erase existing verified entitlement', () async { // Setup an active verified Pro state in storage final verifiedState = EntitlementState.proLifetime(); await coordinator.applyVerifiedEntitlement(verifiedState); expect(coordinator.state.isPro, isTrue); // Now emit an error or canceled event final errorPurchase = billing.createFakePurchase( productId: ProductCatalog.proMonthly, status: StorePurchaseStatus.error, ); billing.emitPurchases([errorPurchase]); await Future.delayed(Duration.zero); // State remains verified Pro expect(coordinator.state.isPro, isTrue); expect(coordinator.state.plan, MotorSignalPlan.proLifetime); }, ); test( 'restored purchase is staged for verification without automatic unlock', () async { final restoredPurchase = billing.createFakePurchase( productId: ProductCatalog.proLifetime, status: StorePurchaseStatus.restored, ); billing.emitPurchases([restoredPurchase]); await Future.delayed(Duration.zero); expect(coordinator.unverifiedPurchases.length, 1); expect( coordinator.state.status, MotorSignalEntitlementStatus.syncPending, ); expect(coordinator.state.isPro, isFalse); }, ); test('unknown product IDs are ignored by coordinator', () async { final unknownPurchase = billing.createFakePurchase( productId: 'hacked_super_pro', status: StorePurchaseStatus.purchased, ); billing.emitPurchases([unknownPurchase]); await Future.delayed(Duration.zero); expect(coordinator.unverifiedPurchases, isEmpty); expect(coordinator.state.status, MotorSignalEntitlementStatus.free); }); test( 'applying verified entitlement completes purchase and updates durable storage', () async { final purchase = billing.createFakePurchase( productId: ProductCatalog.proLifetime, status: StorePurchaseStatus.purchased, token: 'valid_token_123', ); billing.emitPurchases([purchase]); await Future.delayed(Duration.zero); expect(coordinator.unverifiedPurchases.length, 1); // Simulate verification callback from Stage 4B-3 final verifiedState = EntitlementState.proLifetime( productId: ProductCatalog.proLifetime, purchaseTokenHash: purchase.purchaseTokenHash, ); await coordinator.applyVerifiedEntitlement( verifiedState, completedPurchase: purchase, ); expect(coordinator.state.isPro, isTrue); expect(coordinator.unverifiedPurchases, isEmpty); expect(billing.isPurchaseCompleted('valid_token_123'), isTrue); // Verify persisted to storage final cached = await storage.read(); expect(cached.isPro, isTrue); expect(cached.plan, MotorSignalPlan.proLifetime); }, ); }); group('Riverpod integration & Provider boundaries', () { test( 'featureAccessProvider reacts dynamically to verified coordinator changes', () async { final fakeBilling = FakeBillingService(); final storage = InMemoryEntitlementStorage(); final container = ProviderContainer( overrides: [ billingServiceProvider.overrideWithValue(fakeBilling), entitlementStorageProvider.overrideWithValue(storage), verificationQueueProvider.overrideWithValue(InMemoryVerificationQueue()), ], ); addTearDown(container.dispose); final coordinator = container.read(entitlementCoordinatorProvider); await coordinator.initialize(); // Starts Free expect( container .read(featureAccessProvider) .canAccess(MotorSignalFeature.csvExport), isFalse, ); // Emit store purchase (unverified -> still false) final purchase = fakeBilling.createFakePurchase( productId: ProductCatalog.proMonthly, status: StorePurchaseStatus.purchased, ); fakeBilling.emitPurchases([purchase]); await Future.delayed(Duration.zero); expect( container .read(featureAccessProvider) .canAccess(MotorSignalFeature.csvExport), isFalse, ); // Apply verification -> unlocks csvExport await coordinator.applyVerifiedEntitlement( EntitlementState.proMonthlyActive(), ); expect( container .read(featureAccessProvider) .canAccess(MotorSignalFeature.csvExport), isTrue, ); }, ); test( 'production billingServiceProvider defaults to PlayBillingService', () { final container = ProviderContainer(); addTearDown(container.dispose); final service = container.read(billingServiceProvider); expect(service, isA()); expect(service, isNot(isA())); }, ); }); group('Account Independence & Safety', () { test( 'billing service has no dependency on Firebase Auth or cloud accounts', () { final billing = FakeBillingService(); expect(billing, isA()); // Zero AuthUser or Firebase imports required }, ); test( 'restoring purchases operates without any account prerequisites', () async { final billing = FakeBillingService(); await billing.restorePurchases(); expect(billing.restoreCount, 1); }, ); }); }