import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/services/group_rides/group_ride_models.dart'; void main() { group('GroupRideLocation Model Tests', () { test('serializes and deserializes meeting location correctly', () { const loc = GroupRideLocation( latitude: 34.5378, longitude: -83.9849, displayName: 'Dahlonega Square', address: '123 Main St, Dahlonega, GA', locationType: GroupRideLocationType.meetup, notes: 'Meet near north corner', stopDurationMinutes: 15, ); final json = loc.toJson(); expect(json['latitude'], equals(34.5378)); expect(json['longitude'], equals(-83.9849)); expect(json['displayName'], equals('Dahlonega Square')); expect(json['locationType'], equals('meetup')); expect(json['stopDurationMinutes'], equals(15)); final restored = GroupRideLocation.fromJson(json); expect(restored, equals(loc)); }); test('supports intermediate stops and location types', () { expect(GroupRideLocationType.fuel.label, equals('Fuel Stop')); expect(GroupRideLocationType.scenic.label, equals('Scenic Overlook')); expect(GroupRideLocationType.food.label, equals('Food & Drink')); }); }); group('GroupRide Model Tests', () { test('serializes and deserializes full group ride', () { final now = DateTime(2026, 9, 3, 10, 0); final start = DateTime(2026, 9, 10, 9, 0); final departure = DateTime(2026, 9, 10, 9, 30); final ride = GroupRide( id: 'ride_101', organizerUid: 'alice', organizerHandle: 'alice_apex', organizerDisplayName: 'Alice Apex', title: 'Blood Mountain Run', description: 'Twisty mountain loop.', visibility: GroupRideVisibility.friendsOnly, joinPolicy: GroupRideJoinPolicy.inviteOnly, status: GroupRideStatus.scheduled, scheduledStartAt: start, departureAt: departure, participantLimit: 8, participantCount: 3, meetingPoint: const GroupRideLocation( latitude: 34.5, longitude: -83.9, displayName: 'Meetup Chevron', locationType: GroupRideLocationType.meetup, ), destination: const GroupRideLocation( latitude: 34.9, longitude: -83.8, displayName: 'Brasstown Bald', locationType: GroupRideLocationType.destination, ), stops: const [ GroupRideLocation( latitude: 34.7, longitude: -83.85, displayName: 'Vogel State Park', locationType: GroupRideLocationType.rest, ), ], createdAt: now, updatedAt: now, ); final json = ride.toJson(); expect(json['id'], equals('ride_101')); expect(json['organizerUid'], equals('alice')); expect(json['title'], equals('Blood Mountain Run')); expect(json['visibility'], equals('friendsOnly')); expect(json['participantLimit'], equals(8)); expect(json['participantCount'], equals(3)); expect((json['stops'] as List).length, equals(1)); final restored = GroupRide.fromJson(json); expect(restored.id, equals(ride.id)); expect(restored.organizerUid, equals(ride.organizerUid)); expect(restored.title, equals(ride.title)); expect(restored.visibility, equals(GroupRideVisibility.friendsOnly)); expect(restored.isFull, isFalse); expect(restored.isCanceled, isFalse); expect(restored.stops.length, equals(1)); expect(restored.stops.first.displayName, equals('Vogel State Park')); }); test('participant capacity check respects limit', () { final now = DateTime.now(); final ride = GroupRide( id: 'ride_full', organizerUid: 'alice', organizerHandle: 'alice_apex', organizerDisplayName: 'Alice', title: 'Tight Crew', scheduledStartAt: now, departureAt: now, participantLimit: 2, participantCount: 2, meetingPoint: const GroupRideLocation( latitude: 0, longitude: 0, displayName: 'Base', ), createdAt: now, updatedAt: now, ); expect(ride.isFull, isTrue); }); }); group('GroupRideParticipant Model Tests', () { test('serializes and deserializes participant', () { final now = DateTime(2026, 9, 3, 12, 0); final participant = GroupRideParticipant( userId: 'bob', role: GroupRideRole.participant, status: ParticipantStatus.accepted, riderHandle: 'bob_rider', riderDisplayName: 'Bob Rider', riderAvatarPreset: 'helmet_amber', invitedBy: 'alice', createdAt: now, updatedAt: now, ); final json = participant.toJson(); expect(json['userId'], equals('bob')); expect(json['role'], equals('participant')); expect(json['status'], equals('accepted')); expect(json['riderHandle'], equals('bob_rider')); expect(json['invitedBy'], equals('alice')); final restored = GroupRideParticipant.fromJson(json); expect(restored.userId, equals('bob')); expect(restored.status.isActiveMember, isTrue); expect(restored.displayHandle, equals('@bob_rider')); }); }); group('LiveGroupPresence and Ride Lifecycle Tests', () { test('verifies GroupRideStatus underway and isLiveEligible semantics', () { expect(GroupRideStatus.underway.label, equals('Underway')); final now = DateTime.now(); final rideScheduled = GroupRide( id: 'r1', organizerUid: 'alice', organizerHandle: 'alice', organizerDisplayName: 'Alice', title: 'Run', status: GroupRideStatus.scheduled, scheduledStartAt: now, departureAt: now, meetingPoint: const GroupRideLocation(latitude: 0, longitude: 0, displayName: 'Point'), createdAt: now, updatedAt: now, ); expect(rideScheduled.isLiveEligible, isTrue); expect(rideScheduled.isUnderway, isFalse); final rideUnderway = rideScheduled.copyWith(status: GroupRideStatus.underway); expect(rideUnderway.isUnderway, isTrue); expect(rideUnderway.isLiveEligible, isTrue); final rideCompleted = rideScheduled.copyWith(status: GroupRideStatus.completed); expect(rideCompleted.isLiveEligible, isFalse); expect(rideCompleted.isUnderway, isFalse); final rideCanceled = rideScheduled.copyWith(status: GroupRideStatus.canceled); expect(rideCanceled.isLiveEligible, isFalse); }); test('serializes and deserializes LiveGroupPresence correctly', () { final now = DateTime.now(); final expires = now.add(const Duration(seconds: 120)); final presence = LiveGroupPresence( userId: 'bob', groupRideId: 'ride_101', latitude: 34.5378, longitude: -83.9849, heading: 180.5, speedMph: 48.2, accuracyMeters: 4.5, updatedAt: now, expiresAt: expires, sharingEnabled: true, status: LivePresenceStatus.riding, ); final json = presence.toJson(); expect(json['userId'], equals('bob')); expect(json['groupRideId'], equals('ride_101')); expect(json['latitude'], equals(34.5378)); expect(json['longitude'], equals(-83.9849)); expect(json['heading'], equals(180.5)); expect(json['speedMph'], equals(48.2)); expect(json['accuracyMeters'], equals(4.5)); expect(json['sharingEnabled'], isTrue); expect(json['status'], equals('riding')); final restored = LiveGroupPresence.fromJson(json); expect(restored.userId, equals('bob')); expect(restored.groupRideId, equals('ride_101')); expect(restored.latitude, equals(34.5378)); expect(restored.longitude, equals(-83.9849)); expect(restored.status, equals(LivePresenceStatus.riding)); expect(restored.isValidCoordinates, isTrue); expect(restored.isAccurate, isTrue); expect(restored.isFresh, isTrue); expect(restored.isStale, isFalse); }); test('coordinate bounds validation rejects invalid coordinates', () { final now = DateTime.now(); final invalidLat = LiveGroupPresence( userId: 'bob', groupRideId: 'r1', latitude: 91.0, longitude: 0.0, updatedAt: now, expiresAt: now.add(const Duration(seconds: 120)), ); expect(invalidLat.isValidCoordinates, isFalse); final invalidLng = LiveGroupPresence( userId: 'bob', groupRideId: 'r1', latitude: 0.0, longitude: 185.0, updatedAt: now, expiresAt: now.add(const Duration(seconds: 120)), ); expect(invalidLng.isValidCoordinates, isFalse); }); test('freshness and staleness calculation respects timestamp and sharing flag', () { final now = DateTime.now(); final fresh = LiveGroupPresence( userId: 'bob', groupRideId: 'r1', latitude: 35.0, longitude: -84.0, updatedAt: now.subtract(const Duration(seconds: 10)), expiresAt: now.add(const Duration(seconds: 110)), sharingEnabled: true, ); expect(fresh.isFresh, isTrue); expect(fresh.isStale, isFalse); final stale = LiveGroupPresence( userId: 'bob', groupRideId: 'r1', latitude: 35.0, longitude: -84.0, updatedAt: now.subtract(const Duration(seconds: 130)), expiresAt: now.subtract(const Duration(seconds: 10)), sharingEnabled: true, ); expect(stale.isFresh, isFalse); expect(stale.isStale, isTrue); final disabled = fresh.copyWith(sharingEnabled: false); expect(disabled.isFresh, isFalse); expect(disabled.isStale, isTrue); }); }); }