import 'package:flutter_test/flutter_test.dart'; import 'package:uto_manager/domain/entities/authorized_rank.dart'; import 'package:uto_manager/domain/entities/doctrinal_branch.dart'; import 'package:uto_manager/domain/entities/echelon.dart'; import 'package:uto_manager/domain/entities/personnel_record.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/infrastructure/codecs/unit_tree_codec.dart'; void main() { const codec = UnitTreeCodec(); // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const brigade = UnitNode( unitId: 'bde-1', echelon: Echelon.brigade, branch: DoctrinalBranch.infantry, name: 'Test Brigade', isKeyLeader: false, ); const battalion = UnitNode( unitId: 'bn-1', echelon: Echelon.battalion, branch: DoctrinalBranch.infantry, parentUnitId: 'bde-1', name: 'Test Battalion', isKeyLeader: false, ); final slot = PositionSlot( slotId: 'slot-1', unitId: 'bde-1', positionTitle: 'Brigade Commander', dutyMos: DutyMos.unvalidated('11A'), authorizedRank: AuthorizedRank.o6, isKeyLeader: true, ); final soldier = PersonnelRecord( soldierId: 'p-1', name: 'COL Test', primaryMos: DutyMos.unvalidated('11A'), payGrade: AuthorizedRank.o6, readinessState: ReadinessState.fullyMissionCapable, assignedSlotId: 'slot-1', ); // --------------------------------------------------------------------------- // encode / decode round-trip // --------------------------------------------------------------------------- group('encode / decode round-trip', () { test('empty snapshot round-trips correctly', () { final json = codec.encode(units: [], slots: [], personnel: []); final snapshot = codec.decode(json); expect(snapshot.units, isEmpty); expect(snapshot.slots, isEmpty); expect(snapshot.personnel, isEmpty); }); test('units round-trip with all fields preserved', () { final json = codec.encode( units: [brigade, battalion], slots: [], personnel: [], ); final snapshot = codec.decode(json); expect(snapshot.units.length, 2); final decodedBde = snapshot.units.firstWhere((u) => u.unitId == 'bde-1'); expect(decodedBde.name, brigade.name); expect(decodedBde.echelon, brigade.echelon); expect(decodedBde.branch, brigade.branch); expect(decodedBde.parentUnitId, isNull); expect(decodedBde.isKeyLeader, brigade.isKeyLeader); final decodedBn = snapshot.units.firstWhere((u) => u.unitId == 'bn-1'); expect(decodedBn.parentUnitId, 'bde-1'); }); test('slots round-trip with all fields preserved', () { final json = codec.encode( units: [brigade], slots: [slot], personnel: [], ); final snapshot = codec.decode(json); expect(snapshot.slots.length, 1); final decodedSlot = snapshot.slots.first; expect(decodedSlot.slotId, slot.slotId); expect(decodedSlot.unitId, slot.unitId); expect(decodedSlot.positionTitle, slot.positionTitle); expect(decodedSlot.dutyMos.code, slot.dutyMos.code); expect(decodedSlot.authorizedRank, slot.authorizedRank); expect(decodedSlot.isKeyLeader, slot.isKeyLeader); expect(decodedSlot.assignedPersonnelId, isNull); }); test('personnel round-trip with all fields preserved', () { final json = codec.encode( units: [], slots: [], personnel: [soldier], ); final snapshot = codec.decode(json); expect(snapshot.personnel.length, 1); final decoded = snapshot.personnel.first; expect(decoded.soldierId, soldier.soldierId); expect(decoded.name, soldier.name); expect(decoded.primaryMos.code, soldier.primaryMos.code); expect(decoded.payGrade, soldier.payGrade); expect(decoded.readinessState, soldier.readinessState); expect(decoded.assignedSlotId, soldier.assignedSlotId); }); test('full snapshot round-trips correctly', () { final json = codec.encode( units: [brigade, battalion], slots: [slot], personnel: [soldier], ); final snapshot = codec.decode(json); expect(snapshot.units.length, 2); expect(snapshot.slots.length, 1); expect(snapshot.personnel.length, 1); }); test('encodeToMap / decodeFromMap round-trip', () { final map = codec.encodeToMap( units: [brigade], slots: [slot], personnel: [], ); final snapshot = codec.decodeFromMap(map); expect(snapshot.units.length, 1); expect(snapshot.slots.length, 1); }); }); // --------------------------------------------------------------------------- // Schema version // --------------------------------------------------------------------------- group('schema version', () { test('encoded JSON contains version field', () { final json = codec.encode(units: [], slots: [], personnel: []); expect(json, contains('"version"')); expect(json, contains('"exportedAt"')); }); test('throws FormatException for wrong schema version', () { final map = codec.encodeToMap(units: [], slots: [], personnel: []); map['version'] = 99; expect( () => codec.decodeFromMap(map), throwsA(isA()), ); }); test('throws FormatException for missing version', () { final map = codec.encodeToMap(units: [], slots: [], personnel: [])..remove('version'); expect( () => codec.decodeFromMap(map), throwsA(isA()), ); }); test('throws FormatException for non-object JSON root', () { expect( () => codec.decode('[1, 2, 3]'), throwsA(isA()), ); }); }); // --------------------------------------------------------------------------- // Null / optional fields // --------------------------------------------------------------------------- group('optional fields', () { test('null parentUnitId is preserved through round-trip', () { final json = codec.encode(units: [brigade], slots: [], personnel: []); final snapshot = codec.decode(json); expect(snapshot.units.first.parentUnitId, isNull); }); test('null assignedPersonnelId is preserved through round-trip', () { final json = codec.encode(units: [], slots: [slot], personnel: []); final snapshot = codec.decode(json); expect(snapshot.slots.first.assignedPersonnelId, isNull); }); test('null assignedSlotId is preserved through round-trip', () { final unassigned = PersonnelRecord( soldierId: 'p-2', name: 'SGT Free', primaryMos: DutyMos.unvalidated('11B'), payGrade: AuthorizedRank.e5, readinessState: ReadinessState.unknown, ); final json = codec.encode(units: [], slots: [], personnel: [unassigned]); final snapshot = codec.decode(json); expect(snapshot.personnel.first.assignedSlotId, isNull); }); test('qualifications list round-trips correctly', () { final soldierWithQuals = PersonnelRecord( soldierId: 'p-3', name: 'SGT Qualified', primaryMos: DutyMos.unvalidated('11B'), payGrade: AuthorizedRank.e5, readinessState: ReadinessState.fullyMissionCapable, qualifications: const ['Airborne', 'Ranger', 'SERE'], ); final json = codec.encode( units: [], slots: [], personnel: [soldierWithQuals], ); final snapshot = codec.decode(json); expect( snapshot.personnel.first.qualifications, containsAll(['Airborne', 'Ranger', 'SERE']), ); }); }); }