import 'package:flutter_test/flutter_test.dart'; import 'package:fuel_tax_tracker/models/fuel_entry.dart'; import 'package:fuel_tax_tracker/models/vehicle.dart'; import 'package:fuel_tax_tracker/services/fuel_report.dart'; void main() { final truck = Vehicle( id: 'v1', vin: '1FMPU18L1TLB51349', nickname: 'Truck', updatedAt: DateTime.utc(2026, 1, 1), ); final car = Vehicle( id: 'v2', vin: '1GNWK5EG4DR302326', updatedAt: DateTime.utc(2026, 1, 1), ); FuelEntry entry({ required String id, required String vehicleId, required DateTime date, double gallons = 10, double totalCost = 35, String? receiptImagePath, String? receiptDriveFileId, }) => FuelEntry( id: id, vehicleId: vehicleId, date: date, gallons: gallons, pricePerGallon: totalCost / gallons, totalCost: totalCost, updatedAt: date, receiptImagePath: receiptImagePath, receiptDriveFileId: receiptDriveFileId, ); test('sums gallons and cost per vehicle, only for entries with a receipt', () { final report = buildFuelReport( vehicles: [truck, car], fuelEntries: [ entry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 5), gallons: 10, totalCost: 35, receiptImagePath: '/local/a.jpg'), entry( id: 'e2', vehicleId: 'v1', date: DateTime(2026, 3, 20), gallons: 12, totalCost: 42, receiptDriveFileId: 'remote-1'), // No receipt at all — should be excluded even though it's in range. entry(id: 'e3', vehicleId: 'v1', date: DateTime(2026, 3, 10), gallons: 99, totalCost: 999), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.rows, hasLength(1)); expect(report.rows.single.vehicle.id, 'v1'); expect(report.rows.single.entryCount, 2); expect(report.rows.single.totalGallons, 22); expect(report.rows.single.totalCost, 77); }); test('date range is inclusive of both endpoints', () { final report = buildFuelReport( vehicles: [truck], fuelEntries: [ entry( id: 'first-day', vehicleId: 'v1', date: DateTime(2026, 3, 1), receiptImagePath: '/a.jpg'), entry( id: 'last-day', vehicleId: 'v1', date: DateTime(2026, 3, 31), receiptImagePath: '/b.jpg'), entry( id: 'day-after', vehicleId: 'v1', date: DateTime(2026, 4, 1), receiptImagePath: '/c.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.rows.single.entryCount, 2); }); test('a receipt-photo time-of-day does not push it out of range', () { final report = buildFuelReport( vehicles: [truck], fuelEntries: [ entry( id: 'late-in-day', vehicleId: 'v1', date: DateTime(2026, 3, 31, 23, 59), receiptImagePath: '/a.jpg', ), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.rows.single.entryCount, 1); }); test('omits vehicles with no qualifying entries rather than a zero row', () { final report = buildFuelReport( vehicles: [truck, car], fuelEntries: [ entry(id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 5), receiptImagePath: '/a.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.rows, hasLength(1)); expect(report.rows.single.vehicle.id, 'v1'); }); test('rows are sorted by vehicle display label', () { final zVehicle = Vehicle( id: 'v3', vin: 'ZZZ00000000000000', nickname: 'Zeta', updatedAt: DateTime.utc(2026, 1, 1), ); final aVehicle = Vehicle( id: 'v4', vin: 'AAA00000000000000', nickname: 'Alpha', updatedAt: DateTime.utc(2026, 1, 1), ); final report = buildFuelReport( vehicles: [zVehicle, aVehicle], fuelEntries: [ entry(id: 'e1', vehicleId: 'v3', date: DateTime(2026, 3, 5), receiptImagePath: '/a.jpg'), entry(id: 'e2', vehicleId: 'v4', date: DateTime(2026, 3, 5), receiptImagePath: '/b.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.rows.map((r) => r.vehicle.id), ['v4', 'v3']); }); test('report totals sum across all vehicle rows', () { final report = buildFuelReport( vehicles: [truck, car], fuelEntries: [ entry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 5), gallons: 10, totalCost: 35, receiptImagePath: '/a.jpg'), entry( id: 'e2', vehicleId: 'v2', date: DateTime(2026, 3, 6), gallons: 8, totalCost: 28, receiptImagePath: '/b.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.totalGallons, 18); expect(report.totalCost, 63); }); test('a row\'s entries are sorted oldest-first, regardless of insertion order', () { final report = buildFuelReport( vehicles: [truck], fuelEntries: [ entry(id: 'newest', vehicleId: 'v1', date: DateTime(2026, 3, 20), receiptImagePath: '/c.jpg'), entry(id: 'oldest', vehicleId: 'v1', date: DateTime(2026, 3, 5), receiptImagePath: '/a.jpg'), entry(id: 'middle', vehicleId: 'v1', date: DateTime(2026, 3, 12), receiptImagePath: '/b.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.rows.single.entries.map((e) => e.id), ['oldest', 'middle', 'newest']); }); test('fillCount and avgPricePerGallon are derived from the totals across all rows', () { final report = buildFuelReport( vehicles: [truck, car], fuelEntries: [ entry(id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 5), gallons: 10, totalCost: 40, receiptImagePath: '/a.jpg'), entry(id: 'e2', vehicleId: 'v2', date: DateTime(2026, 3, 6), gallons: 10, totalCost: 30, receiptImagePath: '/b.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.fillCount, 2); expect(report.avgPricePerGallon, 3.5); // $70 / 20 gal }); test('avgPricePerGallon is 0, not NaN/infinity, for an empty report', () { final report = buildFuelReport( vehicles: [truck], fuelEntries: const [], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); expect(report.avgPricePerGallon, 0); }); test('allEntries pairs each entry with its vehicle and sorts oldest-first across vehicles', () { final report = buildFuelReport( vehicles: [truck, car], fuelEntries: [ entry(id: 'truck-later', vehicleId: 'v1', date: DateTime(2026, 3, 20), receiptImagePath: '/a.jpg'), entry(id: 'car-earlier', vehicleId: 'v2', date: DateTime(2026, 3, 5), receiptImagePath: '/b.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), ); final all = report.allEntries; expect(all.map((re) => re.entry.id), ['car-earlier', 'truck-later']); expect(all[0].vehicle.id, 'v2'); expect(all[1].vehicle.id, 'v1'); }); test('monthlyTotals buckets entries by calendar month across vehicles, oldest month first', () { final report = buildFuelReport( vehicles: [truck, car], fuelEntries: [ entry(id: 'mar-1', vehicleId: 'v1', date: DateTime(2026, 3, 5), gallons: 10, totalCost: 35, receiptImagePath: '/a.jpg'), entry(id: 'mar-2', vehicleId: 'v2', date: DateTime(2026, 3, 20), gallons: 10, totalCost: 35, receiptImagePath: '/b.jpg'), entry(id: 'apr-1', vehicleId: 'v1', date: DateTime(2026, 4, 2), gallons: 10, totalCost: 40, receiptImagePath: '/c.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 4, 30), ); final months = report.monthlyTotals; expect(months, hasLength(2)); expect(months[0].month, DateTime(2026, 3)); expect(months[0].fillCount, 2); expect(months[0].totalGallons, 20); expect(months[0].totalCost, 70); expect(months[0].avgPricePerGallon, 3.5); expect(months[1].month, DateTime(2026, 4)); expect(months[1].fillCount, 1); }); test('entriesByMonth is the grouping monthlyTotals is built from', () { final report = buildFuelReport( vehicles: [truck], fuelEntries: [ entry(id: 'mar-1', vehicleId: 'v1', date: DateTime(2026, 3, 5), receiptImagePath: '/a.jpg'), entry(id: 'apr-1', vehicleId: 'v1', date: DateTime(2026, 4, 2), receiptImagePath: '/b.jpg'), ], startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 4, 30), ); final byMonth = report.entriesByMonth; expect(byMonth[DateTime(2026, 3)]!.map((re) => re.entry.id), ['mar-1']); expect(byMonth[DateTime(2026, 4)]!.map((re) => re.entry.id), ['apr-1']); }); }