import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:siro_maps/core/services/device_fingerprint_service.dart'; import 'package:siro_maps/core/services/telemetry_tracking_service.dart'; import 'package:siro_maps/data/repositories/map_saas_repository.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late TelemetryTrackingService telemetryService; setUp(() { SharedPreferences.setMockInitialValues({}); telemetryService = TelemetryTrackingService.instance; telemetryService.resetForTesting(); DeviceFingerprintService.instance.setMockState( fingerprint: 'siro_test_hw_unit_test_device_123', ); }); group('TelemetryTrackingService Sampling & Stationary Filter Tests', () { test('Samples position only if at least 3 seconds have passed', () { final baseTime = DateTime(2026, 9, 15, 12, 0, 0); // First point at T=0 final p1 = telemetryService.recordPosition( latitude: 31.9539, longitude: 35.9106, speedKmH: 45.0, heading: 90.0, now: baseTime, ); expect(p1, isTrue); expect(telemetryService.bufferedPointCount, equals(1)); // Attempt second point after only 1 second (T=1s) -> Should be rejected final p2 = telemetryService.recordPosition( latitude: 31.9540, longitude: 35.9107, speedKmH: 46.0, heading: 90.0, now: baseTime.add(const Duration(seconds: 1)), ); expect(p2, isFalse); expect(telemetryService.bufferedPointCount, equals(1)); // Attempt third point after 3 seconds (T=3s) -> Should be accepted final p3 = telemetryService.recordPosition( latitude: 31.9545, longitude: 35.9110, speedKmH: 48.0, heading: 92.0, now: baseTime.add(const Duration(seconds: 3)), ); expect(p3, isTrue); expect(telemetryService.bufferedPointCount, equals(2)); }); test('Smart stationary filter suppresses redundant points when vehicle is stopped', () { final baseTime = DateTime(2026, 9, 15, 12, 0, 0); // Stop at intersection: initial point (speed = 0) final initial = telemetryService.recordPosition( latitude: 31.9500, longitude: 35.9100, speedKmH: 0.0, heading: 0.0, now: baseTime, ); expect(initial, isTrue); expect(telemetryService.bufferedPointCount, equals(1)); // After 5 seconds: still at same spot (speed = 0, distance = 0) -> filter out final duplicateAfter5s = telemetryService.recordPosition( latitude: 31.9500, longitude: 35.9100, speedKmH: 0.0, heading: 0.0, now: baseTime.add(const Duration(seconds: 5)), ); expect(duplicateAfter5s, isFalse); expect(telemetryService.bufferedPointCount, equals(1)); // After 35 seconds: still at same spot -> 30s heartbeat triggers! final heartbeatAfter35s = telemetryService.recordPosition( latitude: 31.9500, longitude: 35.9100, speedKmH: 0.0, heading: 0.0, now: baseTime.add(const Duration(seconds: 35)), ); expect(heartbeatAfter35s, isTrue); expect(telemetryService.bufferedPointCount, equals(2)); }); }); group('Telemetry Batch Upload & Offline Buffer Tests', () { test('Flushes batched telemetry to server in a single request', () async { List receivedPayloadPoints = []; final mockClient = MockClient((request) async { if (request.url.path.contains('/api/telemetry/batch')) { final body = jsonDecode(request.body) as Map; receivedPayloadPoints = body['points'] as List; return http.Response(jsonEncode({'success': true, 'count': receivedPayloadPoints.length}), 201); } return http.Response('Not Found', 404); }); final mockRepo = MapSaasRepository(client: mockClient); // Record 3 points spaced by 3 seconds final baseTime = DateTime(2026, 9, 15, 12, 0, 0); telemetryService.recordPosition( latitude: 31.951, longitude: 35.911, speedKmH: 30, heading: 45, now: baseTime, ); telemetryService.recordPosition( latitude: 31.952, longitude: 35.912, speedKmH: 35, heading: 45, now: baseTime.add(const Duration(seconds: 3)), ); telemetryService.recordPosition( latitude: 31.953, longitude: 35.913, speedKmH: 40, heading: 50, now: baseTime.add(const Duration(seconds: 6)), ); expect(telemetryService.bufferedPointCount, equals(3)); // Flush buffer to backend final success = await telemetryService.flush(repository: mockRepo); expect(success, isTrue); expect(receivedPayloadPoints.length, equals(3)); // Buffer should be empty after successful flush expect(telemetryService.bufferedPointCount, equals(0)); }); test('Retains points in offline buffer if batch upload encounters error', () async { final offlineClient = MockClient((request) async { return http.Response('Gateway Timeout', 504); }); final mockRepo = MapSaasRepository(client: offlineClient); telemetryService.recordPosition( latitude: 31.951, longitude: 35.911, speedKmH: 20, heading: 10, now: DateTime(2026, 9, 15, 12, 0, 0), ); expect(telemetryService.bufferedPointCount, equals(1)); final success = await telemetryService.flush(repository: mockRepo); expect(success, isFalse); // Points must still be retained for next attempt expect(telemetryService.bufferedPointCount, equals(1)); }); }); group('10-Day Periodic Updates & Maintenance Tests', () { test('Skips update if less than 10 days since last check', () async { final now = DateTime(2026, 9, 15, 12, 0, 0); final lastCheck = now.subtract(const Duration(days: 4)); // Only 4 days ago SharedPreferences.setMockInitialValues({ TelemetryTrackingService.prefKeyLastPeriodicCheck: lastCheck.millisecondsSinceEpoch, }); final prefs = await SharedPreferences.getInstance(); final executed = await telemetryService.checkPeriodicUpdates( now: now, mockPrefs: prefs, ); expect(executed, isFalse); }); test('Executes maintenance cycle if 10 or more days elapsed', () async { final now = DateTime(2026, 9, 15, 12, 0, 0); final lastCheck = now.subtract(const Duration(days: 12)); // 12 days ago SharedPreferences.setMockInitialValues({ TelemetryTrackingService.prefKeyLastPeriodicCheck: lastCheck.millisecondsSinceEpoch, }); final prefs = await SharedPreferences.getInstance(); final executed = await telemetryService.checkPeriodicUpdates( now: now, mockPrefs: prefs, ); expect(executed, isTrue); final updatedTimestamp = prefs.getInt(TelemetryTrackingService.prefKeyLastPeriodicCheck); expect(updatedTimestamp, equals(now.millisecondsSinceEpoch)); }); }); }