import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:fuel_tax_tracker/models/fuel_entry.dart'; import 'package:fuel_tax_tracker/models/vehicle.dart'; import 'package:fuel_tax_tracker/screens/receipt_detail_screen.dart'; import 'package:fuel_tax_tracker/screens/receipts_screen.dart'; import 'package:fuel_tax_tracker/services/app_state.dart'; void main() { final vehicle = Vehicle( id: 'v1', vin: '1FMPU18L1TLB51349', nickname: 'Red Truck', updatedAt: DateTime.utc(2026, 1, 1), ); testWidgets('shows an empty state with no fuel entries', (tester) async { final appState = AppState() ..isLoading = false ..vehicles = [vehicle]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); expect(find.textContaining('No fuel entries'), findsOneWidget); }); testWidgets('shows the hero total and a receipt row with date/amount/vehicle chip', (tester) async { final entry = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 5, 14), gallons: 10.523, pricePerGallon: 3.5, totalCost: 32.48, updatedAt: DateTime.utc(2026, 5, 14), receiptDriveFileId: 'remote-1', ); final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); // Hero total sums totalCost across all entries โ€” just the one here. expect(find.text('\$32.48'), findsNWidgets(2)); // hero total + row amount expect(find.text('Total Fuel Cost'), findsOneWidget, reason: 'no year prefix'); expect(find.text('Total Gallons'), findsOneWidget); expect(find.text('11'), findsOneWidget, reason: 'hero total gallons, rounded to whole gallons'); expect(find.text('May 14, 2026'), findsOneWidget); expect(find.text('10.523 gal'), findsOneWidget, reason: 'row subtitle'); expect(find.text('Red Truck'), findsOneWidget); }); testWidgets( 'shows an estimated fuel refund in the hero banner and next to each row\'s gallons ' 'when the setting is on', (tester) async { final entry = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 5, 14), gallons: 10.523, pricePerGallon: 3.5, totalCost: 32.48, updatedAt: DateTime.utc(2026, 5, 14), receiptDriveFileId: 'remote-1', ); final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry] ..showEstimatedFuelRefund = true; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); expect(find.text('Est. Refund'), findsOneWidget, reason: 'replaces the hero icon'); // 10.523 gal rounds up to 11, * $0.125/gal = $1.375, floored to $1.37 โ€” // the hero's own standalone stat value... expect(find.text('\$1.37'), findsOneWidget); // ...and the same figure again, folded into the row's combined text. expect(find.textContaining('10.523 gal ยท Est. \$1.37'), findsOneWidget); }); testWidgets("doesn't show an estimated fuel refund anywhere when the setting is off (default)", (tester) async { final entry = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 5, 14), gallons: 10.523, pricePerGallon: 3.5, totalCost: 32.48, updatedAt: DateTime.utc(2026, 5, 14), receiptDriveFileId: 'remote-1', ); final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); expect(appState.showEstimatedFuelRefund, isFalse, reason: 'off by default'); expect(find.text('Est. Refund'), findsNothing); expect(find.textContaining('Est. \$'), findsNothing); }); testWidgets('tapping a receipted row opens the receipt detail page', (tester) async { final entry = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 5, 14), gallons: 10, pricePerGallon: 3.5, totalCost: 35, updatedAt: DateTime.utc(2026, 5, 14), receiptDriveFileId: 'remote-1', ); final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); await tester.tap(find.text('May 14, 2026')); await tester.pumpAndSettle(); expect(find.byType(ReceiptDetailScreen), findsOneWidget); }); testWidgets('tapping a row with no receipt photo still opens the detail page', (tester) async { final entry = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 5, 14), gallons: 10, pricePerGallon: 3.5, totalCost: 35, updatedAt: DateTime.utc(2026, 5, 14), ); final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); await tester.tap(find.text('May 14, 2026')); await tester.pumpAndSettle(); expect(find.byType(ReceiptDetailScreen), findsOneWidget, reason: 'the data is still viewable/editable even without a photo'); }); testWidgets('formats large gallon totals with thousands separators', (tester) async { final entry = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 8, 5), gallons: 1234.5678, pricePerGallon: 3.5, totalCost: 4320.99, updatedAt: DateTime.utc(2026, 8, 5), receiptDriveFileId: 'remote-1', ); final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [entry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); expect(find.text('1,235'), findsOneWidget, reason: 'hero total gallons, rounded to whole gallons'); expect(find.text('1,234.568 gal'), findsOneWidget, reason: 'row subtitle keeps full precision'); }); testWidgets('date range selector filters totals and the list, and recolors the active label', (tester) async { // Aug 5 2026: within Current Month, Last 3 Months, and Year to Date. final recentEntry = FuelEntry( id: 'recent', vehicleId: 'v1', date: DateTime(2026, 8, 5), gallons: 10, pricePerGallon: 3, totalCost: 30, updatedAt: DateTime.utc(2026, 8, 5), receiptDriveFileId: 'r1', ); // Jul 1 2026: within Last 3 Months and Year to Date, not Current Month. final midEntry = FuelEntry( id: 'mid', vehicleId: 'v1', date: DateTime(2026, 7, 1), gallons: 20, pricePerGallon: 3, totalCost: 60, updatedAt: DateTime.utc(2026, 7, 1), receiptDriveFileId: 'r2', ); // Mar 1 2026: within Year to Date only. final oldEntry = FuelEntry( id: 'old', vehicleId: 'v1', date: DateTime(2026, 3, 1), gallons: 40, pricePerGallon: 3, totalCost: 120, updatedAt: DateTime.utc(2026, 3, 1), receiptDriveFileId: 'r3', ); final appState = AppState() ..isLoading = false ..vehicles = [vehicle] ..fuelEntries = [recentEntry, midEntry, oldEntry]; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); bool isBold(String label) => tester .widgetList(find.text(label)) .any((t) => t.style?.fontWeight == FontWeight.w800); // Defaults to Year to Date: all three visible. expect(find.text('Aug 5, 2026'), findsOneWidget); expect(find.text('Jul 1, 2026'), findsOneWidget); expect(find.text('Mar 1, 2026'), findsOneWidget); expect(isBold('Year to Date'), isTrue, reason: 'selected by default'); await tester.tap(find.text('Last 3 Months')); await tester.pumpAndSettle(); expect(find.text('Aug 5, 2026'), findsOneWidget); expect(find.text('Jul 1, 2026'), findsOneWidget); expect(find.text('Mar 1, 2026'), findsNothing, reason: 'older than 3 months back'); expect(isBold('Last 3 Months'), isTrue); expect(isBold('Year to Date'), isFalse, reason: 'no longer selected'); await tester.tap(find.text('Current Month')); await tester.pumpAndSettle(); expect(find.text('Aug 5, 2026'), findsOneWidget); expect(find.text('Jul 1, 2026'), findsNothing, reason: 'not in the current month'); expect(find.text('Mar 1, 2026'), findsNothing); }); testWidgets( 'tapping + with no vehicles opens vehicle creation and prompts to create one for the receipt', (tester) async { final appState = AppState()..isLoading = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReceiptsScreen()), ), ); await tester.tap(find.byIcon(Icons.add)); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Add Vehicle'), findsOneWidget); expect(find.text('Create a vehicle to attach receipt'), findsOneWidget); await tester.tap(find.text('OK')); await tester.pumpAndSettle(); expect(find.text('Create a vehicle to attach receipt'), findsNothing); expect(find.widgetWithText(AppBar, 'Add Vehicle'), findsOneWidget, reason: 'still on the form'); }); // Not covered here: that saving the new vehicle continues into receipt // capture (rather than back to an empty Receipts tab). Exercising that // needs AddEditVehicleScreen's real _save() to run, which touches a real // database โ€” and combining a real sqflite_common_ffi database with // testWidgets (as opposed to a plain test()) reliably deadlocks in this // project's test environment (confirmed directly: the identical // `DatabaseService().init()` call returns immediately under test(), but // times out under testWidgets()). Verified manually on-device instead. }