import 'package:cardash_launcher/data/models/bluetooth_device_info.dart'; import 'package:cardash_launcher/data/models/bluetooth_power_state.dart'; import 'package:cardash_launcher/data/models/gps_speed.dart'; import 'package:cardash_launcher/data/models/location_reading.dart'; import 'package:cardash_launcher/data/models/obd_state.dart'; import 'package:cardash_launcher/data/repositories/settings_repository.dart'; import 'package:cardash_launcher/data/services/telemetry_bridge.dart'; import 'package:cardash_launcher/features/home/obd_controller.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; const _obdAddress = 'AA:BB:CC:DD:EE:01'; const _phoneAddress = 'AA:BB:CC:DD:EE:02'; const _obdAdapter = BluetoothDeviceInfo( name: 'ELM327 v1.5', address: _obdAddress, deviceClass: 0x1F00, ); const _phone = BluetoothDeviceInfo( name: 'Pixel 8', address: _phoneAddress, deviceClass: 0x0200, ); class _FakeTelemetry implements TelemetryPlatform { BluetoothPowerState state = BluetoothPowerState.on; List bonded = const []; bool permissionGranted = true; TelemetryException? connectError; ObdState obdState = const ObdState(); int listCalls = 0; int stateCalls = 0; int connectCalls = 0; String? connectedAddress; @override Future> listBondedDevices() async { listCalls++; return bonded; } @override Future bluetoothState() async { stateCalls++; return state; } @override Future ensureBluetoothPermission() async => permissionGranted; @override Future openBluetoothSettings() async => true; @override Future ensureLocationPermission() async => true; @override Future getLocation() async => const LocationReading(latitude: 0, longitude: 0, place: 'Home'); @override Future connectObd(String address) async { connectCalls++; connectedAddress = address; final problem = connectError; if (problem != null) throw problem; } @override Future disconnectObd() async {} @override Future getObdState() async => obdState; @override Future startGpsSpeed() async => false; @override Future stopGpsSpeed() async {} @override Future getGpsSpeed() async => const GpsSpeed(); } Future _settingsWithAdapter(String? address) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final settings = SettingsRepository(prefs); await settings.setObdAdapterAddress(address); return settings; } void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('classifies OBD adapters and non-OBD devices', () { expect(_obdAdapter.isLikelyObd, isTrue); expect(_phone.isLikelyObd, isFalse); const headset = BluetoothDeviceInfo( name: 'Car audio', address: 'AA:BB:CC:DD:EE:03', deviceClass: 0x0400, ); expect(headset.isLikelyObd, isFalse); const sppDevice = BluetoothDeviceInfo( name: 'Unknown', address: 'AA:BB:CC:DD:EE:04', deviceClass: 0x0200, uuids: ['00001101-0000-1000-8000-00805f9b34fb'], ); expect(sppDevice.isLikelyObd, isTrue); const unnamedClass = BluetoothDeviceInfo( name: 'Unknown', address: 'AA:BB:CC:DD:EE:05', ); expect(unnamedClass.isLikelyObd, isTrue); const parsed = BluetoothDeviceInfo( name: 'Phone', address: _phoneAddress, deviceClass: 0x0200, ); expect(parsed.majorClass, 0x0200); }); test('splits paired devices into OBD and other lists', () async { final settings = await _settingsWithAdapter(null); final telemetry = _FakeTelemetry()..bonded = const [_obdAdapter, _phone]; final controller = ObdController(telemetry, settings); addTearDown(controller.dispose); expect(await controller.refreshDevices(force: true), isTrue); expect(controller.devices, hasLength(2)); expect(controller.obdDevices.single.address, _obdAddress); expect(controller.otherDevices.single.address, _phoneAddress); }); test('resume checks power state but does not relist known devices', () async { var now = DateTime(2026); final settings = await _settingsWithAdapter(null); final telemetry = _FakeTelemetry()..bonded = const [_obdAdapter]; final controller = ObdController(telemetry, settings, clock: () => now); addTearDown(controller.dispose); expect(await controller.refreshDevices(force: true), isTrue); expect(telemetry.listCalls, 1); expect(telemetry.stateCalls, 1); now = now.add(const Duration(seconds: 5)); expect(await controller.refreshDevices(dueToResume: true), isTrue); expect(telemetry.listCalls, 1); expect(telemetry.stateCalls, 2); }); test('turning on is reported without listing devices', () async { final settings = await _settingsWithAdapter(null); final telemetry = _FakeTelemetry() ..state = BluetoothPowerState.turningOn ..bonded = const [_obdAdapter]; final controller = ObdController(telemetry, settings); addTearDown(controller.dispose); expect(await controller.refreshDevices(force: true), isFalse); expect(controller.bluetoothState, BluetoothPowerState.turningOn); expect(telemetry.listCalls, 0); expect(controller.statusLabel, 'Bluetooth is starting…'); }); test('denied Bluetooth permission clears the loading state', () async { final settings = await _settingsWithAdapter(null); final telemetry = _FakeTelemetry()..permissionGranted = false; final controller = ObdController(telemetry, settings); addTearDown(controller.dispose); expect( await controller.refreshDevices(requestPermission: true, force: true), isFalse, ); expect(controller.loadingDevices, isFalse); expect(controller.needsBluetoothPermission, isTrue); expect(telemetry.listCalls, 0); }); test('start does not auto-connect to a saved non-OBD device', () async { final settings = await _settingsWithAdapter(_phoneAddress); final telemetry = _FakeTelemetry()..bonded = const [_phone]; final controller = ObdController(telemetry, settings)..initialize(); addTearDown(controller.dispose); await controller.start(); expect(telemetry.connectCalls, 0); expect(controller.error, contains('does not look like an OBD-II')); expect(controller.canRetry, isTrue); await controller.retryConnection(); controller.stop(); expect(telemetry.connectCalls, 1); }); test('start connects a saved OBD adapter', () async { final settings = await _settingsWithAdapter(_obdAddress); final telemetry = _FakeTelemetry()..bonded = const [_obdAdapter]; final controller = ObdController(telemetry, settings)..initialize(); addTearDown(controller.dispose); await controller.start(); controller.stop(); expect(telemetry.connectCalls, 1); expect(telemetry.connectedAddress, _obdAddress); }); test('connect timeout stops polling and exposes retry', () async { final settings = await _settingsWithAdapter(_obdAddress); final telemetry = _FakeTelemetry() ..bonded = const [_obdAdapter] ..connectError = TelemetryException( 'Bluetooth is not responding.', code: 'timeout', ); final controller = ObdController(telemetry, settings)..initialize(); addTearDown(controller.dispose); await controller.start(); controller.stop(); expect(telemetry.connectCalls, 1); expect(controller.canRetry, isTrue); expect(controller.statusLabel, 'Not connected — adapter not responding'); }); test('parses retryExhausted from the native snapshot', () { final state = ObdState.fromMap({ 'connected': false, 'connecting': false, 'error': 'Could not connect.', 'retryExhausted': true, }); expect(state.retryExhausted, isTrue); }); }