import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:fuel_tax_tracker/models/fuel_entry.dart'; import 'package:fuel_tax_tracker/models/vehicle.dart'; import 'package:fuel_tax_tracker/screens/edit_fuel_entry_screen.dart'; import 'package:fuel_tax_tracker/screens/receipt_detail_screen.dart'; import 'package:fuel_tax_tracker/screens/vehicle_detail_screen.dart'; import 'package:fuel_tax_tracker/services/app_state.dart'; import 'package:fuel_tax_tracker/services/database_service.dart'; /// Covers editing a fuel entry's logged data (date/gallons/price/total) — /// not its receipt photo, which [EditFuelEntryScreen] deliberately shows /// read-only. The database-level overwrite semantics ([DatabaseService] /// group) are what [AppState.updateFuelEntry] relies on: it just calls /// [DatabaseService.saveFuelEntry] again with the same id, same as any /// other upsert in this app (see [DatabaseService.saveVehicle]). void main() { final vehicle = Vehicle(id: 'v1', vin: '1FMPU18L1TLB51349', updatedAt: DateTime.utc(2026, 1, 1)); final otherVehicle = Vehicle(id: 'v2', vin: '1HGCM82633A004352', nickname: 'Second Car', updatedAt: DateTime.utc(2026, 1, 1)); final entry = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 5, 14, 30), gallons: 10.5, pricePerGallon: 3.499, totalCost: 36.74, updatedAt: DateTime.utc(2026, 3, 5), ); group('DatabaseService.saveFuelEntry overwrite semantics', () { late Directory tempDir; late DatabaseService database; setUpAll(() { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; }); setUp(() async { tempDir = await Directory.systemTemp.createTemp('edit_fuel_entry_test_'); PathProviderPlatform.instance = _FakePathProviderPlatform(tempDir.path); database = DatabaseService(); await database.init(); }); tearDown(() async { await database.rawDb.close(); await tempDir.delete(recursive: true); }); test('saving again with the same id overwrites gallons/price/total/date, marking it dirty', () async { await database.saveVehicle(vehicle); await database.saveFuelEntry(entry); // Clear the dirty flag the way a completed sync would, so the // overwrite below can prove it gets re-marked dirty. await database.rawDb.update('fuel_entries', {'dirty': 0}, where: 'id = ?', whereArgs: ['e1']); await database.saveFuelEntry(FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 6, 9, 0), gallons: 11.2, pricePerGallon: 3.599, totalCost: 40.31, updatedAt: DateTime.utc(2026, 3, 6), )); final rows = await database.getFuelEntries(); expect(rows, hasLength(1), reason: 'overwritten, not duplicated'); final saved = rows.single; expect(saved.gallons, 11.2); expect(saved.pricePerGallon, 3.599); expect(saved.totalCost, 40.31); expect(saved.date, DateTime(2026, 3, 6, 9, 0)); final rawRow = (await database.rawDb.query('fuel_entries', where: 'id = ?', whereArgs: ['e1'])).single; expect(rawRow['dirty'], 1); }); test('saving again with a different vehicleId moves the entry to that vehicle', () async { await database.saveVehicle(vehicle); await database.saveVehicle(otherVehicle); await database.saveFuelEntry(entry); await database.saveFuelEntry(FuelEntry( id: entry.id, vehicleId: otherVehicle.id, date: entry.date, gallons: entry.gallons, pricePerGallon: entry.pricePerGallon, totalCost: entry.totalCost, updatedAt: DateTime.utc(2026, 3, 6), )); final saved = (await database.getFuelEntries()).single; expect(saved.vehicleId, otherVehicle.id); }); }); group('EditFuelEntryScreen', () { Future pumpVehicleDetail(WidgetTester tester, AppState appState) async { await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: MaterialApp(home: VehicleDetailScreen(vehicleId: vehicle.id)), ), ); } testWidgets( 'tapping an entry row opens its receipt detail page, and its edit icon opens ' 'EditFuelEntryScreen pre-filled', (tester) async { final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry]; await pumpVehicleDetail(tester, appState); await tester.tap(find.text('10.500 gal • \$36.74')); await tester.pumpAndSettle(); expect(find.byType(ReceiptDetailScreen), findsOneWidget); await tester.tap(find.byTooltip('Edit fuel entry')); await tester.pumpAndSettle(); expect(find.byType(EditFuelEntryScreen), findsOneWidget); expect(find.widgetWithText(TextFormField, '10.500'), findsOneWidget); expect(find.widgetWithText(TextFormField, '3.499'), findsOneWidget); expect(find.widgetWithText(TextFormField, '36.74'), findsOneWidget); }); testWidgets('clearing a required field shows a validation error instead of saving', (tester) async { final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: MaterialApp(home: EditFuelEntryScreen(entry: entry)), ), ); await tester.enterText(find.widgetWithText(TextFormField, '10.500'), ''); await tester.drag(find.byType(ListView), const Offset(0, -300)); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Save Changes')); await tester.pumpAndSettle(); expect(find.text('Required'), findsOneWidget); expect(find.byType(EditFuelEntryScreen), findsOneWidget, reason: 'did not navigate away'); }); testWidgets('shows a vehicle picker pre-selected to the entry\'s current vehicle', (tester) async { final appState = AppState() ..isLoading = false ..vehicles = [vehicle, otherVehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: MaterialApp(home: EditFuelEntryScreen(entry: entry)), ), ); expect(find.widgetWithText(DropdownButtonFormField, vehicle.displayLabel), findsOneWidget); }); testWidgets('picking a different vehicle updates the selection shown', (tester) async { final appState = AppState() ..isLoading = false ..vehicles = [vehicle, otherVehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: MaterialApp(home: EditFuelEntryScreen(entry: entry)), ), ); await tester.tap(find.byType(DropdownButtonFormField)); await tester.pumpAndSettle(); // Two matches once the menu is open: the closed field's current // selection, and the same label repeated as a menu item. await tester.tap(find.text(otherVehicle.displayLabel).last); await tester.pumpAndSettle(); expect(find.widgetWithText(DropdownButtonFormField, otherVehicle.displayLabel), findsOneWidget); expect(find.widgetWithText(DropdownButtonFormField, vehicle.displayLabel), findsNothing); }); }); } class _FakePathProviderPlatform extends PathProviderPlatform with MockPlatformInterfaceMixin { final String tempPath; _FakePathProviderPlatform(this.tempPath); @override Future getApplicationDocumentsPath() async => tempPath; @override Future getTemporaryPath() async => tempPath; }