import 'dart:typed_data'; 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'; import 'package:fuel_tax_tracker/services/fuel_report_pdf.dart'; /// PDF content isn't practically assertable byte-for-byte, so this is a /// smoke test: every include-images/include-line-items combination should /// render without throwing and produce a well-formed (non-trivial, valid /// header) document — the real coverage of what each section *contains* /// lives in fuel_report_test.dart (data) and is exercised by hand. void main() { final vehicle = Vehicle( id: 'v1', vin: '1FMPU18L1TLB51349', nickname: 'Truck', updatedAt: DateTime.utc(2026, 1, 1), ); final entryA = FuelEntry( id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 5), gallons: 10, pricePerGallon: 3.5, totalCost: 35, updatedAt: DateTime.utc(2026, 3, 5), receiptImagePath: '/local/a.jpg', ); final entryB = FuelEntry( id: 'e2', vehicleId: 'v1', date: DateTime(2026, 3, 12), gallons: 12, pricePerGallon: 3.6, totalCost: 43.2, updatedAt: DateTime.utc(2026, 3, 12), receiptDriveFileId: 'remote-1', ); final report = FuelReport( startDate: DateTime(2026, 3, 1), endDate: DateTime(2026, 3, 31), rows: [ VehicleReportRow( vehicle: vehicle, entries: [entryA, entryB], totalGallons: 22, totalCost: 78.2, ), ], ); // A 1x1 red pixel PNG — enough for the pdf package to embed as an image. final onePixelPng = Uint8List.fromList([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ]); Future expectValidPdf(Uint8List bytes) async { expect(bytes.length, greaterThan(100)); expect(String.fromCharCodes(bytes.take(5)), '%PDF-'); } test('summary only (both toggles off)', () async { final bytes = await buildFuelReportPdf(report); await expectValidPdf(bytes); }); test('with line items, no images', () async { final bytes = await buildFuelReportPdf(report, includeLineItems: true); await expectValidPdf(bytes); }); test('with images, no line items', () async { final bytes = await buildFuelReportPdf( report, includeReceiptImages: true, receiptImages: {'e1': onePixelPng, 'e2': onePixelPng}, ); await expectValidPdf(bytes); }); test('with both — line items and images each get their own section, cross-referenced by number', () async { final bytes = await buildFuelReportPdf( report, includeLineItems: true, includeReceiptImages: true, receiptImages: {'e1': onePixelPng, 'e2': onePixelPng}, ); await expectValidPdf(bytes); }); test('a report with enough entries to span multiple pages does not throw', () async { // Regression test: the page banner/watermark used to be drawn by // wrapping the *entire*, unbounded-height body in a Stack, which // doesn't paginate — MultiPage threw "Widget won't fit into the page" // for anything long enough to need a page break. Enough months/entries // here to force at least one break. final vehicle2 = Vehicle(id: 'v2', vin: '1GNWK5EG4DR302326', updatedAt: DateTime.utc(2026, 1, 1)); final manyEntries = []; for (var month = 1; month <= 12; month++) { for (var day = 2; day <= 26; day += 6) { final id = 'm$month-d$day'; manyEntries.add(FuelEntry( id: id, vehicleId: month.isEven ? 'v1' : 'v2', date: DateTime(2026, month, day), gallons: 20, pricePerGallon: 3.5, totalCost: 70, updatedAt: DateTime.utc(2026, month, day), receiptDriveFileId: 'remote-$id', )); } } final bigReport = buildFuelReport( vehicles: [vehicle, vehicle2], fuelEntries: manyEntries, startDate: DateTime(2026, 1, 1), endDate: DateTime(2026, 12, 31), ); final bytes = await buildFuelReportPdf( bigReport, includeLineItems: true, includeReceiptImages: true, receiptImages: {for (final e in manyEntries) e.id: onePixelPng}, ); await expectValidPdf(bytes); }); test('receiptImages missing an entry does not crash — that photo is just skipped', () async { final bytes = await buildFuelReportPdf( report, includeReceiptImages: true, receiptImages: {'e1': onePixelPng}, // e2 deliberately missing ); await expectValidPdf(bytes); }); test('empty report (no rows) with all sections on', () async { final empty = FuelReport(startDate: DateTime(2026, 1, 1), endDate: DateTime(2026, 1, 31), rows: []); final bytes = await buildFuelReportPdf(empty, includeLineItems: true, includeReceiptImages: true); await expectValidPdf(bytes); }); test('fuelReportFileName formats the date range', () { expect(fuelReportFileName(report), 'fuel-report-20260301-20260331.pdf'); }); }