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/services/unit_tree_service.dart'; import 'package:uto_manager/domain/value_objects/duty_mos.dart'; void main() { const service = UnitTreeService(); // --------------------------------------------------------------------------- // Test fixtures // --------------------------------------------------------------------------- UnitNode makeUnit({ required String id, required Echelon echelon, String? parentId, String name = 'Test Unit', }) { return UnitNode( unitId: id, echelon: echelon, branch: DoctrinalBranch.infantry, name: name, isKeyLeader: false, parentUnitId: parentId, ); } PositionSlot makeSlot({ required String slotId, required String unitId, String? assignedPersonnelId, bool isKeyLeader = false, }) { return PositionSlot( slotId: slotId, unitId: unitId, positionTitle: 'Test Position', dutyMos: DutyMos('11B'), authorizedRank: AuthorizedRank.e5, isKeyLeader: isKeyLeader, assignedPersonnelId: assignedPersonnelId, ); } PersonnelRecord makeSoldier({ required String id, String? assignedSlotId, ReadinessState readiness = ReadinessState.fullyMissionCapable, }) { return PersonnelRecord( soldierId: id, name: 'SGT Test', primaryMos: DutyMos('11B'), payGrade: AuthorizedRank.e5, readinessState: readiness, assignedSlotId: assignedSlotId, ); } // --------------------------------------------------------------------------- // buildTree // --------------------------------------------------------------------------- group('buildTree', () { test('returns empty list for empty input', () { final roots = service.buildTree([], []); expect(roots, isEmpty); }); test('returns single root node with no children', () { final unit = makeUnit(id: 'u1', echelon: Echelon.brigade); final roots = service.buildTree([unit], []); expect(roots.length, 1); expect(roots.first.unit.unitId, 'u1'); expect(roots.first.children, isEmpty); }); test('builds parent-child relationship correctly', () { final brigade = makeUnit(id: 'bde', echelon: Echelon.brigade); final battalion = makeUnit(id: 'bn', echelon: Echelon.battalion, parentId: 'bde'); final roots = service.buildTree([brigade, battalion], []); expect(roots.length, 1); expect(roots.first.children.length, 1); expect(roots.first.children.first.unit.unitId, 'bn'); }); test('attaches slots to correct unit nodes', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot1 = makeSlot(slotId: 's1', unitId: 'u1'); final slot2 = makeSlot(slotId: 's2', unitId: 'u1'); final roots = service.buildTree([unit], [slot1, slot2]); expect(roots.first.slots.length, 2); }); test('multiple roots are returned correctly', () { final bde1 = makeUnit(id: 'bde1', echelon: Echelon.brigade, name: 'Brigade 1'); final bde2 = makeUnit(id: 'bde2', echelon: Echelon.brigade, name: 'Brigade 2'); final roots = service.buildTree([bde1, bde2], []); expect(roots.length, 2); }); test('fill rate is 0 when no slots', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final roots = service.buildTree([unit], []); expect(roots.first.fillRate, 0.0); }); test('fill rate is 100 when all slots filled', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot = makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1'); final roots = service.buildTree([unit], [slot]); expect(roots.first.fillRate, 100.0); }); test('fill rate is 50 when half slots filled', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot1 = makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1'); final slot2 = makeSlot(slotId: 's2', unitId: 'u1'); final roots = service.buildTree([unit], [slot1, slot2]); expect(roots.first.fillRate, 50.0); }); test('totalSlots includes children slots', () { final platoon = makeUnit(id: 'plt', echelon: Echelon.platoon); final squad = makeUnit(id: 'sq', echelon: Echelon.squad, parentId: 'plt'); final pltSlot = makeSlot(slotId: 'ps1', unitId: 'plt'); final sqSlot = makeSlot(slotId: 'ss1', unitId: 'sq'); final roots = service.buildTree([platoon, squad], [pltSlot, sqSlot]); expect(roots.first.totalSlots, 2); }); }); // --------------------------------------------------------------------------- // getSubtree // --------------------------------------------------------------------------- group('getSubtree', () { test('returns only the root when no children', () { final unit = makeUnit(id: 'u1', echelon: Echelon.brigade); final result = service.getSubtree('u1', [unit]); expect(result.length, 1); expect(result.first.unitId, 'u1'); }); test('returns root and all descendants', () { final bde = makeUnit(id: 'bde', echelon: Echelon.brigade); final bn = makeUnit(id: 'bn', echelon: Echelon.battalion, parentId: 'bde'); final co = makeUnit(id: 'co', echelon: Echelon.company, parentId: 'bn'); final result = service.getSubtree('bde', [bde, bn, co]); expect(result.length, 3); expect(result.map((u) => u.unitId), containsAll(['bde', 'bn', 'co'])); }); test('throws StateError for unknown unitId', () { expect(() => service.getSubtree('unknown', []), throwsStateError); }); }); // --------------------------------------------------------------------------- // getAncestors // --------------------------------------------------------------------------- group('getAncestors', () { test('returns empty list for root unit', () { final bde = makeUnit(id: 'bde', echelon: Echelon.brigade); final result = service.getAncestors('bde', [bde]); expect(result, isEmpty); }); test('returns correct ancestor chain', () { final bde = makeUnit(id: 'bde', echelon: Echelon.brigade); final bn = makeUnit(id: 'bn', echelon: Echelon.battalion, parentId: 'bde'); final co = makeUnit(id: 'co', echelon: Echelon.company, parentId: 'bn'); final result = service.getAncestors('co', [bde, bn, co]); expect(result.length, 2); expect(result[0].unitId, 'bn'); expect(result[1].unitId, 'bde'); }); }); // --------------------------------------------------------------------------- // validateReparent // --------------------------------------------------------------------------- group('validateReparent', () { test('valid reparent returns valid result', () { final bde = makeUnit(id: 'bde', echelon: Echelon.brigade); final bn1 = makeUnit(id: 'bn1', echelon: Echelon.battalion, parentId: 'bde'); final bn2 = makeUnit(id: 'bn2', echelon: Echelon.battalion, parentId: 'bde'); final co = makeUnit(id: 'co', echelon: Echelon.company, parentId: 'bn1'); final result = service.validateReparent(co, bn2, [bde, bn1, bn2, co]); expect(result.isValid, isTrue); }); test('cannot parent unit to itself', () { final unit = makeUnit(id: 'u1', echelon: Echelon.company); final result = service.validateReparent(unit, unit, [unit]); expect(result.isValid, isFalse); expect(result.reason, contains('cannot be its own parent')); }); test('invalid echelon hierarchy returns invalid result', () { final bde = makeUnit(id: 'bde', echelon: Echelon.brigade); final squad = makeUnit(id: 'sq', echelon: Echelon.squad); final result = service.validateReparent(squad, bde, [bde, squad]); expect(result.isValid, isFalse); expect(result.reason, contains('cannot be a child of')); }); test('circular reference is rejected', () { final bde = makeUnit(id: 'bde', echelon: Echelon.brigade); final bn = makeUnit(id: 'bn', echelon: Echelon.battalion, parentId: 'bde'); final co = makeUnit(id: 'co', echelon: Echelon.company, parentId: 'bn'); // Try to make bde (brigade) a child of co (company) — echelon is valid // direction-wise only if we swap, but the real issue is circular ancestry. // Use a case where echelon is valid but would create a cycle: // Move bn to be a child of co (bn is ancestor of co → circular). final result = service.validateReparent(bn, co, [bde, bn, co]); expect(result.isValid, isFalse); expect(result.reason, isNotEmpty); }); }); // --------------------------------------------------------------------------- // validateAssignment // --------------------------------------------------------------------------- group('validateAssignment', () { test('valid assignment returns valid result', () { final slot = makeSlot(slotId: 's1', unitId: 'u1'); final soldier = makeSoldier(id: 'p1'); final result = service.validateAssignment(slot, soldier); expect(result.isValid, isTrue); }); test('filled slot returns invalid result', () { final slot = makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'existing'); final soldier = makeSoldier(id: 'p1'); final result = service.validateAssignment(slot, soldier); expect(result.isValid, isFalse); expect(result.reason, contains('already filled')); }); test('already assigned soldier returns invalid result', () { final slot = makeSlot(slotId: 's1', unitId: 'u1'); final soldier = makeSoldier(id: 'p1', assignedSlotId: 'other-slot'); final result = service.validateAssignment(slot, soldier); expect(result.isValid, isFalse); expect(result.reason, contains('already assigned')); }); }); // --------------------------------------------------------------------------- // calculateFillRate // --------------------------------------------------------------------------- group('calculateFillRate', () { test('returns 0.0 for empty slot list', () { expect(service.calculateFillRate([]), 0.0); }); test('returns 1.0 when all slots filled', () { final slots = [ makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1'), makeSlot(slotId: 's2', unitId: 'u1', assignedPersonnelId: 'p2'), ]; expect(service.calculateFillRate(slots), 1.0); }); test('returns 0.5 when half slots filled', () { final slots = [ makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1'), makeSlot(slotId: 's2', unitId: 'u1'), ]; expect(service.calculateFillRate(slots), 0.5); }); }); // --------------------------------------------------------------------------- // getVacantKeyLeaderSlots // --------------------------------------------------------------------------- group('getVacantKeyLeaderSlots', () { test('returns only vacant key leader slots', () { final slots = [ makeSlot(slotId: 's1', unitId: 'u1', isKeyLeader: true), makeSlot(slotId: 's2', unitId: 'u1', isKeyLeader: true, assignedPersonnelId: 'p1'), makeSlot(slotId: 's3', unitId: 'u1'), ]; final result = service.getVacantKeyLeaderSlots(slots); expect(result.length, 1); expect(result.first.slotId, 's1'); }); }); // --------------------------------------------------------------------------- // getMosMismatches // --------------------------------------------------------------------------- group('getMosMismatches', () { test('returns slots where assigned soldier MOS does not match', () { final slot = PositionSlot( slotId: 's1', unitId: 'u1', positionTitle: 'Medic', dutyMos: DutyMos('68W'), authorizedRank: AuthorizedRank.e4, isKeyLeader: false, assignedPersonnelId: 'p1', ); final soldier = PersonnelRecord( soldierId: 'p1', name: 'SGT Wrong', primaryMos: DutyMos('11B'), // Wrong MOS payGrade: AuthorizedRank.e4, readinessState: ReadinessState.fullyMissionCapable, assignedSlotId: 's1', ); final result = service.getMosMismatches([slot], [soldier]); expect(result.length, 1); expect(result.first.slotId, 's1'); }); test('returns empty list when all MOS match', () { final slot = makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1'); final soldier = makeSoldier(id: 'p1', assignedSlotId: 's1'); final result = service.getMosMismatches([slot], [soldier]); expect(result, isEmpty); }); }); }