import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/native.dart'; import 'package:drift/drift.dart' as drift; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/core/theme/theme.dart'; import 'package:motorcycle_dash/core/theme/theme_mode_provider.dart'; import 'package:motorcycle_dash/database/database.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/features/settings/diagnostics_screen.dart'; import 'package:motorcycle_dash/features/settings/field_test_live_log_screen.dart'; import 'package:motorcycle_dash/features/settings/field_test_report_screen.dart'; import 'package:motorcycle_dash/features/field_test/field_test_pre_ride_check_screen.dart'; import 'package:motorcycle_dash/services/field_test/field_test_logger.dart'; import 'package:motorcycle_dash/services/telemetry_service.dart'; import 'package:motorcycle_dash/services/github/github_sync_service.dart'; import 'package:geolocator/geolocator.dart'; import 'package:motorcycle_dash/services/location_service.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart' as live_sample; import 'package:motorcycle_dash/models/field_test_event.dart'; class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService { @override GitHubUploadState get state => GitHubUploadState.idle; @override String get statusMessage => ''; @override Set get queuedRideIds => const {}; @override bool isRideUploaded(int rideId) => false; @override String? getRideCommitSha(int rideId) => null; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeLocationService extends ChangeNotifier implements LocationService { @override Future requestPermissions() async => true; @override Future hasPermissions() async => true; @override void Function(Position, bool, String?)? onGpsDiagnostic; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late AppDatabase db; late SharedPreferences prefs; setUp(() async { db = AppDatabase.forTesting(NativeDatabase.memory()); SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); }); tearDown(() async { await db.close(); }); final canonicalViewports = { '320x640 (very narrow phone)': const Size(320, 640), '400x800 (standard phone)': const Size(400, 800), '412x915 (tall modern phone)': const Size(412, 915), '640x320 (short landscape)': const Size(640, 320), '780x360 (wide landscape)': const Size(780, 360), '800x360 (wide landscape)': const Size(800, 360), '840x390 (ultra-wide landscape)': const Size(840, 390), '768x1024 (portrait tablet)': const Size(768, 1024), '1024x768 (landscape tablet)': const Size(1024, 768), }; Widget buildTestHost({ required Widget child, FieldTestLogger? fieldTestLogger, Stream? telemetryStream, }) { return ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), locationServiceProvider.overrideWith((ref) => _FakeLocationService()), if (fieldTestLogger != null) fieldTestLoggerProvider.overrideWithValue(fieldTestLogger), githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), if (telemetryStream != null) telemetryStreamProvider.overrideWith((ref) => telemetryStream), ], child: Consumer( builder: (context, ref, _) { final themeMode = ref.watch(appThemeModeProvider); return MaterialApp( themeMode: themeMode, theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, home: child, ); }, ), ); } group('Canonical 9-Viewport DiagnosticsScreen Responsive Tests', () { for (final entry in canonicalViewports.entries) { testWidgets('renders cleanly on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); final telemetryController = StreamController.broadcast(); addTearDown(() => telemetryController.close()); await tester.pumpWidget( buildTestHost( child: const DiagnosticsScreen(), telemetryStream: telemetryController.stream, ), ); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.text('Sensor Diagnostics'), findsOneWidget); expect(find.text('SENSOR HEALTH & SIGNAL'), findsOneWidget); expect(find.text('GPS & NAVIGATION METRICS'), findsOneWidget); expect(find.text('IMU, G-FORCE & ORIENTATION'), findsOneWidget); expect(tester.takeException(), isNull); }); } }); group('Canonical 9-Viewport FieldTestLiveLogScreen Responsive Tests', () { for (final entry in canonicalViewports.entries) { testWidgets('renders cleanly on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( buildTestHost( child: const FieldTestLiveLogScreen(), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('Black-Box Live Log'), findsOneWidget); expect(find.text('Telemetry Rate'), findsOneWidget); expect(find.text('GPS Accuracy'), findsOneWidget); expect(tester.takeException(), isNull); }); } }); group('Canonical 9-Viewport FieldTestReportScreen Responsive Tests', () { for (final entry in canonicalViewports.entries) { testWidgets('renders empty state cleanly on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( buildTestHost( child: const FieldTestReportScreen(), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('Forensic Field Test Report'), findsOneWidget); expect(find.textContaining('No rides recorded yet'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('renders populated forensic report on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); // Insert test ride and summary final now = DateTime.now(); final rideId = await db.into(db.rides).insert( RidesCompanion.insert( startTime: now.subtract(const Duration(minutes: 10)), distanceMiles: const drift.Value(5.2), movingTime: const drift.Value(500), stoppedTime: const drift.Value(100), ), ); await db.into(db.fieldTestSummaries).insert( FieldTestSummariesCompanion.insert( rideId: rideId, startTime: now.subtract(const Duration(minutes: 10)), durationSeconds: const drift.Value(600), totalSamples: const drift.Value(12000), validSamples: const drift.Value(11980), suspectSamples: const drift.Value(15), rejectedSamples: const drift.Value(5), validPercentage: const drift.Value(99.8), suspectPercentage: const drift.Value(0.1), rejectedPercentage: const drift.Value(0.1), gpsFixCount: const drift.Value(11950), gpsStaleEventCount: const drift.Value(2), gpsJumpRejectCount: const drift.Value(1), telemetryGapCount: const drift.Value(0), imuSaturationCount: const drift.Value(0), accelerationEventCount: const drift.Value(4), brakingEventCount: const drift.Value(3), zeroTo60Count: const drift.Value(1), zeroTo100Count: const drift.Value(0), databaseSamplesWritten: const drift.Value(12000), databaseSamplesLost: const drift.Value(0), eventCount: const drift.Value(10), rideQualityScore: const drift.Value(96), rideQualityRating: const drift.Value('EXCELLENT'), ), ); await tester.pumpWidget( buildTestHost( child: FieldTestReportScreen(rideId: rideId), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('FIELD TEST RESULT'), findsOneWidget); expect(find.text('EXCELLENT'), findsOneWidget); expect(find.text('96'), findsOneWidget); expect(find.text('SENSOR HEALTH & PIPELINE'), findsOneWidget); expect(find.text('PERFORMANCE SPRINT METRICS'), findsOneWidget); expect(find.text('FORENSIC EVENT TIMELINE'), findsOneWidget); expect(tester.takeException(), isNull); }); } }); group('Canonical 9-Viewport FieldTestPreRideCheckScreen Responsive Tests', () { for (final entry in canonicalViewports.entries) { testWidgets('renders cleanly on ${entry.key}', (tester) async { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( buildTestHost( child: const FieldTestPreRideCheckScreen(), ), ); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('Field Test Pre-Ride Check'), findsOneWidget); expect(find.text('REQUIRED SYSTEM CHECKS'), findsOneWidget); expect(find.text('RECOMMENDED PRE-FLIGHT CHECKS'), findsOneWidget); expect(find.text('SQLite Database Storage'), findsOneWidget); expect(find.text('Drift SQLite Storage verified'), findsOneWidget); expect(tester.takeException(), isNull); }); } }); group('Field Test Active Session & State Preservation Across Viewport & Theme', () { testWidgets('preserves active logging session when resized and theme changed', (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); final testLogger = FieldTestLogger(db); testLogger.startSession(77); testLogger.log( eventType: FieldTestEventType.sampleAccepted, severity: EventSeverity.info, message: 'Telemetry sample 20Hz accepted', ); final container = ProviderContainer( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), databaseProvider.overrideWithValue(db), fieldTestLoggerProvider.overrideWithValue(testLogger), githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), ], ); await tester.pumpWidget( UncontrolledProviderScope( container: container, child: Consumer( builder: (context, ref, _) { final themeMode = ref.watch(appThemeModeProvider); return MaterialApp( themeMode: themeMode, theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, home: const FieldTestLiveLogScreen(), ); }, ), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('RECORDING (Ride #77)'), findsOneWidget); expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget); // Resize viewport to landscape tablet tester.view.physicalSize = const Size(1024, 768); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('RECORDING (Ride #77)'), findsOneWidget); expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget); // Toggle Theme Mode to Light container.read(appThemeModeProvider.notifier).setThemeMode(ThemeMode.light); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('RECORDING (Ride #77)'), findsOneWidget); expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget); // Toggle Theme Mode back to Dark container.read(appThemeModeProvider.notifier).setThemeMode(ThemeMode.dark); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('RECORDING (Ride #77)'), findsOneWidget); expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget); testLogger.dispose(); container.dispose(); }); }); }