MotorSignal-App/test/garage_maintenance_test.dart

246 lines
9.6 KiB
Dart

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<String, dynamic>;
// 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'));
});
});
}