import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/features/social/clubs/club_membership_controller.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_providers.dart'; import 'package:motorcycle_dash/services/clubs/club_service.dart'; class FakeClubServiceForController extends ClubService { String? lastAction; String? lastClubId; String? lastCallerUid; bool? lastAccept; bool shouldThrow = false; String? throwCode; String? throwMessage; Future Function()? onJoinAsync; @override Future joinOpenClub(String clubId, {String? callerUid}) async { if (onJoinAsync != null) { await onJoinAsync!(); } if (shouldThrow) { throw ClubOperationException(throwMessage ?? 'Error', code: throwCode); } lastAction = 'joinOpen'; lastClubId = clubId; lastCallerUid = callerUid; } @override Future requestToJoinClub(String clubId, {String? callerUid}) async { if (shouldThrow) { throw ClubOperationException(throwMessage ?? 'Error', code: throwCode); } lastAction = 'requestToJoin'; lastClubId = clubId; lastCallerUid = callerUid; } @override Future withdrawJoinRequest(String clubId, {String? callerUid}) async { if (shouldThrow) { throw ClubOperationException(throwMessage ?? 'Error', code: throwCode); } lastAction = 'withdrawRequest'; lastClubId = clubId; lastCallerUid = callerUid; } @override Future respondToInvitation(String clubId, {required bool accept, String? callerUid}) async { if (shouldThrow) { throw ClubOperationException(throwMessage ?? 'Error', code: throwCode); } lastAction = 'respondInvite'; lastClubId = clubId; lastAccept = accept; lastCallerUid = callerUid; } @override Future leaveClub(String clubId, {String? callerUid}) async { if (shouldThrow) { throw ClubOperationException(throwMessage ?? 'Error', code: throwCode); } lastAction = 'leaveClub'; lastClubId = clubId; lastCallerUid = callerUid; } } void main() { group('ClubMembershipController Tests', () { late FakeClubServiceForController fakeService; late ProviderContainer container; setUp(() { fakeService = FakeClubServiceForController(); container = ProviderContainer( overrides: [ clubServiceProvider.overrideWithValue(fakeService), currentUserProvider.overrideWith( (ref) => const AuthUser(uid: 'user_rider_1', isAnonymous: false), ), ], ); }); tearDown(() { container.dispose(); }); test('joinOpenClub executes and transitions state to success', () async { final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.joinOpenClub('club_1'); expect(success, isTrue); expect(fakeService.lastAction, 'joinOpen'); expect(fakeService.lastClubId, 'club_1'); expect(fakeService.lastCallerUid, 'user_rider_1'); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.isSubmitting, isFalse); expect(state.success, 'Successfully joined club!'); expect(state.error, isNull); }); test('requestToJoinClub executes and transitions state to success', () async { final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.requestToJoinClub('club_1'); expect(success, isTrue); expect(fakeService.lastAction, 'requestToJoin'); expect(fakeService.lastClubId, 'club_1'); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.isSubmitting, isFalse); expect(state.success, 'Join request submitted to club leadership.'); expect(state.error, isNull); }); test('withdrawJoinRequest executes and transitions state to success', () async { final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.withdrawJoinRequest('club_1'); expect(success, isTrue); expect(fakeService.lastAction, 'withdrawRequest'); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.isSubmitting, isFalse); expect(state.success, 'Join request withdrawn.'); }); test('acceptInvitation executes and transitions state to success', () async { final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.acceptInvitation('club_1'); expect(success, isTrue); expect(fakeService.lastAction, 'respondInvite'); expect(fakeService.lastAccept, isTrue); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.success, contains('Welcome to the club')); }); test('declineInvitation executes and transitions state to success', () async { final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.declineInvitation('club_1'); expect(success, isTrue); expect(fakeService.lastAction, 'respondInvite'); expect(fakeService.lastAccept, isFalse); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.success, 'Invitation declined.'); }); test('leaveClub executes and transitions state to success', () async { final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.leaveClub('club_1'); expect(success, isTrue); expect(fakeService.lastAction, 'leaveClub'); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.success, 'You have left the club.'); }); test('fails gracefully when user is unauthenticated', () async { final unauthContainer = ProviderContainer( overrides: [ clubServiceProvider.overrideWithValue(fakeService), currentUserProvider.overrideWith((ref) => null), ], ); final controller = unauthContainer.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.joinOpenClub('club_1'); expect(success, isFalse); final state = unauthContainer.read(clubMembershipControllerProvider('club_1')); expect(state.error, contains('must be signed in')); unauthContainer.dispose(); }); test('maps banned error code to friendly generic message without leaking mod info', () async { fakeService.shouldThrow = true; fakeService.throwCode = 'banned'; fakeService.throwMessage = 'User is in bans subcollection.'; final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.joinOpenClub('club_1'); expect(success, isFalse); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.error, 'Membership is unavailable for this club.'); }); test('maps owner-cannot-leave error code properly', () async { fakeService.shouldThrow = true; fakeService.throwCode = 'owner-cannot-leave'; fakeService.throwMessage = 'Owner cannot leave.'; final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success = await controller.leaveClub('club_1'); expect(success, isFalse); final state = container.read(clubMembershipControllerProvider('club_1')); expect(state.error, contains('transferring ownership first')); }); test('maps policy-mismatch, not-found, and unavailable error codes', () async { fakeService.shouldThrow = true; fakeService.throwCode = 'policy-mismatch'; fakeService.throwMessage = 'Open join not allowed.'; final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final success1 = await controller.joinOpenClub('club_1'); expect(success1, isFalse); expect(container.read(clubMembershipControllerProvider('club_1')).error, 'This club does not allow open joining. Please submit a request.'); fakeService.throwCode = 'not-found'; fakeService.throwMessage = 'Not found'; final success2 = await controller.joinOpenClub('club_1'); expect(success2, isFalse); expect(container.read(clubMembershipControllerProvider('club_1')).error, 'Club or membership record was not found.'); fakeService.throwCode = 'unavailable'; fakeService.throwMessage = 'Firestore down'; final success3 = await controller.joinOpenClub('club_1'); expect(success3, isFalse); expect(container.read(clubMembershipControllerProvider('club_1')).error, 'Club services are temporarily unavailable. Please try again later.'); }); test('rejects concurrent duplicate mutation while in-flight', () async { final completer = Completer(); fakeService.onJoinAsync = () => completer.future; final controller = container.read(clubMembershipControllerProvider('club_1').notifier); final firstFuture = controller.joinOpenClub('club_1'); expect(container.read(clubMembershipControllerProvider('club_1')).isSubmitting, isTrue); // Attempt second rapid mutation while first is still pending final secondSuccess = await controller.joinOpenClub('club_1'); expect(secondSuccess, isFalse); completer.complete(); final firstSuccess = await firstFuture; expect(firstSuccess, isTrue); expect(container.read(clubMembershipControllerProvider('club_1')).isSubmitting, isFalse); }); test('isolates controller state per club family instance', () async { final controllerA = container.read(clubMembershipControllerProvider('club_A').notifier); final controllerB = container.read(clubMembershipControllerProvider('club_B').notifier); await controllerA.joinOpenClub('club_A'); await controllerB.requestToJoinClub('club_B'); expect(container.read(clubMembershipControllerProvider('club_A')).success, 'Successfully joined club!'); expect(container.read(clubMembershipControllerProvider('club_B')).success, 'Join request submitted to club leadership.'); }); test('clearMessages resets state', () async { final controller = container.read(clubMembershipControllerProvider('club_1').notifier); await controller.joinOpenClub('club_1'); expect(container.read(clubMembershipControllerProvider('club_1')).success, isNotNull); controller.clearMessages(); final cleared = container.read(clubMembershipControllerProvider('club_1')); expect(cleared.success, isNull); expect(cleared.error, isNull); }); }); }