import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:drift/native.dart'; import 'package:motorcycle_dash/database/database.dart'; import 'package:motorcycle_dash/models/maintenance_models.dart'; import 'package:motorcycle_dash/features/garage/garage_providers.dart'; import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.dart'; import 'package:motorcycle_dash/services/cloud/firebase_account_sync_service.dart'; void main() { group('MaintenanceData & Countdown Pill Logic', () { test('calculatePills returns empty when no odometer set and no records exist', () { const data = MaintenanceData(baseOdometer: 0.0, serviceRecords: []); final pills = data.calculatePills(0.0); expect(pills, isEmpty, reason: 'Must not synthesize fake countdowns for untracked motorcycles'); }); test('calculatePills calculates correct countdown when base odometer is set', () { const data = MaintenanceData(baseOdometer: 1000.0, serviceRecords: []); final pills = data.calculatePills(500.0); // current odo = 1500 expect(pills.length, 4); final oilPill = pills.firstWhere((p) => p.key == 'oil'); expect(oilPill.intervalMiles, 3000); expect(oilPill.milesRemaining, 2500.0); expect(oilPill.urgency, MaintenanceUrgency.ok); final chainPill = pills.firstWhere((p) => p.key == 'chain'); expect(chainPill.intervalMiles, 600); // current odo 1500, last serviced 1000 -> 500 miles ridden -> 600 - 500 = 100 remaining expect(chainPill.milesRemaining, 100.0); }); test('calculatePills marks overdue and due soon appropriately based on last service', () { final record = ServiceRecord( id: 'srv_1', date: DateTime.now().subtract(const Duration(days: 30)), odometer: 10000.0, serviceType: 'Engine Oil & Filter', cost: 45.0, ); final data = MaintenanceData( baseOdometer: 9000.0, serviceRecords: [record], ); // Current odo = 9000 + 3800 = 12800. Oil last serviced at 10000. Miles since service = 2800. // Oil interval = 3000. Remaining = 200 mi (<= 350 dueSoonThreshold) final pillsDueSoon = data.calculatePills(3800.0); final oilDueSoon = pillsDueSoon.firstWhere((p) => p.key == 'oil'); expect(oilDueSoon.urgency, MaintenanceUrgency.dueSoon); expect(oilDueSoon.milesRemaining, 200.0); // Current odo = 9000 + 4200 = 13200. Miles since service = 3200. Remaining = -200 (overdue) final pillsOverdue = data.calculatePills(4200.0); final oilOverdue = pillsOverdue.firstWhere((p) => p.key == 'oil'); expect(oilOverdue.urgency, MaintenanceUrgency.overdue); expect(oilOverdue.milesRemaining, -200.0); }); test('updateSpecJsonWithMaintenance preserves existing specs while modifying maintenance', () { final initialSpec = json.encode({ 'displacementCc': 599.0, 'horsepower': 120.0, }); final updatedJson = MaintenanceData.updateSpecJsonWithMaintenance( originalSpecJson: initialSpec, baseOdometer: 14500.0, serviceRecords: [ ServiceRecord( id: 'srv_100', date: DateTime(2026, 5, 1), odometer: 14500.0, serviceType: 'Chain Clean & Lube', notes: 'Motul paste', ), ], customIntervals: {'chain': 500}, ); final parsed = json.decode(updatedJson); expect(parsed['displacementCc'], 599.0); expect(parsed['horsepower'], 120.0); expect(parsed['baseOdometer'], 14500.0); expect(parsed['serviceRecords'], hasLength(1)); expect(parsed['customIntervals']['chain'], 500); final data = MaintenanceData.fromSpecificationJson(updatedJson); expect(data.baseOdometer, 14500.0); expect(data.serviceRecords.first.serviceType, 'Chain Clean & Lube'); expect(data.customIntervals['chain'], 500); }); }); group('GarageController Maintenance CRUD Operations', () { late AppDatabase db; late GarageController controller; setUp(() { db = AppDatabase.forTesting(NativeDatabase.memory()); controller = GarageController(db, HybridMotorcycleRepository()); }); tearDown(() async { await db.close(); }); test('add, update, and delete service records in database', () async { // 1. Insert a test motorcycle final bikeId = await db.into(db.garageMotorcycles).insert( GarageMotorcyclesCompanion.insert( year: 2001, makeId: 'yamaha', makeName: 'Yamaha', modelId: 'yzf-r6', modelName: 'YZF-R6', dateAdded: DateTime.now(), lastUpdated: DateTime.now(), ), ); // 2. Set base odometer await controller.updateBaseOdometer(bikeId, 21500.0); var bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle(); var data = MaintenanceData.fromSpecificationJson(bike.specificationJson); expect(data.baseOdometer, 21500.0); expect(data.serviceRecords, isEmpty); // 3. Add a service record final record1 = ServiceRecord( id: 'srv_test_1', date: DateTime(2026, 4, 10), odometer: 21500.0, serviceType: 'Engine Oil & Filter', notes: 'Castrol Power1', cost: 39.99, ); await controller.addServiceRecord(bikeId, record1); bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle(); data = MaintenanceData.fromSpecificationJson(bike.specificationJson); expect(data.serviceRecords, hasLength(1)); expect(data.serviceRecords.first.serviceType, 'Engine Oil & Filter'); expect(data.serviceRecords.first.cost, 39.99); // 4. Update the service record final updatedRecord = ServiceRecord( id: 'srv_test_1', date: DateTime(2026, 4, 10), odometer: 21550.0, serviceType: 'Engine Oil & Filter (Full Synthetic)', notes: 'Castrol Power1 4T + K&N Filter', cost: 54.99, ); await controller.updateServiceRecord(bikeId, updatedRecord); bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle(); data = MaintenanceData.fromSpecificationJson(bike.specificationJson); expect(data.serviceRecords, hasLength(1)); expect(data.serviceRecords.first.odometer, 21550.0); expect(data.serviceRecords.first.serviceType, 'Engine Oil & Filter (Full Synthetic)'); expect(data.serviceRecords.first.cost, 54.99); // 5. Update custom interval await controller.updateCustomInterval(bikeId, 'oil', 2500); bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle(); data = MaintenanceData.fromSpecificationJson(bike.specificationJson); expect(data.customIntervals['oil'], 2500); // 6. Delete service record await controller.deleteServiceRecord(bikeId, 'srv_test_1'); bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle(); data = MaintenanceData.fromSpecificationJson(bike.specificationJson); expect(data.serviceRecords, isEmpty); // base odometer and custom intervals should still be intact expect(data.baseOdometer, 21500.0); expect(data.customIntervals['oil'], 2500); }); }); group('Cross-Device Maintenance Sync Merging', () { test('mergeSpecificationJson preserves service records from both devices and attaches cloudId', () { final localJson = MaintenanceData.updateSpecJsonWithMaintenance( originalSpecJson: json.encode({'displacementCc': 599.0, 'vin': 'JYARJ06E'}), baseOdometer: 15200.0, serviceRecords: [ ServiceRecord( id: 'srv_phone_oil', date: DateTime(2026, 3, 1), odometer: 15000.0, serviceType: 'Oil & Filter', ), ], customIntervals: {'oil': 3000}, ); final remoteJson = MaintenanceData.updateSpecJsonWithMaintenance( originalSpecJson: json.encode({'displacementCc': 599.0, 'color': 'Blue'}), baseOdometer: 15800.0, serviceRecords: [ ServiceRecord( id: 'srv_web_tires', date: DateTime(2026, 4, 1), odometer: 15500.0, serviceType: 'Front/Rear Tires', ), ], customIntervals: {'tires': 6000}, ); final mergedJson = FirebaseAccountSyncService.mergeSpecificationJson( localSpecJson: localJson, remoteSpecJson: remoteJson, cloudId: 'cloud_bike_abc123', ); final mergedData = MaintenanceData.fromSpecificationJson(mergedJson); final rawDecoded = json.decode(mergedJson) as Map; // Verifies cloudId attached expect(rawDecoded['cloudId'], equals('cloud_bike_abc123')); // Verifies both local and remote service records survived expect(mergedData.serviceRecords, hasLength(2)); final recordIds = mergedData.serviceRecords.map((r) => r.id).toList(); expect(recordIds, contains('srv_phone_oil')); expect(recordIds, contains('srv_web_tires')); // Verifies highest base odometer was selected expect(mergedData.baseOdometer, equals(15800.0)); // Verifies custom intervals were merged expect(mergedData.customIntervals['oil'], equals(3000)); expect(mergedData.customIntervals['tires'], equals(6000)); // Verifies specifications preserved expect(rawDecoded['displacementCc'], equals(599.0)); expect(rawDecoded['vin'], equals('JYARJ06E')); }); }); }