import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/core/theme/theme.dart'; import 'package:motorcycle_dash/features/group_rides/group_ride_detail_screen.dart'; import 'package:motorcycle_dash/features/records/records_service.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/clubs/club_models.dart'; import 'package:motorcycle_dash/services/clubs/club_providers.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/group_ride_service.dart'; import 'package:motorcycle_dash/services/social/rider_profile_models.dart'; import 'package:motorcycle_dash/services/social/social_providers.dart'; class _FakeAuth implements AuthService { final AuthUser? user; _FakeAuth({this.user}); @override Stream get authStateChanges => Stream.value( user != null ? AuthState.authenticated(user!, isFirebaseConfigured: true) : const AuthState.unauthenticated(isFirebaseConfigured: true), ); @override AuthState get currentState => user != null ? AuthState.authenticated(user!, isFirebaseConfigured: true) : const AuthState.unauthenticated(isFirebaseConfigured: true); @override AuthUser? get currentUser => user; @override bool get isConfigured => true; @override bool get isEmailVerified => true; @override Future getIdToken([bool forceRefresh = false]) async => 'mock-id-token'; @override Future signInWithEmailAndPassword(String email, String password) async {} @override Future createUserWithEmailAndPassword(String email, String password, {String? displayName}) async {} @override Future signInWithGoogle() async {} @override Future signInAnonymously() async {} @override Future ensureBackendIdentity() async {} @override Future sendPasswordResetEmail(String email) async {} @override Future signOut() async {} @override void dispose() {} @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _RecordingGroupRideService extends Fake implements GroupRideService { String? lastModeratedCallerUid; String? lastModeratedGroupRideId; String? lastModeratedRequesterUid; bool? lastModeratedApprove; @override Future respondToJoinRequest({ required String callerUid, required String groupRideId, required bool approve, required String requesterUid, }) async { lastModeratedCallerUid = callerUid; lastModeratedGroupRideId = groupRideId; lastModeratedRequesterUid = requesterUid; lastModeratedApprove = approve; } @override Stream participantDocStream({ required String groupRideId, required String participantUid, }) => Stream.value(null); @override Stream groupRideStream(String groupRideId) => Stream.value(null); @override Stream livePresenceDocStream({ required String groupRideId, required String targetUid, }) => Stream.value(null); @override Future publishLivePresence({ required String callerUid, required String groupRideId, required double latitude, required double longitude, double? heading, double? speedMph, double? accuracyMeters, LivePresenceStatus status = LivePresenceStatus.riding, bool sharingEnabled = true, }) async {} @override Future clearLivePresence({ required String callerUid, required String groupRideId, }) async {} } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late SharedPreferences prefs; final now = DateTime(2026, 9, 24, 10, 0); final organizerUser = const AuthUser( uid: 'organizer_1', email: 'organizer@example.com', displayName: 'Alice Apex', isAnonymous: false, ); final adminUser = const AuthUser( uid: 'admin_1', email: 'admin@example.com', displayName: 'Charlie Admin', isAnonymous: false, ); final regularUser = const AuthUser( uid: 'rider_1', email: 'rider@example.com', displayName: 'Bob Rider', isAnonymous: false, ); final testRide = GroupRide( id: 'ride_101', organizerUid: 'organizer_1', organizerHandle: 'alice_apex', organizerDisplayName: 'Alice Apex', title: 'Blue Ridge Twisties', description: 'Scenic group ride through Blood Mountain.', status: GroupRideStatus.scheduled, joinPolicy: GroupRideJoinPolicy.requestToJoin, scheduledStartAt: now.add(const Duration(days: 1)), departureAt: now.add(const Duration(days: 1, minutes: 30)), participantLimit: 10, participantCount: 1, accessUids: const ['organizer_1'], meetingPoint: const GroupRideLocation( latitude: 34.5, longitude: -84.0, displayName: 'Dahlonega Square', ), clubId: 'club_77', createdAt: now, updatedAt: now, ); final organizerParticipant = GroupRideParticipant( userId: 'organizer_1', role: GroupRideRole.organizer, status: ParticipantStatus.accepted, riderDisplayName: 'Alice Apex', riderHandle: 'alice_apex', createdAt: now, updatedAt: now, ); final testProfile = RiderProfile( uid: 'organizer_1', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: now, updatedAt: now, ); final pendingRequest = GroupRideJoinRequest( userId: 'requester_42', riderDisplayName: 'Dave Curve', riderHandle: 'dave_curves', createdAt: now, ); setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); }); Widget buildScreen({ required AuthUser user, required GroupRide ride, required _RecordingGroupRideService mockService, AsyncValue>? joinRequestsValue, ClubMember? clubMember, }) { return ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(_FakeAuth(user: user)), currentUserProvider.overrideWithValue(user), groupRideServiceProvider.overrideWithValue(mockService), groupRideStreamProvider(ride.id).overrideWith((ref) => Stream.value(ride)), groupRideParticipantsProvider(ride.id).overrideWith((ref) => Stream.value([organizerParticipant])), groupRideLivePresenceProvider(ride.id).overrideWith((ref) => Stream.value(const [])), myRiderProfileProvider.overrideWith((ref) => Stream.value(testProfile)), myFriendsProvider.overrideWith((ref) => Stream.value(const [])), if (ride.clubId != null) currentMemberProvider(ride.clubId!).overrideWith((ref) => Stream.value(clubMember)), if (joinRequestsValue != null) groupRideJoinRequestsProvider(ride.id).overrideWith((ref) => joinRequestsValue.value ?? const []) else groupRideJoinRequestsProvider(ride.id).overrideWith((ref) => [pendingRequest]), ], child: MaterialApp( theme: AppTheme.darkTheme, home: GroupRideDetailScreen(rideId: ride.id), ), ); } group('GroupRideDetailScreen Join Request Moderation UI Tests', () { testWidgets('organizer sees JOIN REQUESTS section and pending request details', (tester) async { final mockService = _RecordingGroupRideService(); await tester.pumpWidget( buildScreen( user: organizerUser, ride: testRide, mockService: mockService, ), ); await tester.pumpAndSettle(); expect(find.text('JOIN REQUESTS (1)'), findsOneWidget); expect(find.text('Dave Curve'), findsOneWidget); expect(find.text('@dave_curves'), findsOneWidget); expect(find.byIcon(Icons.check), findsOneWidget); expect(find.byIcon(Icons.close), findsOneWidget); expect(find.byIcon(Icons.refresh), findsOneWidget); }); testWidgets('organizer approves join request invokes service and triggers feedback', (tester) async { final mockService = _RecordingGroupRideService(); await tester.pumpWidget( buildScreen( user: organizerUser, ride: testRide, mockService: mockService, ), ); await tester.pumpAndSettle(); final approveButton = find.byTooltip('Approve request'); expect(approveButton, findsOneWidget); await tester.tap(approveButton); await tester.pump(); expect(mockService.lastModeratedCallerUid, equals('organizer_1')); expect(mockService.lastModeratedGroupRideId, equals('ride_101')); expect(mockService.lastModeratedRequesterUid, equals('requester_42')); expect(mockService.lastModeratedApprove, isTrue); await tester.pumpAndSettle(); expect(find.text('Approved Dave Curve to join the ride.'), findsOneWidget); }); testWidgets('organizer declines join request invokes service with approve: false', (tester) async { final mockService = _RecordingGroupRideService(); await tester.pumpWidget( buildScreen( user: organizerUser, ride: testRide, mockService: mockService, ), ); await tester.pumpAndSettle(); final declineButton = find.byTooltip('Decline request'); expect(declineButton, findsOneWidget); await tester.tap(declineButton); await tester.pump(); expect(mockService.lastModeratedCallerUid, equals('organizer_1')); expect(mockService.lastModeratedGroupRideId, equals('ride_101')); expect(mockService.lastModeratedRequesterUid, equals('requester_42')); expect(mockService.lastModeratedApprove, isFalse); await tester.pumpAndSettle(); expect(find.text('Declined join request from Dave Curve.'), findsOneWidget); }); testWidgets('club admin who is not organizer sees moderation controls', (tester) async { final mockService = _RecordingGroupRideService(); final adminMember = ClubMember( uid: adminUser.uid, clubId: testRide.clubId!, status: ClubMemberStatus.active, role: ClubRole.admin, joinedAt: now, updatedAt: now, ); await tester.pumpWidget( buildScreen( user: adminUser, ride: testRide, mockService: mockService, clubMember: adminMember, ), ); await tester.pumpAndSettle(); expect(find.text('JOIN REQUESTS (1)'), findsOneWidget); expect(find.text('Dave Curve'), findsOneWidget); expect(find.byTooltip('Approve request'), findsOneWidget); expect(find.byTooltip('Decline request'), findsOneWidget); }); testWidgets('regular rider does not see JOIN REQUESTS section', (tester) async { final mockService = _RecordingGroupRideService(); final regularMember = ClubMember( uid: regularUser.uid, clubId: testRide.clubId!, status: ClubMemberStatus.active, role: ClubRole.member, joinedAt: now, updatedAt: now, ); await tester.pumpWidget( buildScreen( user: regularUser, ride: testRide, mockService: mockService, clubMember: regularMember, ), ); await tester.pumpAndSettle(); expect(find.textContaining('JOIN REQUESTS'), findsNothing); expect(find.byTooltip('Approve request'), findsNothing); expect(find.byTooltip('Decline request'), findsNothing); }); testWidgets('empty requests list does not render section header', (tester) async { final mockService = _RecordingGroupRideService(); await tester.pumpWidget( buildScreen( user: organizerUser, ride: testRide, mockService: mockService, joinRequestsValue: const AsyncValue.data([]), ), ); await tester.pumpAndSettle(); expect(find.textContaining('JOIN REQUESTS'), findsNothing); }); testWidgets('displays error message and retry button on fetch failure', (tester) async { final mockService = _RecordingGroupRideService(); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(_FakeAuth(user: organizerUser)), currentUserProvider.overrideWithValue(organizerUser), groupRideServiceProvider.overrideWithValue(mockService), groupRideStreamProvider(testRide.id).overrideWith((ref) => Stream.value(testRide)), groupRideParticipantsProvider(testRide.id).overrideWith((ref) => Stream.value([organizerParticipant])), groupRideLivePresenceProvider(testRide.id).overrideWith((ref) => Stream.value(const [])), myRiderProfileProvider.overrideWith((ref) => Stream.value(testProfile)), myFriendsProvider.overrideWith((ref) => Stream.value(const [])), groupRideJoinRequestsProvider(testRide.id).overrideWith( (ref) => Future.error(StateError('Server connection error')), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: GroupRideDetailScreen(rideId: testRide.id), ), ), ); await tester.pumpAndSettle(); expect(find.text('Unable to load join requests.'), findsOneWidget); expect(find.text('Retry'), findsOneWidget); }); testWidgets('canceled ride hides JOIN REQUESTS section even for organizer', (tester) async { final mockService = _RecordingGroupRideService(); final canceledRide = testRide.copyWith(status: GroupRideStatus.canceled); await tester.pumpWidget( buildScreen( user: organizerUser, ride: canceledRide, mockService: mockService, ), ); await tester.pumpAndSettle(); expect(find.textContaining('JOIN REQUESTS'), findsNothing); }); }); }