import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:intl/intl.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 { // These dates are derived from "now" rather than written out, because // the screen filters against DateTime.now() (see _DateRange.startDate). // Fixed dates quietly change which bucket they land in as the calendar // moves — an earlier version of this test hardcoded August dates and // started failing the moment September arrived. final now = DateTime.now(); final format = DateFormat.yMMMd(); // The 1st of this month: always inside all three ranges. final recentDate = DateTime(now.year, now.month, 1); // The 1st of last month: always inside Last 3 Months, never inside // Current Month. Also inside Year to Date except in January, when // "last month" lands in the previous year. final midDate = DateTime(now.year, now.month - 1, 1); final midInYearToDate = midDate.year == now.year; // Jan 1 this year: always inside Year to Date. It only falls *outside* // Last 3 Months from April onward — before that, Last 3 Months already // reaches back to Jan 1 or earlier, so no date at all can be "Year to // Date but not Last 3 Months" and that distinction isn't testable. final oldDate = DateTime(now.year, 1, 1); final oldOutsideLast3Months = oldDate.isBefore(DateTime(now.year, now.month - 2, 1)); // In January the 1st of this month *is* Jan 1, so these two would // render the same date text and findsOneWidget would double-count. final oldIsDistinctFromRecent = oldDate != recentDate; final recentEntry = FuelEntry( id: 'recent', vehicleId: 'v1', date: recentDate, gallons: 10, pricePerGallon: 3, totalCost: 30, updatedAt: recentDate.toUtc(), receiptDriveFileId: 'r1', ); final midEntry = FuelEntry( id: 'mid', vehicleId: 'v1', date: midDate, gallons: 20, pricePerGallon: 3, totalCost: 60, updatedAt: midDate.toUtc(), receiptDriveFileId: 'r2', ); final oldEntry = FuelEntry( id: 'old', vehicleId: 'v1', date: oldDate, gallons: 40, pricePerGallon: 3, totalCost: 120, updatedAt: oldDate.toUtc(), 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. expect(find.text(format.format(recentDate)), findsOneWidget); if (midInYearToDate) { expect(find.text(format.format(midDate)), findsOneWidget); } if (oldIsDistinctFromRecent) { expect(find.text(format.format(oldDate)), 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(format.format(recentDate)), findsOneWidget); expect(find.text(format.format(midDate)), findsOneWidget, reason: 'last month is always within the last three'); if (oldOutsideLast3Months) { expect(find.text(format.format(oldDate)), findsNothing, reason: 'older than three 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(format.format(recentDate)), findsOneWidget); expect(find.text(format.format(midDate)), findsNothing, reason: 'not in the current month'); if (oldIsDistinctFromRecent) { expect(find.text(format.format(oldDate)), 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. }