import 'dart:io'; 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/cloud/cloud_storage_provider.dart'; import 'package:fuel_tax_tracker/services/fuel_report.dart'; import 'package:fuel_tax_tracker/services/fuel_report_images.dart'; void main() { final vehicle = Vehicle( id: 'v1', vin: '1FMPU18L1TLB51349', updatedAt: DateTime.utc(2026, 1, 1), ); late Directory tempDir; setUp(() async { tempDir = await Directory.systemTemp.createTemp('fuel_report_images_test_'); }); tearDown(() async { await tempDir.delete(recursive: true); }); FuelReport reportWith(List entries) => FuelReport( startDate: DateTime(2026, 1, 1), endDate: DateTime(2026, 12, 31), rows: [ VehicleReportRow( vehicle: vehicle, entries: entries, totalGallons: entries.fold(0.0, (sum, e) => sum + e.gallons), totalCost: entries.fold(0.0, (sum, e) => sum + e.totalCost), ), ], ); FuelEntry entry({ required String id, String? receiptImagePath, String? receiptDriveFileId, }) => FuelEntry( id: id, vehicleId: 'v1', date: DateTime(2026, 3, 1), gallons: 10, pricePerGallon: 3.5, totalCost: 35, updatedAt: DateTime.utc(2026, 3, 1), receiptImagePath: receiptImagePath, receiptDriveFileId: receiptDriveFileId, ); test('reads bytes from a local file when one exists', () async { final file = File('${tempDir.path}/a.jpg')..writeAsBytesSync([1, 2, 3]); final report = reportWith([entry(id: 'e1', receiptImagePath: file.path)]); final result = await loadReceiptImageBytes(report: report, activeProvider: null); expect(result.bytesByEntryId['e1'], [1, 2, 3]); expect(result.failedEntries, isEmpty); }); test('falls back to downloading via the cloud provider when there is no local file', () async { final report = reportWith([entry(id: 'e1', receiptDriveFileId: 'remote-1')]); final provider = _FakeProvider(_FakeSession(bytesByFileId: {'remote-1': [4, 5, 6]})); final result = await loadReceiptImageBytes(report: report, activeProvider: provider); expect(result.bytesByEntryId['e1'], [4, 5, 6]); expect(result.failedEntries, isEmpty); }); test('reports (rather than silently drops) an entry with neither a local file nor a cloud reference', () async { final report = reportWith([entry(id: 'e1')]); final result = await loadReceiptImageBytes(report: report, activeProvider: null); expect(result.bytesByEntryId.containsKey('e1'), isFalse); expect(result.failedEntries.map((e) => e.id), ['e1']); }); test('reports (rather than throws) when the download fails', () async { final report = reportWith([entry(id: 'e1', receiptDriveFileId: 'missing')]); final provider = _FakeProvider(_FakeSession(bytesByFileId: {})); final result = await loadReceiptImageBytes(report: report, activeProvider: provider); expect(result.bytesByEntryId.containsKey('e1'), isFalse); expect(result.failedEntries.map((e) => e.id), ['e1']); }); test('reports a cloud-only entry when there is no signed-in provider', () async { final report = reportWith([entry(id: 'e1', receiptDriveFileId: 'remote-1')]); final result = await loadReceiptImageBytes(report: report, activeProvider: null); expect(result.bytesByEntryId.containsKey('e1'), isFalse); expect(result.failedEntries.map((e) => e.id), ['e1']); }); test('a receipt uploaded via a different provider than the one currently connected fails cleanly', () async { // Mirrors the real scenario this was built for: a WebDAV-style path // stored as receiptDriveFileId, but the currently active provider is // something else entirely (here, the fake) — the ID is meaningless to // it and the download call throws, same as any other failed download. final report = reportWith([ entry( id: 'e1', receiptDriveFileId: '/remote.php/dav/files/user/Show Me The Fuel Refund/receipts/VIN/e1.jpg', ), ]); final provider = _FakeProvider(_FakeSession(bytesByFileId: {})); final result = await loadReceiptImageBytes(report: report, activeProvider: provider); expect(result.bytesByEntryId.containsKey('e1'), isFalse); expect(result.failedEntries.map((e) => e.id), ['e1']); }); test('mixed success and failure across multiple entries', () async { final file = File('${tempDir.path}/a.jpg')..writeAsBytesSync([9]); final report = reportWith([ entry(id: 'local', receiptImagePath: file.path), entry(id: 'cloud-ok', receiptDriveFileId: 'remote-1'), entry(id: 'cloud-missing', receiptDriveFileId: 'nope'), ]); final provider = _FakeProvider(_FakeSession(bytesByFileId: {'remote-1': [1]})); final result = await loadReceiptImageBytes(report: report, activeProvider: provider); expect(result.bytesByEntryId.keys, {'local', 'cloud-ok'}); expect(result.failedEntries.map((e) => e.id), ['cloud-missing']); }); } class _FakeSession implements CloudStorageSession { final Map> bytesByFileId; _FakeSession({required this.bytesByFileId}); @override Future> downloadFileBytes(String fileId) async { final bytes = bytesByFileId[fileId]; if (bytes == null) throw StateError('not found: $fileId'); return bytes; } @override bool get supportsSharedWithMe => false; @override Future> listFolders({String? parentId, bool sharedWithMe = false}) async => []; @override Future findOrCreateFolder({required String parentId, required String name}) async => ''; @override Future moveFolder({required String folderId, required String newParentId}) async => folderId; @override Future findFile({required String folderId, required String name}) async => null; @override Future uploadFile({ required String folderId, required String name, String? existingFileId, required File localFile, required String contentType, }) async => CloudFileInfo(id: 'x', versionTag: null); @override Future deleteFile(String fileId) async {} @override Future createLockFile({required String folderId, required String name}) async => 'lock'; @override Future> listLockFiles(String folderId) async => []; @override void close() {} } class _FakeProvider implements CloudStorageProvider { final _FakeSession session; _FakeProvider(this.session); @override CloudProviderId get id => CloudProviderId.googleDrive; @override String get displayName => 'Fake Provider'; @override bool get isSignedIn => true; @override String? get accountLabel => 'tester@example.com'; @override Future attemptSilentSignIn() async => true; @override Future signIn() async => accountLabel!; @override Future signOut() async {} @override CloudStorageSession beginSession() => session; }