import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/core/theme/theme.dart'; import 'package:motorcycle_dash/features/group_rides/group_rides_hub_screen.dart'; import 'package:motorcycle_dash/features/group_rides/create_group_ride_screen.dart'; import 'package:motorcycle_dash/features/group_rides/group_ride_detail_screen.dart'; import 'package:motorcycle_dash/features/group_rides/live_group_ride_screen.dart'; import 'package:motorcycle_dash/features/social/rider_profile_screen.dart'; import 'package:motorcycle_dash/features/social/friends_screen.dart'; import 'package:motorcycle_dash/services/auth/auth_providers.dart'; import 'package:motorcycle_dash/services/auth/auth_service.dart'; import 'package:motorcycle_dash/services/auth/auth_state.dart'; import 'package:motorcycle_dash/services/auth/auth_user.dart'; import 'package:motorcycle_dash/services/group_rides/group_ride_models.dart'; import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart'; import 'package:motorcycle_dash/services/group_rides/live_broadcast_controller.dart'; import 'package:motorcycle_dash/services/social/rider_profile_models.dart'; import 'package:motorcycle_dash/services/social/social_providers.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/services/routes/route_providers.dart'; class _FakeAuth implements AuthService { @override Future ensureBackendIdentity() async {} final _states = StreamController.broadcast(); AuthUser? user; _FakeAuth({this.user}); @override Stream get authStateChanges => _states.stream; @override AuthState get currentState => user == null ? const AuthState.unauthenticated(isFirebaseConfigured: true) : (user!.isAnonymous ? AuthState.anonymous(user!, isFirebaseConfigured: true) : AuthState.authenticated(user!, isFirebaseConfigured: true)); @override AuthUser? get currentUser => user; @override bool get isConfigured => true; @override bool get isEmailVerified => true; @override Future deleteCurrentUser() async {} @override void dispose() => _states.close(); @override Future getIdToken([bool forceRefresh = false]) async => 'token'; @override Future reauthenticate({String? password}) async {} @override Future reloadCurrentUser() async {} @override Future sendEmailVerification() async {} @override Future sendPasswordResetEmail(String email) async {} @override Future signInAnonymously() async {} @override Future signInWithEmailAndPassword(String email, String password) async {} @override Future createUserWithEmailAndPassword(String email, String password, {String? displayName}) async {} @override Future signInWithGoogle() async {} @override Future signOut() async {} } class _FakeLiveBroadcastController extends StateNotifier implements LiveBroadcastController { _FakeLiveBroadcastController(super.initialState); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); @override Future setOptIn(bool optIn) async {} @override Future setStatus(LivePresenceStatus status) async {} @override Future triggerRegroup() async {} @override Future clearRegroup() async {} @override void updateAppLifecycle(AppLifecycleState state) {} } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late SharedPreferences prefs; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); }); const testUser = AuthUser( uid: 'uid_lead', email: 'rider@motorsignal.app', displayName: 'Canyon Lead', isAnonymous: false, ); final testProfile = RiderProfile( uid: 'uid_lead', handle: 'canyon_lead', displayName: 'Canyon Lead', bio: 'Twisty roads and track days', region: 'Blue Ridge, NC', avatarPreset: 'preset_neon_cyan', privacySettings: const ProfilePrivacySettings( isSearchableByHandle: true, isProfilePublic: true, allowFriendRequests: true, ), createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final now = DateTime(2026, 9, 1, 12, 0); final testRide = GroupRide( id: 'ride_101', organizerUid: 'uid_lead', organizerHandle: 'canyon_lead', organizerDisplayName: 'Canyon Lead', title: 'Blue Ridge Twisties Run', description: 'Scenic group ride through Blue Ridge twisties.', scheduledStartAt: DateTime(2026, 10, 15, 9, 0), departureAt: DateTime(2026, 10, 15, 9, 30), status: GroupRideStatus.underway, joinPolicy: GroupRideJoinPolicy.open, visibility: GroupRideVisibility.public, meetingPoint: const GroupRideLocation( latitude: 35.5951, longitude: -82.5515, displayName: 'Asheville Meetup', locationType: GroupRideLocationType.meetup, ), destination: const GroupRideLocation( latitude: 35.7951, longitude: -82.2515, displayName: 'Mount Mitchell Peak', locationType: GroupRideLocationType.destination, ), participantCount: 3, stops: const [ GroupRideLocation( latitude: 35.65, longitude: -82.40, displayName: 'Scenic Overlook Stop', locationType: GroupRideLocationType.scenic, ), ], createdAt: now, updatedAt: now, ); final List testParticipants = [ GroupRideParticipant( userId: 'uid_lead', riderHandle: 'canyon_lead', riderDisplayName: 'Canyon Lead', role: GroupRideRole.lead, status: ParticipantStatus.accepted, createdAt: now, updatedAt: now, ), GroupRideParticipant( userId: 'uid_sweep', riderHandle: 'tail_gunner', riderDisplayName: 'Tail Gunner', role: GroupRideRole.sweep, status: ParticipantStatus.accepted, createdAt: now, updatedAt: now, ), GroupRideParticipant( userId: 'uid_rider3', riderHandle: 'moto_enthusiast', riderDisplayName: 'Moto Enthusiast', role: GroupRideRole.participant, status: ParticipantStatus.accepted, createdAt: now, updatedAt: now, ), ]; final List testPresences = [ LiveGroupPresence( userId: 'uid_lead', groupRideId: 'ride_101', latitude: 35.5951, longitude: -82.5515, heading: 45.0, speedMph: 45.0, accuracyMeters: 5.0, status: LivePresenceStatus.riding, sharingEnabled: true, updatedAt: now, expiresAt: now.add(const Duration(seconds: 120)), ), LiveGroupPresence( userId: 'uid_sweep', groupRideId: 'ride_101', latitude: 35.5900, longitude: -82.5500, heading: 40.0, speedMph: 42.0, accuracyMeters: 5.0, status: LivePresenceStatus.riding, sharingEnabled: true, updatedAt: now.subtract(const Duration(seconds: 8)), expiresAt: now.add(const Duration(seconds: 112)), ), ]; final List testFriends = [ FriendSummary( uid: 'uid_sweep', handle: 'tail_gunner', displayName: 'Tail Gunner', avatarPreset: 'preset_amber_flame', friendedAt: DateTime(2026, 8, 1), ), FriendSummary( uid: 'uid_rider3', handle: 'moto_enthusiast', displayName: 'Moto Enthusiast', avatarPreset: 'preset_electric_green', friendedAt: DateTime(2026, 8, 10), ), ]; final List testIncomingRequests = [ FriendRequest( requestId: 'req_1', senderUid: 'uid_guest', senderHandle: 'speed_demon', senderDisplayName: 'Speed Demon', senderAvatarPreset: 'preset_hyper_purple', receiverUid: 'uid_lead', sentAt: DateTime(2026, 9, 4), ), ]; final viewports = { 'Compact Phone (320x640)': const Size(320, 640), 'Standard Phone (400x800)': const Size(400, 800), 'Tall Phone (412x915)': const Size(412, 915), 'Small Landscape (640x320)': const Size(640, 320), 'Short Moto Mount Landscape (780x360)': const Size(780, 360), 'Standard Landscape (800x360)': const Size(800, 360), 'Wide Landscape (840x390)': const Size(840, 390), 'Medium Tablet Portrait (768x1024)': const Size(768, 1024), 'Expanded Tablet Landscape (1024x768)': const Size(1024, 768), }; group('GroupRidesHubScreen Responsive Viewport Matrix', () { for (final entry in viewports.entries) { testWidgets('Renders cleanly without overflow on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: testUser); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(testUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(testProfile), ), upcomingGroupRidesProvider.overrideWith( (ref) => Stream.value([testRide]), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const GroupRidesHubScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('GROUP RIDES'), findsOneWidget); expect(tester.takeException(), isNull); }); } }); group('CreateGroupRideScreen Responsive Matrix', () { for (final entry in [ const MapEntry('Compact (400x800)', Size(400, 800)), const MapEntry('Short Landscape (800x360)', Size(800, 360)), const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)), ]) { testWidgets('Renders cleanly on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: testUser); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(testUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(testProfile), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('CREATE GROUP RIDE'), findsOneWidget); expect(find.text('1. RIDE DETAILS'), findsOneWidget); expect(tester.takeException(), isNull); }); } }); group('GroupRideDetailScreen Responsive Matrix', () { for (final entry in [ const MapEntry('Compact (400x800)', Size(400, 800)), const MapEntry('Short Landscape (800x360)', Size(800, 360)), const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)), ]) { testWidgets('Renders 2-column on wide and details cleanly on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: testUser); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(testUser), groupRideStreamProvider('ride_101').overrideWith( (ref) => Stream.value(testRide), ), groupRideParticipantsProvider('ride_101').overrideWith( (ref) => Stream.value(testParticipants), ), groupRideLivePresenceProvider('ride_101').overrideWith( (ref) => Stream.value(testPresences), ), myRiderProfileProvider.overrideWith( (ref) => Stream.value(testProfile), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const GroupRideDetailScreen(rideId: 'ride_101'), ), ), ); await tester.pumpAndSettle(); expect(find.text('Blue Ridge Twisties Run'), findsOneWidget); expect(tester.takeException(), isNull); }); } testWidgets( 'Attach Saved Route sheet empty state does not overflow in landscape (800x360) with keyboard inset and portrait (#75)', (tester) async { addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); addTearDown(tester.view.resetViewInsets); final fakeAuth = _FakeAuth(user: testUser); for (final viewportSize in const [ Size(800, 360), Size(640, 320), Size(400, 800), ]) { final isLandscape = viewportSize.width > viewportSize.height; tester.view.physicalSize = viewportSize; tester.view.devicePixelRatio = 1.0; tester.view.resetViewInsets(); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(testUser), groupRideStreamProvider('ride_101').overrideWith( (ref) => Stream.value(testRide), ), groupRideParticipantsProvider('ride_101').overrideWith( (ref) => Stream.value(testParticipants), ), groupRideLivePresenceProvider('ride_101').overrideWith( (ref) => Stream.value(testPresences), ), myRiderProfileProvider.overrideWith( (ref) => Stream.value(testProfile), ), userRoutesProvider('uid_lead').overrideWith( (ref) => Stream.value(const []), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const GroupRideDetailScreen(rideId: 'ride_101'), ), ), ); await tester.pumpAndSettle(); final attachBtn = find.byKey(const ValueKey('attach_saved_route_button')); await tester.scrollUntilVisible( attachBtn, 150.0, scrollable: find.byType(Scrollable).first, ); await tester.pumpAndSettle(); await tester.tap(attachBtn); await tester.pump(); expect(tester.takeException(), isNull); await tester.pump(const Duration(milliseconds: 300)); expect(tester.takeException(), isNull); expect(find.text('ATTACH SAVED ROUTE'), findsWidgets); expect(find.text('You have no saved routes yet.'), findsOneWidget); expect(find.text('CREATE A ROUTE'), findsOneWidget); final createBtnRect = tester.getRect(find.widgetWithText(ElevatedButton, 'CREATE A ROUTE')); expect( createBtnRect.bottom, lessThanOrEqualTo(viewportSize.height), reason: 'CREATE A ROUTE button should fit inside $viewportSize', ); if (isLandscape) { tester.view.viewInsets = const FakeViewPadding(bottom: 180); await tester.pump(); expect(tester.takeException(), isNull); await tester.pump(const Duration(milliseconds: 200)); expect(tester.takeException(), isNull); await tester.ensureVisible(find.text('CREATE A ROUTE')); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); final createBtnKeyboardRect = tester .getRect(find.widgetWithText(ElevatedButton, 'CREATE A ROUTE')); expect( createBtnKeyboardRect.bottom, lessThanOrEqualTo(viewportSize.height - 180 + 1.0), reason: 'CREATE A ROUTE should remain above keyboard at $viewportSize', ); } // Dismiss sheet Navigator.of(tester.element(find.text('CREATE A ROUTE'))).pop(); await tester.pumpAndSettle(); } }); }); group('LiveGroupRideScreen Responsive Matrix', () { for (final entry in [ const MapEntry('Compact (400x800)', Size(400, 800)), const MapEntry('Short Landscape (800x360)', Size(800, 360)), const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)), ]) { testWidgets('Renders map, status bar, and controls on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: testUser); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(testUser), groupRideStreamProvider('ride_101').overrideWith( (ref) => Stream.value(testRide), ), groupRideParticipantsProvider('ride_101').overrideWith( (ref) => Stream.value(testParticipants), ), groupRideLivePresenceProvider('ride_101').overrideWith( (ref) => Stream.value(testPresences), ), liveBroadcastControllerProvider('ride_101').overrideWith( (ref) => _FakeLiveBroadcastController( const LiveBroadcastState( isOptedIn: true, isBroadcasting: true, status: LivePresenceStatus.riding, ), ), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const LiveGroupRideScreen(rideId: 'ride_101'), ), ), ); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('live_ride_broadcast_switch')), findsOneWidget); expect(find.byKey(const ValueKey('live_ride_recenter_button')), findsOneWidget); expect(tester.takeException(), isNull); }); } }); group('RiderProfileScreen & FriendsScreen Responsive Matrix', () { for (final entry in [ const MapEntry('Compact (400x800)', Size(400, 800)), const MapEntry('Wide (1024x768)', Size(1024, 768)), ]) { testWidgets('RiderProfileScreen renders on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: testUser); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(testUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(testProfile), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const RiderProfileScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('Rider Profile'), findsOneWidget); expect(find.text('PRIVACY CONTROLS'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('FriendsScreen renders on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: testUser); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(testUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(testProfile), ), myFriendsProvider.overrideWith( (ref) => Stream.value(testFriends), ), myIncomingRequestsProvider.overrideWith( (ref) => Stream.value(testIncomingRequests), ), myOutgoingRequestsProvider.overrideWith( (ref) => Stream.value([]), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const FriendsScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('Friends & Riders'), findsOneWidget); expect(tester.takeException(), isNull); }); } }); }