import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/features/entitlements/widgets/pro_lock_badge.dart'; import 'package:motorcycle_dash/services/billing/product_catalog.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/feature_access_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FeatureAccessService Matrix Tests', () { test( 'Free tier allows basic cloud backup but denies all Pro-only features', () { final freeState = EntitlementState.free(); final service = FeatureAccessService(freeState); // Cloud backup is permitted for free users expect(service.canAccess(MotorSignalFeature.cloudBackup), isTrue); final proOnlyFeatures = MotorSignalFeature.values.where( (f) => f != MotorSignalFeature.cloudBackup, ); for (final feature in proOnlyFeatures) { expect( service.canAccess(feature), isFalse, reason: 'Free tier must not access ${feature.name}', ); } }, ); test('Active Pro Monthly grants all features', () { final proActiveState = EntitlementState.proMonthlyActive( productId: ProductCatalog.proMonthly, expiresAt: DateTime.now().add(const Duration(days: 30)), ); final service = FeatureAccessService(proActiveState); for (final feature in MotorSignalFeature.values) { expect( service.canAccess(feature), isTrue, reason: 'Active Pro Monthly must access ${feature.name}', ); } }); test( 'Pro Monthly in Grace Period grants all features for rider continuity', () { final graceState = EntitlementState.normalized( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyGrace, verificationStatus: EntitlementVerificationStatus.verified, source: PurchaseSource.googlePlay, productId: ProductCatalog.proMonthly, expiresAt: DateTime.now().add(const Duration(days: 2)), ); final service = FeatureAccessService(graceState); for (final feature in MotorSignalFeature.values) { expect( service.canAccess(feature), isTrue, reason: 'Grace period must access ${feature.name}', ); } }, ); test('Pro Monthly on Account Hold denies all Pro-only features', () { final holdState = EntitlementState.normalized( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyAccountHold, verificationStatus: EntitlementVerificationStatus.verified, source: PurchaseSource.googlePlay, productId: ProductCatalog.proMonthly, ); final service = FeatureAccessService(holdState); final proOnlyFeatures = MotorSignalFeature.values.where( (f) => f != MotorSignalFeature.cloudBackup, ); for (final feature in proOnlyFeatures) { expect( service.canAccess(feature), isFalse, reason: 'Account hold must deny ${feature.name}', ); } }); test('Pro Monthly Paused denies all Pro-only features', () { final pausedState = EntitlementState.normalized( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyPaused, verificationStatus: EntitlementVerificationStatus.verified, source: PurchaseSource.googlePlay, productId: ProductCatalog.proMonthly, ); final service = FeatureAccessService(pausedState); final proOnlyFeatures = MotorSignalFeature.values.where( (f) => f != MotorSignalFeature.cloudBackup, ); for (final feature in proOnlyFeatures) { expect( service.canAccess(feature), isFalse, reason: 'Paused subscription must deny ${feature.name}', ); } }); test('Pro Monthly Expired denies all Pro-only features', () { final expiredState = EntitlementState.normalized( plan: MotorSignalPlan.proMonthly, status: MotorSignalEntitlementStatus.proMonthlyExpired, verificationStatus: EntitlementVerificationStatus.expired, source: PurchaseSource.googlePlay, productId: ProductCatalog.proMonthly, ); final service = FeatureAccessService(expiredState); final proOnlyFeatures = MotorSignalFeature.values.where( (f) => f != MotorSignalFeature.cloudBackup, ); for (final feature in proOnlyFeatures) { expect( service.canAccess(feature), isFalse, reason: 'Expired subscription must deny ${feature.name}', ); } }); test('Pro Lifetime grants all features permanently', () { final lifetimeState = EntitlementState.proLifetime( productId: ProductCatalog.proLifetime, ); final service = FeatureAccessService(lifetimeState); for (final feature in MotorSignalFeature.values) { expect( service.canAccess(feature), isTrue, reason: 'Pro Lifetime must access ${feature.name}', ); } }); }); group('UI Lock & Badge Widgets Tests', () { testWidgets('ProBadge renders with PRO label and styling', (tester) async { await tester.pumpWidget( const MaterialApp(home: Scaffold(body: ProBadge())), ); expect(find.text('PRO'), findsOneWidget); }); testWidgets( 'ProLockedRow renders when user is Free and hides when user is Pro', (tester) async { // 1. When user is Free, row is visible await tester.pumpWidget( ProviderScope( overrides: [ entitlementProvider.overrideWithValue( const EntitlementState.free(), ), ], child: const MaterialApp( home: Scaffold( body: ProLockedRow( feature: MotorSignalFeature.csvExport, title: Text('CSV Export'), subtitle: Text('Export telemetry data as spreadsheet'), ), ), ), ), ); expect(find.text('CSV Export'), findsOneWidget); expect( find.text('Export telemetry data as spreadsheet'), findsOneWidget, ); expect(find.text('PRO'), findsOneWidget); expect(find.byIcon(Icons.lock_outline), findsOneWidget); // 2. When user is Pro, row hides (SizedBox.shrink) final proState = EntitlementState.proLifetime( productId: ProductCatalog.proLifetime, ); await tester.pumpWidget( ProviderScope( overrides: [entitlementProvider.overrideWithValue(proState)], child: const MaterialApp( home: Scaffold( body: ProLockedRow( feature: MotorSignalFeature.csvExport, title: Text('CSV Export'), subtitle: Text('Export telemetry data as spreadsheet'), ), ), ), ), ); expect(find.text('CSV Export'), findsNothing); expect(find.text('PRO'), findsNothing); }, ); test( 'FeatureBenefitInfo provides title and description for all features', () { for (final feature in MotorSignalFeature.values) { final info = FeatureBenefitInfo.forFeature(feature); expect(info.title, isNotEmpty); expect(info.description, isNotEmpty); expect(info.icon, isNotNull); } }, ); }); }