import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/services/social/rider_profile_models.dart'; import 'package:motorcycle_dash/services/social/rider_profile_service.dart'; void main() { group('RiderProfile Models', () { test('PresetAvatar contains valid presets and resolves default', () { expect(PresetAvatar.availablePresets.isNotEmpty, isTrue); final first = PresetAvatar.availablePresets.first; expect(PresetAvatar.byId(first.id).id, equals(first.id)); expect(PresetAvatar.byId('unknown_nonexistent').id, equals(first.id)); }); test('ProfilePrivacySettings serialization and defaults', () { const defaults = ProfilePrivacySettings(); expect(defaults.isSearchableByHandle, isTrue); expect(defaults.isProfilePublic, isTrue); expect(defaults.allowFriendRequests, isTrue); final json = defaults.toJson(); final restored = ProfilePrivacySettings.fromJson(json); expect(restored, equals(defaults)); final modified = defaults.copyWith(isProfilePublic: false); expect(modified.isProfilePublic, isFalse); expect(modified.isSearchableByHandle, isTrue); expect(modified.allowFriendRequests, isTrue); }); test('RiderProfile serialization round-trip', () { final now = DateTime(2026, 9, 3, 12, 0, 0); final profile = RiderProfile( uid: 'user_123', handle: 'apex_pilot', displayName: 'Apex Pilot', bio: 'Weekend track and mountain rider.', region: 'North Georgia Mountains', avatarPreset: 'helmet_cyan', avatarUrl: null, privacySettings: const ProfilePrivacySettings( isSearchableByHandle: true, isProfilePublic: false, allowFriendRequests: true, ), createdAt: now, updatedAt: now, ); final json = profile.toJson(); expect(json['id'], equals('user_123')); expect(json['username'], equals('apex_pilot')); expect(json['displayName'], equals('Apex Pilot')); expect(json['bio'], equals('Weekend track and mountain rider.')); expect(json['location'], equals('North Georgia Mountains')); expect(json['avatarPreset'], equals('helmet_cyan')); final restored = RiderProfile.fromJson(json); expect(restored.uid, equals(profile.uid)); expect(restored.handle, equals(profile.handle)); expect(restored.displayName, equals(profile.displayName)); expect(restored.bio, equals(profile.bio)); expect(restored.region, equals(profile.region)); expect(restored.avatarPreset, equals(profile.avatarPreset)); expect(restored.privacySettings.isProfilePublic, isFalse); expect(restored.displayHandle, equals('@apex_pilot')); // Public projection test final publicJson = profile.toPublicProfileJson(); expect(publicJson['id'], equals('user_123')); expect(publicJson['username'], equals('apex_pilot')); expect(publicJson['displayName'], equals('Apex Pilot')); expect(publicJson['bio'], equals('Weekend track and mountain rider.')); expect(publicJson['location'], equals('North Georgia Mountains')); expect(publicJson['avatarPreset'], equals('helmet_cyan')); expect(publicJson['isSearchableByHandle'], isTrue); expect(publicJson['isProfilePublic'], isFalse); expect(publicJson['allowFriendRequests'], isTrue); // Ensure private keys are never included expect(publicJson.containsKey('email'), isFalse); expect(publicJson.containsKey('rides'), isFalse); final fromPublic = RiderProfile.fromPublicProfileJson(publicJson); expect(fromPublic.uid, equals('user_123')); expect(fromPublic.handle, equals('apex_pilot')); expect(fromPublic.displayName, equals('Apex Pilot')); expect(fromPublic.privacySettings.isProfilePublic, isFalse); }); test('FriendSummary serialization round-trip', () { final now = DateTime(2026, 9, 3, 14, 30); final summary = FriendSummary( uid: 'user_456', handle: 'track_demon', displayName: 'Track Demon', avatarPreset: 'tach_neon', friendedAt: now, ); final json = summary.toJson(); final restored = FriendSummary.fromJson(json); expect(restored.uid, equals('user_456')); expect(restored.handle, equals('track_demon')); expect(restored.displayHandle, equals('@track_demon')); expect(restored.avatarPreset, equals('tach_neon')); }); test('FriendRequest serialization round-trip', () { final now = DateTime(2026, 9, 3, 15, 0); final request = FriendRequest( requestId: 'req_001', senderUid: 'sender_01', senderHandle: 'sender_pilot', senderDisplayName: 'Sender Pilot', senderAvatarPreset: 'helmet_amber', receiverUid: 'receiver_02', sentAt: now, ); final json = request.toJson(); final restored = FriendRequest.fromJson(json); expect(restored.requestId, equals('req_001')); expect(restored.senderUid, equals('sender_01')); expect(restored.receiverUid, equals('receiver_02')); }); test('BlockedRider serialization round-trip', () { final now = DateTime(2026, 9, 3, 16, 0); final block = BlockedRider( blockedUid: 'spammer_99', blockedHandle: 'spammer', blockedDisplayName: 'Bad Actor', blockedAt: now, ); final json = block.toJson(); final restored = BlockedRider.fromJson(json); expect(restored.blockedUid, equals('spammer_99')); expect(restored.blockedHandle, equals('spammer')); }); }); group('RiderProfileService validation and offline fallback', () { test('rejects invalid handles on saveProfile', () async { final service = RiderProfileService(); expect( () => service.saveProfile( uid: 'uid_1', handle: 'no', // too short displayName: 'Rider', ), throwsA(isA()), ); expect( () => service.saveProfile( uid: 'uid_1', handle: 'admin', // reserved displayName: 'Admin User', ), throwsA(isA()), ); }); test('creates offline profile when Firestore is unconfigured', () async { final service = RiderProfileService(); final profile = await service.saveProfile( uid: 'uid_local', handle: 'local_pilot', displayName: 'Local Pilot', bio: 'Local solo rider', ); expect(profile.uid, equals('uid_local')); expect(profile.handle, equals('local_pilot')); expect(profile.displayName, equals('Local Pilot')); expect(profile.bio, equals('Local solo rider')); }); }); group('RiderProfileService handle transaction', () { RiderProfile existingProfile(String handle) { final timestamp = DateTime(2026, 9, 7, 10); return RiderProfile( uid: 'uid_1', handle: handle, displayName: 'Original Rider', createdAt: timestamp, updatedAt: timestamp, ); } test('updates an unchanged handle without rewriting its reservation', () async { final firestore = FakeFirebaseFirestore(); final original = existingProfile('apex_rider'); await firestore.collection('users').doc(original.uid).set(original.toJson()); await firestore.collection('handles').doc(original.handle).set({ 'handle': original.handle, 'uid': original.uid, 'createdAt': 'original-reservation', }); final service = RiderProfileService(firestore: firestore); await service.saveProfile( uid: original.uid, handle: original.handle, displayName: 'Updated Rider', ); final handle = await firestore.collection('handles').doc(original.handle).get(); final privateProfile = await firestore.collection('users').doc(original.uid).get(); final publicProfile = await firestore.collection('public_profiles').doc(original.uid).get(); expect(handle.data()?['createdAt'], 'original-reservation'); expect(privateProfile.data()?['displayName'], 'Updated Rider'); expect(publicProfile.data()?['displayName'], 'Updated Rider'); }); test('atomically reserves a changed handle and releases the old handle', () async { final firestore = FakeFirebaseFirestore(); final original = existingProfile('old_handle'); await firestore.collection('users').doc(original.uid).set(original.toJson()); await firestore.collection('handles').doc(original.handle).set({ 'handle': original.handle, 'uid': original.uid, }); final service = RiderProfileService(firestore: firestore); await service.saveProfile( uid: original.uid, handle: 'new_handle', displayName: 'Updated Rider', ); final oldHandle = await firestore.collection('handles').doc('old_handle').get(); final newHandle = await firestore.collection('handles').doc('new_handle').get(); final privateProfile = await firestore.collection('users').doc(original.uid).get(); final publicProfile = await firestore.collection('public_profiles').doc(original.uid).get(); expect(oldHandle.exists, isFalse); expect(newHandle.data()?['uid'], original.uid); expect(privateProfile.data()?['username'], 'new_handle'); expect(publicProfile.data()?['username'], 'new_handle'); }); test('collision leaves the old reservation and both profiles unchanged', () async { final firestore = FakeFirebaseFirestore(); final original = existingProfile('old_handle'); await firestore.collection('users').doc(original.uid).set(original.toJson()); await firestore.collection('public_profiles').doc(original.uid).set(original.toPublicProfileJson()); await firestore.collection('handles').doc(original.handle).set({ 'handle': original.handle, 'uid': original.uid, }); await firestore.collection('handles').doc('reserved_handle').set({ 'handle': 'reserved_handle', 'uid': 'other_uid', }); final service = RiderProfileService(firestore: firestore); await expectLater( service.saveProfile( uid: original.uid, handle: 'reserved_handle', displayName: 'Should Not Persist', ), throwsA(isA()), ); final oldHandle = await firestore.collection('handles').doc('old_handle').get(); final reservedHandle = await firestore.collection('handles').doc('reserved_handle').get(); final privateProfile = await firestore.collection('users').doc(original.uid).get(); final publicProfile = await firestore.collection('public_profiles').doc(original.uid).get(); expect(oldHandle.data()?['uid'], original.uid); expect(reservedHandle.data()?['uid'], 'other_uid'); expect(privateProfile.data()?['username'], 'old_handle'); expect(publicProfile.data()?['username'], 'old_handle'); }); }); }