import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/core/theme/theme_extensions.dart'; import 'package:motorcycle_dash/features/social/clubs/club_detail_screen.dart'; import 'package:motorcycle_dash/services/auth/auth_providers.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/clubs/club_service.dart'; class MockClubServiceForDetail extends ClubService { String? lastAction; String? lastClubId; bool? lastAccept; @override Future joinOpenClub(String clubId, {String? callerUid}) async { lastAction = 'joinOpen'; lastClubId = clubId; } @override Future requestToJoinClub(String clubId, {String? callerUid}) async { lastAction = 'requestToJoin'; lastClubId = clubId; } @override Future withdrawJoinRequest(String clubId, {String? callerUid}) async { lastAction = 'withdrawRequest'; lastClubId = clubId; } @override Future respondToInvitation(String clubId, {required bool accept, String? callerUid}) async { lastAction = 'respondInvite'; lastClubId = clubId; lastAccept = accept; } @override Future leaveClub(String clubId, {String? callerUid}) async { lastAction = 'leaveClub'; lastClubId = clubId; } } void main() { group('ClubDetailScreen Widget Lifecycle Tests', () { late MockClubServiceForDetail mockService; const testUser = AuthUser(uid: 'rider_1', isAnonymous: false); final openClub = Club( id: 'club_open', name: 'Open Track Riders', slug: 'open-track', description: 'Track day motorcycle community', ownerUid: 'owner_bob', memberCount: 12, visibility: ClubVisibility.public, admissionPolicy: ClubAdmissionPolicy.open, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final requestClub = Club( id: 'club_request', name: 'Sierra Twisties', slug: 'sierra-twisties', description: 'Mountain pass riding group', ownerUid: 'owner_charlie', memberCount: 5, visibility: ClubVisibility.public, admissionPolicy: ClubAdmissionPolicy.request, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final inviteClub = Club( id: 'club_invite', name: 'Elite Racers', slug: 'elite-racers', description: 'Strictly invite only', ownerUid: 'owner_dan', memberCount: 3, visibility: ClubVisibility.public, admissionPolicy: ClubAdmissionPolicy.inviteOnly, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); setUp(() { mockService = MockClubServiceForDetail(); }); testWidgets('renders Open Admission state and executes Join Club', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_open').overrideWith((ref) => Stream.value(openClub)), currentMemberProvider('club_open').overrideWith((ref) => Stream.value(null)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_open'), ), ), ); await tester.pumpAndSettle(); expect(find.text('OPEN ADMISSION'), findsOneWidget); expect(find.text('Open Membership'), findsOneWidget); final joinButton = find.widgetWithText(FilledButton, 'Join Club'); expect(joinButton, findsOneWidget); await tester.tap(joinButton); await tester.pumpAndSettle(); expect(mockService.lastAction, 'joinOpen'); expect(mockService.lastClubId, 'club_open'); }); testWidgets('renders Request Admission state and executes Request to Join', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_request').overrideWith((ref) => Stream.value(requestClub)), currentMemberProvider('club_request').overrideWith((ref) => Stream.value(null)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_request'), ), ), ); await tester.pumpAndSettle(); expect(find.text('REQUEST REQUIRED'), findsOneWidget); expect(find.text('Request Admission'), findsOneWidget); final requestButton = find.widgetWithText(FilledButton, 'Request to Join'); expect(requestButton, findsOneWidget); await tester.tap(requestButton); await tester.pumpAndSettle(); expect(mockService.lastAction, 'requestToJoin'); expect(mockService.lastClubId, 'club_request'); }); testWidgets('renders Invite-Only state with no join action', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_invite').overrideWith((ref) => Stream.value(inviteClub)), currentMemberProvider('club_invite').overrideWith((ref) => Stream.value(null)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_invite'), ), ), ); await tester.pumpAndSettle(); expect(find.text('Invite-Only Club'), findsOneWidget); expect(find.text('Join Club'), findsNothing); expect(find.text('Request to Join'), findsNothing); }); testWidgets('renders Invited state with Accept and Decline actions', (tester) async { final inviteMember = ClubMember( uid: 'rider_1', clubId: 'club_request', status: ClubMemberStatus.invited, role: ClubRole.member, updatedAt: DateTime(2026, 1, 1), ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_request').overrideWith((ref) => Stream.value(requestClub)), currentMemberProvider('club_request').overrideWith((ref) => Stream.value(inviteMember)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_request'), ), ), ); await tester.pumpAndSettle(); expect(find.text('INVITED'), findsOneWidget); expect(find.text('You Are Invited'), findsOneWidget); expect(find.text('Accept'), findsOneWidget); expect(find.text('Decline'), findsOneWidget); await tester.tap(find.text('Accept')); await tester.pumpAndSettle(); expect(mockService.lastAction, 'respondInvite'); expect(mockService.lastAccept, isTrue); }); testWidgets('renders Requested state with Withdraw Request action', (tester) async { final requestMember = ClubMember( uid: 'rider_1', clubId: 'club_request', status: ClubMemberStatus.requested, role: ClubRole.member, requestedAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_request').overrideWith((ref) => Stream.value(requestClub)), currentMemberProvider('club_request').overrideWith((ref) => Stream.value(requestMember)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_request'), ), ), ); await tester.pumpAndSettle(); expect(find.text('REQUEST PENDING'), findsOneWidget); expect(find.text('Membership Requested'), findsOneWidget); expect(find.text('Withdraw Request'), findsOneWidget); await tester.tap(find.text('Withdraw Request')); await tester.pumpAndSettle(); expect(mockService.lastAction, 'withdrawRequest'); }); testWidgets('renders Active Member state with Leave Club and confirmation dialog', (tester) async { final activeMember = ClubMember( uid: 'rider_1', clubId: 'club_open', status: ClubMemberStatus.active, role: ClubRole.member, joinedAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_open').overrideWith((ref) => Stream.value(openClub)), currentMemberProvider('club_open').overrideWith((ref) => Stream.value(activeMember)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_open'), ), ), ); await tester.pumpAndSettle(); expect(find.text('MEMBER'), findsOneWidget); expect(find.text('Active Member'), findsOneWidget); expect(find.text('Leave Club'), findsOneWidget); // Tap Leave Club to open confirmation dialog await tester.tap(find.text('Leave Club')); await tester.pumpAndSettle(); expect(find.text('Leave Open Track Riders?'), findsOneWidget); expect(find.text('Cancel'), findsOneWidget); // Confirm leaving inside the dialog final confirmButtonFinder = find.widgetWithText(FilledButton, 'Leave Club'); expect(confirmButtonFinder, findsOneWidget); await tester.tap(confirmButtonFinder); await tester.pumpAndSettle(); expect(mockService.lastAction, 'leaveClub'); expect(mockService.lastClubId, 'club_open'); }); testWidgets('renders Owner state with ownership transfer constraint note and no leave button', (tester) async { const ownerUser = AuthUser(uid: 'owner_bob', isAnonymous: false); final ownerMember = ClubMember( uid: 'owner_bob', clubId: 'club_open', status: ClubMemberStatus.active, role: ClubRole.owner, joinedAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => ownerUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_open').overrideWith((ref) => Stream.value(openClub)), currentMemberProvider('club_open').overrideWith((ref) => Stream.value(ownerMember)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_open'), ), ), ); await tester.pumpAndSettle(); expect(find.text('Club Owner'), findsOneWidget); expect(find.textContaining('Ownership must be transferred before leaving'), findsOneWidget); expect(find.text('Leave Club'), findsNothing); }); testWidgets('renders loading state and prevents action button clicks while membership is loading', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_open').overrideWith((ref) => Stream.value(openClub)), currentMemberProvider('club_open').overrideWith((ref) => const Stream.empty()), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_open'), ), ), ); await tester.pump(); expect(find.text('Checking Membership Status...'), findsOneWidget); expect(find.text('Join Club'), findsNothing); expect(find.text('Request to Join'), findsNothing); expect(find.text('Leave Club'), findsNothing); }); testWidgets('renders retryable error state when membership stream errors', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_open').overrideWith((ref) => Stream.value(openClub)), currentMemberProvider('club_open').overrideWith((ref) => Stream.error(Exception('Network error'))), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_open'), ), ), ); await tester.pumpAndSettle(); expect(find.text('Membership Status Unavailable'), findsOneWidget); expect(find.text('Retry'), findsOneWidget); expect(find.text('Join Club'), findsNothing); expect(find.text('Leave Club'), findsNothing); }); testWidgets('renders clean unavailable screen with back navigation on club stream error', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('private_club').overrideWith((ref) => Stream.error(Exception('permission-denied'))), currentMemberProvider('private_club').overrideWith((ref) => const Stream.empty()), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'private_club'), ), ), ); await tester.pumpAndSettle(); expect(find.text('Club Unavailable'), findsOneWidget); expect(find.text('Unable to display this club at this time.'), findsOneWidget); expect(find.text('Back to Clubs'), findsOneWidget); }); testWidgets('renders without overflow on portrait 412x915', (tester) async { tester.view.physicalSize = const Size(412, 915); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_open').overrideWith((ref) => Stream.value(openClub)), currentMemberProvider('club_open').overrideWith((ref) => Stream.value(null)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_open'), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); // Scroll to bottom await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -400)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('renders without overflow on landscape 915x412 including dialog', (tester) async { tester.view.physicalSize = const Size(915, 412); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); final activeMember = ClubMember( uid: 'rider_1', clubId: 'club_open', status: ClubMemberStatus.active, role: ClubRole.member, joinedAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => testUser), clubServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_open').overrideWith((ref) => Stream.value(openClub)), currentMemberProvider('club_open').overrideWith((ref) => Stream.value(activeMember)), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubDetailScreen(clubId: 'club_open'), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); // Open leave confirmation dialog await tester.ensureVisible(find.text('Leave Club')); await tester.pumpAndSettle(); await tester.tap(find.text('Leave Club')); await tester.pumpAndSettle(); expect(find.text('Leave Open Track Riders?'), findsOneWidget); expect(tester.takeException(), isNull); }); }); }