import 'package:flutter_test/flutter_test.dart'; import 'package:uto_manager/domain/entities/doctrinal_branch.dart'; import 'package:uto_manager/domain/entities/echelon.dart'; import 'package:uto_manager/domain/entities/position_slot.dart'; import 'package:uto_manager/domain/entities/unit_node.dart'; import 'package:uto_manager/domain/value_objects/duty_mos.dart'; import 'package:uto_manager/domain/entities/authorized_rank.dart'; import 'package:uto_manager/infrastructure/codecs/pdf_chart_exporter.dart'; void main() { const exporter = PdfChartExporter(); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- UnitNode _makeUnit( String id, Echelon echelon, { String? parentId, String name = '', }) => UnitNode( unitId: id, echelon: echelon, branch: DoctrinalBranch.infantry, parentUnitId: parentId, name: name.isEmpty ? 'Unit $id' : name, isKeyLeader: false, ); PositionSlot _makeSlot(String id, String unitId) => PositionSlot( slotId: id, unitId: unitId, positionTitle: 'Rifleman', dutyMos: DutyMos.unvalidated('11B'), authorizedRank: AuthorizedRank.e3, isKeyLeader: false, ); // --------------------------------------------------------------------------- // Basic output // --------------------------------------------------------------------------- group('PdfChartExporter — basic output', () { test('returns non-empty bytes for empty tree', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test Unit', ); expect(bytes, isNotEmpty); }); test('output starts with PDF magic bytes %PDF-1.4', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test Unit', ); final header = String.fromCharCodes(bytes.take(8)); expect(header, startsWith('%PDF-1.4')); }); test('output ends with %%EOF marker', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test Unit', ); final tail = String.fromCharCodes(bytes.skip(bytes.length - 10)); expect(tail, contains('%%EOF')); }); test('output contains unit designation on cover page', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: '1st MBCT', ); final content = String.fromCharCodes(bytes); expect(content, contains('1st MBCT')); }); test('output contains UNCLASSIFIED classification banner', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test', ); final content = String.fromCharCodes(bytes); expect(content, contains('UNCLASSIFIED')); }); test('output contains xref table', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test', ); final content = String.fromCharCodes(bytes); expect(content, contains('xref')); expect(content, contains('trailer')); expect(content, contains('startxref')); }); }); // --------------------------------------------------------------------------- // Tree rendering // --------------------------------------------------------------------------- group('PdfChartExporter — tree rendering', () { test('single root unit renders without error', () { final units = [_makeUnit('u1', Echelon.platoon, name: '1st Platoon')]; final bytes = exporter.export( units: units, slots: const [], unitDesignation: '1st Platoon', ); final content = String.fromCharCodes(bytes); expect(content, contains('1st Platoon')); }); test('parent-child tree renders connector lines', () { final units = [ _makeUnit('u1', Echelon.platoon, name: '1st Platoon'), _makeUnit('u2', Echelon.squad, parentId: 'u1', name: '1st Squad'), _makeUnit('u3', Echelon.squad, parentId: 'u1', name: '2nd Squad'), ]; final bytes = exporter.export( units: units, slots: const [], unitDesignation: '1st Platoon', ); // Connector lines use 'm' (moveto) and 'l' (lineto) PDF operators. final content = String.fromCharCodes(bytes); expect(content, contains(' m ')); expect(content, contains(' l S')); }); test('all echelon levels render without error', () { final echelons = Echelon.values; final units = echelons .asMap() .entries .map((e) => _makeUnit('u${e.key}', e.value, name: e.value.displayName)) .toList(); expect( () => exporter.export( units: units, slots: const [], unitDesignation: 'All Echelons', ), returnsNormally, ); }); test('unit names appear in PDF content stream', () { final units = [ _makeUnit('u1', Echelon.company, name: 'Alpha Company'), _makeUnit('u2', Echelon.platoon, parentId: 'u1', name: '1st Platoon'), ]; final bytes = exporter.export( units: units, slots: const [], unitDesignation: 'Alpha Company', ); final content = String.fromCharCodes(bytes); // Names are truncated to 18 chars; both fit. expect(content, contains('Alpha Company')); expect(content, contains('1st Platoon')); }); test('slots are accepted without error', () { final units = [_makeUnit('u1', Echelon.squad)]; final slots = List.generate(9, (i) => _makeSlot('sl$i', 'u1')); expect( () => exporter.export( units: units, slots: slots, unitDesignation: 'Test Squad', ), returnsNormally, ); }); }); // --------------------------------------------------------------------------- // Page sizes // --------------------------------------------------------------------------- group('PdfChartExporter — page sizes', () { for (final size in PdfPageSize.values) { test('${size.name} page size produces valid PDF', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test', pageSize: size, ); final content = String.fromCharCodes(bytes); expect(content, startsWith('%PDF-1.4')); expect(content, contains('%%EOF')); }); } test('letter page MediaBox contains 612 and 792', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test', pageSize: PdfPageSize.letter, ); final content = String.fromCharCodes(bytes); expect(content, contains('612.0')); expect(content, contains('792.0')); }); test('tabloid page MediaBox contains 792 and 1224', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test', pageSize: PdfPageSize.tabloid, ); final content = String.fromCharCodes(bytes); expect(content, contains('792.0')); expect(content, contains('1224.0')); }); }); // --------------------------------------------------------------------------- // Export date // --------------------------------------------------------------------------- group('PdfChartExporter — export date', () { test('custom export date appears in cover page', () { final date = DateTime(2026, 8, 9); final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test', exportDate: date, ); final content = String.fromCharCodes(bytes); expect(content, contains('2026-08-09')); }); test('defaults to current date when exportDate is null', () { final bytes = exporter.export( units: const [], slots: const [], unitDesignation: 'Test', ); final year = DateTime.now().year.toString(); final content = String.fromCharCodes(bytes); expect(content, contains(year)); }); }); // --------------------------------------------------------------------------- // Large tree // --------------------------------------------------------------------------- group('PdfChartExporter — large tree', () { test('50-node tree exports without error', () { final units = []; units.add(_makeUnit('root', Echelon.company, name: 'Alpha Co')); for (var i = 0; i < 3; i++) { units.add(_makeUnit('plt$i', Echelon.platoon, parentId: 'root', name: '${i + 1}st Plt')); for (var j = 0; j < 3; j++) { units.add(_makeUnit( 'sq${i}_$j', Echelon.squad, parentId: 'plt$i', name: '${j + 1}st Sq', )); for (var k = 0; k < 2; k++) { units.add(_makeUnit( 'tm${i}_${j}_$k', Echelon.team, parentId: 'sq${i}_$j', name: '${k + 1} Team', )); } } } expect( () => exporter.export( units: units, slots: const [], unitDesignation: 'Alpha Co', ), returnsNormally, ); }); }); }