import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cashumit/models/report_transaction.dart'; import 'package:cashumit/models/report_signature_config.dart'; import 'package:cashumit/models/saved_report.dart'; import 'package:cashumit/services/report_pdf_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('ReportTransaction Model Tests', () { test('Pengeluaran without attachments should be incomplete and missing', () { final tx = ReportTransaction( id: '1', date: DateTime(2026, 1, 1), description: 'Beli kertas', amount: 50000, direction: 'pengeluaran', counterpartyName: 'Toko Buku', ); expect(tx.isPengeluaran, true); expect(tx.isPemasukan, false); expect(tx.hasFotoBarang, false); expect(tx.hasInvoice, false); expect(tx.isComplete, false); expect(tx.attachmentStatus, 'missing'); expect(tx.tags, isEmpty); }); test('Pengeluaran with only foto barang should have partial status', () { final tx = ReportTransaction( id: '2', date: DateTime(2026, 1, 2), description: 'Beli filament', amount: 150000, direction: 'pengeluaran', counterpartyName: 'Toko 3D', fotoBarangPath: '/path/to/foto.jpg', ); expect(tx.hasFotoBarang, true); expect(tx.hasInvoice, false); expect(tx.isComplete, false); expect(tx.attachmentStatus, 'partial'); }); test('Pengeluaran with both foto and invoice should be complete', () { final tx = ReportTransaction( id: '3', date: DateTime(2026, 1, 3), description: 'Beli komponen', amount: 250000, direction: 'pengeluaran', counterpartyName: 'Toko Elektro', fotoBarangPath: '/path/to/foto.jpg', invoicePath: '/path/to/invoice.pdf', invoiceType: 'pdf', ); expect(tx.hasFotoBarang, true); expect(tx.hasInvoice, true); expect(tx.isComplete, true); expect(tx.attachmentStatus, 'complete'); }); test('Pemasukan is always considered complete', () { final tx = ReportTransaction( id: '4', date: DateTime(2026, 1, 4), description: 'Jasa Cetak 3D', amount: 300000, direction: 'pemasukan', counterpartyName: 'Pelanggan A', ); expect(tx.isPemasukan, true); expect(tx.isPengeluaran, false); expect(tx.isComplete, true); expect(tx.attachmentStatus, 'not_required'); }); test('Serialization and deserialization should preserve fields including tags and IDs', () { final tx = ReportTransaction( id: '5', date: DateTime(2026, 1, 5, 14, 30), description: 'Test serialisasi', amount: 75000, direction: 'pengeluaran', counterpartyName: 'Toko Test', tags: ['atk', 'upj', 'operasional'], transactionGroupId: 'grp_123', transactionJournalId: 'jrn_456', fotoBarangPath: '/img.jpg', fotoBarangName: 'img.jpg', invoicePath: '/inv.pdf', invoiceName: 'inv.pdf', invoiceType: 'pdf', invoiceSnapshotPath: '/inv_snapshot.png', ); final json = tx.toJson(); final fromJson = ReportTransaction.fromJson(json); expect(fromJson.id, tx.id); expect(fromJson.date, tx.date); expect(fromJson.description, tx.description); expect(fromJson.amount, tx.amount); expect(fromJson.direction, tx.direction); expect(fromJson.counterpartyName, tx.counterpartyName); expect(fromJson.tags, ['atk', 'upj', 'operasional']); expect(fromJson.transactionGroupId, 'grp_123'); expect(fromJson.transactionJournalId, 'jrn_456'); expect(fromJson.fotoBarangPath, tx.fotoBarangPath); expect(fromJson.invoicePath, tx.invoicePath); expect(fromJson.invoiceType, tx.invoiceType); expect(fromJson.invoiceSnapshotPath, tx.invoiceSnapshotPath); expect(fromJson.hasInvoiceSnapshot, true); }); test('copyWith creates modified copy correctly', () { final tx = ReportTransaction( id: '6', date: DateTime(2026, 1, 6), description: 'Asli', amount: 10000, direction: 'pengeluaran', counterpartyName: 'Lawan', tags: ['lama'], ); final modified = tx.copyWith( description: 'Diubah', amount: 25000, tags: ['baru', 'upj'], invoiceSnapshotPath: '/snapshot.png', ); expect(modified.id, '6'); expect(modified.description, 'Diubah'); expect(modified.amount, 25000); expect(modified.tags, ['baru', 'upj']); expect(modified.invoiceSnapshotPath, '/snapshot.png'); expect(modified.hasInvoiceSnapshot, true); expect(tx.description, 'Asli'); // Original unchanged expect(tx.hasInvoiceSnapshot, false); }); }); group('ReportSignatureConfig Tests', () { setUp(() { SharedPreferences.setMockInitialValues({}); }); test('Default values should match 4 signatories in Laporan Keuangan 2026 - Agustus.pdf', () async { final config = await ReportSignatureConfig.load(); expect(config.reportTitle, 'LAPORAN KEUANGAN UPJ MEKATRONIKA'); expect(config.city, 'Malang'); // 1. Pelaksana UPJ (Kiri Atas) expect(config.signatory1Name, 'Anggoro Dwi Nur Rohman, S.Pd, M.T'); expect(config.signatory1Nip, 'NNIP. 20533816260193012106054'); // 2. Pembuat Laporan (Kanan Atas) expect(config.signatory2Name, 'Rama Dwi Nofanto Setiawan'); // 3. Koordinator UPJ (Kiri Bawah) expect(config.signatory3Name, 'Puguh Hartono, SPd'); expect(config.signatory3Nip, 'NIP. 197003152003121009'); // 4. KPA BLUD SMKN 4 Malang (Kanan Bawah) expect(config.signatory4Name, 'Drs. Gunawan Dwiyono, S.ST, M.Pd'); expect(config.signatory4Nip, 'NIP. 19670930 199512 1 002'); }); test('Custom 4-signatory values can be saved and loaded', () async { const customConfig = ReportSignatureConfig( reportTitle: 'LAPORAN KAS TAHUNAN', city: 'Surabaya', signatory1Role: 'Wakil Kepala Sekolah', signatory1Name: 'Budi Santoso, M.Pd', signatory1Nip: '198001012005011001', signatory2Role: 'Penyusun', signatory2Name: 'Ahmad Dahlan', signatory2Nip: '09876', signatory3Role: 'Kepala Program Keahlian', signatory3Name: 'Siti Aminah, S.T.', signatory3Nip: '198502022010022002', signatory4Role: 'Kepala SMKN 1', signatory4Name: 'Dr. H. Sulaiman, M.M.', signatory4Nip: '196503031990031003', ); await customConfig.save(); final loaded = await ReportSignatureConfig.load(); expect(loaded.reportTitle, 'LAPORAN KAS TAHUNAN'); expect(loaded.city, 'Surabaya'); expect(loaded.signatory1Name, 'Budi Santoso, M.Pd'); expect(loaded.signatory2Name, 'Ahmad Dahlan'); expect(loaded.signatory3Name, 'Siti Aminah, S.T.'); expect(loaded.signatory4Name, 'Dr. H. Sulaiman, M.M.'); }); test('JSON serialization works correctly', () { const config = ReportSignatureConfig(); final json = config.toJson(); final fromJson = ReportSignatureConfig.fromJson(json); expect(fromJson.reportTitle, config.reportTitle); expect(fromJson.signatory1Name, config.signatory1Name); expect(fromJson.signatory4Name, config.signatory4Name); }); }); group('SavedReport Model Tests', () { test('Calculates financial summary and attachment counts accurately', () { final tx1 = ReportTransaction( id: '1', date: DateTime(2026, 8, 1), description: 'Pemasukan UPJ', amount: 1000000, direction: 'pemasukan', counterpartyName: 'Pelanggan', ); final tx2 = ReportTransaction( id: '2', date: DateTime(2026, 8, 2), description: 'Beli Pylox', amount: 65000, direction: 'pengeluaran', counterpartyName: 'Toko Seng Jaya', fotoBarangPath: '/foto.jpg', invoicePath: '/nota.pdf', invoiceType: 'pdf', ); final tx3 = ReportTransaction( id: '3', date: DateTime(2026, 8, 3), description: 'Beli PCB', amount: 8000, direction: 'pengeluaran', counterpartyName: 'Toko Elektronika', ); final saved = SavedReport( id: 'sr_001', title: 'Laporan Kas Agustus 2026', accountId: 'acc_1', accountName: 'Dompet UPJ', startDate: DateTime(2026, 8, 1), endDate: DateTime(2026, 8, 31), createdAt: DateTime(2026, 8, 31), updatedAt: DateTime(2026, 8, 31), transactions: [tx1, tx2, tx3], ); expect(saved.totalPemasukan, 1000000); expect(saved.totalPengeluaran, 73000); expect(saved.saldoBersih, 927000); expect(saved.totalPengeluaranCount, 2); expect(saved.completedAttachmentCount, 1); expect(saved.incompleteAttachmentCount, 1); // Test JSON serialization final json = saved.toJson(); final restored = SavedReport.fromJson(json); expect(restored.id, saved.id); expect(restored.title, saved.title); expect(restored.accountName, 'Dompet UPJ'); expect(restored.transactions.length, 3); expect(restored.saldoBersih, 927000); }); }); group('ReportPdfService Tests', () { test('paginateTransactions guarantees at least 5 rows on the last page', () { // 1. Sedikit transaksi (10 items) -> 1 halaman final list10 = List.generate( 10, (i) => ReportTransaction( id: '$i', date: DateTime(2026, 8, 1), description: 'Item $i', amount: 10000, direction: 'pengeluaran', counterpartyName: 'Toko $i', ), ); final paged10 = ReportPdfService.paginateTransactions(list10); expect(paged10.length, 1); expect(paged10[0].length, 10); // 2. Transaksi batas 1 halaman (28 items) -> 1 halaman final list28 = List.generate( 28, (i) => ReportTransaction( id: '$i', date: DateTime(2026, 8, 1), description: 'Item $i', amount: 10000, direction: 'pengeluaran', counterpartyName: 'Toko $i', ), ); final paged28 = ReportPdfService.paginateTransactions(list28); expect(paged28.length, 1); expect(paged28[0].length, 28); // 3. Transaksi 30 items -> 2 halaman: Page 1 = 25, Page 2 = 5! final list30 = List.generate( 30, (i) => ReportTransaction( id: '$i', date: DateTime(2026, 8, 1), description: 'Item $i', amount: 10000, direction: 'pengeluaran', counterpartyName: 'Toko $i', ), ); final paged30 = ReportPdfService.paginateTransactions(list30); expect(paged30.length, 2); expect(paged30[0].length, 25); expect(paged30[1].length, 5); // Minimal 5 di lembar terakhir! // 4. Transaksi 34 items (sesuai dokumen acuan) -> 2 halaman: Page 1 = 29, Page 2 = 5! final list34 = List.generate( 34, (i) => ReportTransaction( id: '$i', date: DateTime(2026, 8, 1), description: 'Item $i', amount: 10000, direction: 'pengeluaran', counterpartyName: 'Toko $i', ), ); final paged34 = ReportPdfService.paginateTransactions(list34); expect(paged34.length, 2); expect(paged34[0].length, 29); expect(paged34[1].length, 5); // Lembar terakhir persis 5 transaksi! // 5. Transaksi 45 items -> 2 halaman: Page 1 = 34, Page 2 = 11 (>= 5) final list45 = List.generate( 45, (i) => ReportTransaction( id: '$i', date: DateTime(2026, 8, 1), description: 'Item $i', amount: 10000, direction: 'pengeluaran', counterpartyName: 'Toko $i', ), ); final paged45 = ReportPdfService.paginateTransactions(list45); expect(paged45.length, 2); expect(paged45[0].length, 34); expect(paged45[1].length, 11); expect(paged45.last.length >= 5, true); }); test('Generate PDF with 4 signatories grid 2x2 and Firefly links without throwing errors', () async { final txList = [ ReportTransaction( id: '1', date: DateTime(2026, 8, 1), description: 'Pemasukan Kas Awal', amount: 1000000, direction: 'pemasukan', counterpartyName: 'Dompet UPJ', transactionGroupId: '101', ), ReportTransaction( id: '2', date: DateTime(2026, 8, 2), description: 'Beli Pylox blackflat Dan white', amount: 65000, direction: 'pengeluaran', counterpartyName: 'Toko Seng Jaya', transactionGroupId: '102', ), ]; const config = ReportSignatureConfig(); final tempFile = File('${Directory.systemTemp.path}/test_report_4sig_${DateTime.now().millisecondsSinceEpoch}.pdf'); final pdfFile = await ReportPdfService.generateReportPdf( transactions: txList, startDate: DateTime(2026, 8, 1), endDate: DateTime(2026, 8, 3), config: config, accountName: 'Dompet UPJ', targetFile: tempFile, fireflyBaseUrl: 'http://100.98.70.89:8080', ); expect(pdfFile, isNotNull); expect(pdfFile.existsSync(), true); expect(pdfFile.lengthSync(), greaterThan(1000)); // Clean up test file if (pdfFile.existsSync()) { pdfFile.deleteSync(); } }); test('Generate PDF with 34 transactions splits into 2 pages with 5 on last page', () async { final txList = List.generate( 34, (i) => ReportTransaction( id: '$i', date: DateTime(2026, 8, i + 1 > 31 ? 31 : i + 1), description: 'Transaksi ke-$i', amount: (i + 1) * 10000.0, direction: i % 3 == 0 ? 'pemasukan' : 'pengeluaran', counterpartyName: 'Lawan $i', transactionGroupId: 'group_$i', ), ); const config = ReportSignatureConfig(); final tempFile = File('${Directory.systemTemp.path}/test_report_34tx_${DateTime.now().millisecondsSinceEpoch}.pdf'); final pdfFile = await ReportPdfService.generateReportPdf( transactions: txList, startDate: DateTime(2026, 8, 1), endDate: DateTime(2026, 8, 31), config: config, accountName: 'Dompet UPJ', targetFile: tempFile, fireflyBaseUrl: 'http://100.98.70.89:8080', ); expect(pdfFile, isNotNull); expect(pdfFile.existsSync(), true); expect(pdfFile.lengthSync(), greaterThan(2000)); // Clean up test file if (pdfFile.existsSync()) { pdfFile.deleteSync(); } }); }); }