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/mtoe_validation_engine.dart'; import 'package:uto_manager/domain/value_objects/duty_mos.dart'; void main() { const engine = MtoeValidationEngine(); // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- UnitNode makeUnit({ required String id, required Echelon echelon, DoctrinalBranch branch = DoctrinalBranch.infantry, String? parentId, String name = 'Test Unit', }) => UnitNode( unitId: id, echelon: echelon, branch: branch, parentUnitId: parentId, name: name, isKeyLeader: false, ); PositionSlot makeSlot({ required String slotId, required String unitId, String mos = '11B', AuthorizedRank rank = AuthorizedRank.e5, bool isKeyLeader = false, String? assignedPersonnelId, }) => PositionSlot( slotId: slotId, unitId: unitId, positionTitle: 'Test Position', dutyMos: DutyMos.unvalidated(mos), authorizedRank: rank, isKeyLeader: isKeyLeader, assignedPersonnelId: assignedPersonnelId, ); PersonnelRecord makeSoldier({ required String id, String mos = '11B', AuthorizedRank rank = AuthorizedRank.e5, String? assignedSlotId, ReadinessState readiness = ReadinessState.fullyMissionCapable, }) => PersonnelRecord( soldierId: id, name: 'SGT Test', primaryMos: DutyMos.unvalidated(mos), payGrade: rank, readinessState: readiness, assignedSlotId: assignedSlotId, ); // --------------------------------------------------------------------------- // Empty input // --------------------------------------------------------------------------- group('empty input', () { test('returns no findings for empty tree', () { final findings = engine.validate(units: [], slots: [], personnel: []); expect(findings, isEmpty); }); }); // --------------------------------------------------------------------------- // V001 — MOS Mismatch // --------------------------------------------------------------------------- group('V001 — MOS mismatch', () { test('flags slot where assigned soldier MOS does not match', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final slot = makeSlot( slotId: 's1', unitId: 'u1', mos: '68W', assignedPersonnelId: 'p1', ); final soldier = makeSoldier(id: 'p1', assignedSlotId: 's1'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [soldier], ); expect(findings.any((f) => f.findingCode == 'V001'), isTrue); expect( findings.firstWhere((f) => f.findingCode == 'V001').severity, FindingSeverity.warning, ); }); test('no V001 when MOS matches', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final slot = makeSlot( slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1', ); final soldier = makeSoldier(id: 'p1', assignedSlotId: 's1'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [soldier], ); expect(findings.any((f) => f.findingCode == 'V001'), isFalse); }); test('no V001 for vacant slots', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V001'), isFalse); }); }); // --------------------------------------------------------------------------- // V002 — Rank Mismatch // --------------------------------------------------------------------------- group('V002 — rank mismatch', () { test('flags slot where soldier rank is below minimum', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final slot = makeSlot( slotId: 's1', unitId: 'u1', rank: AuthorizedRank.e6, assignedPersonnelId: 'p1', ); final soldier = makeSoldier( id: 'p1', rank: AuthorizedRank.e4, assignedSlotId: 's1', ); final findings = engine.validate( units: [unit], slots: [slot], personnel: [soldier], ); expect(findings.any((f) => f.findingCode == 'V002'), isTrue); expect( findings.firstWhere((f) => f.findingCode == 'V002').severity, FindingSeverity.warning, ); }); test('no V002 when rank meets minimum', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final slot = makeSlot( slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1', ); final soldier = makeSoldier( id: 'p1', rank: AuthorizedRank.e6, assignedSlotId: 's1', ); final findings = engine.validate( units: [unit], slots: [slot], personnel: [soldier], ); expect(findings.any((f) => f.findingCode == 'V002'), isFalse); }); }); // --------------------------------------------------------------------------- // V003 — Vacant Key Leader // --------------------------------------------------------------------------- group('V003 — vacant key leader', () { test('flags vacant key leader slot as critical', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot = makeSlot( slotId: 's1', unitId: 'u1', isKeyLeader: true, ); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V003'), isTrue); expect( findings.firstWhere((f) => f.findingCode == 'V003').severity, FindingSeverity.critical, ); }); test('no V003 when key leader slot is filled', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot = makeSlot( slotId: 's1', unitId: 'u1', isKeyLeader: true, assignedPersonnelId: 'p1', ); final soldier = makeSoldier(id: 'p1', assignedSlotId: 's1'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [soldier], ); expect(findings.any((f) => f.findingCode == 'V003'), isFalse); }); test('no V003 for non-key-leader vacant slots', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot = makeSlot(slotId: 's1', unitId: 'u1'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V003'), isFalse); }); }); // --------------------------------------------------------------------------- // V004 — Missing Combat Medic // --------------------------------------------------------------------------- group('V004 — missing combat medic', () { test('flags platoon with no 68W slot as advisory', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot = makeSlot(slotId: 's1', unitId: 'u1'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V004'), isTrue); expect( findings.firstWhere((f) => f.findingCode == 'V004').severity, FindingSeverity.advisory, ); }); test('no V004 when platoon has 68W slot', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V004'), isFalse); }); test('no V004 for non-platoon echelons', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final slot = makeSlot(slotId: 's1', unitId: 'u1'); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V004'), isFalse); }); }); // --------------------------------------------------------------------------- // V005 — Missing Fire Support Specialist // --------------------------------------------------------------------------- group('V005 — missing fire support specialist', () { test('flags platoon with no 13F slot as advisory', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot68w = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W'); final findings = engine.validate( units: [unit], slots: [slot68w], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V005'), isTrue); }); test('no V005 when platoon has 13F slot', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final slot68w = makeSlot(slotId: 's1', unitId: 'u1', mos: '68W'); final slot13f = makeSlot(slotId: 's2', unitId: 'u1', mos: '13F'); final findings = engine.validate( units: [unit], slots: [slot68w, slot13f], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V005'), isFalse); }); }); // --------------------------------------------------------------------------- // V006 — Structural Hierarchy Violation // --------------------------------------------------------------------------- group('V006 — structural hierarchy violation', () { test('flags squad directly under brigade as critical', () { final brigade = makeUnit(id: 'bde', echelon: Echelon.brigade); final squad = makeUnit( id: 'sq', echelon: Echelon.squad, parentId: 'bde', ); final findings = engine.validate( units: [brigade, squad], slots: [], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V006'), isTrue); expect( findings.firstWhere((f) => f.findingCode == 'V006').severity, FindingSeverity.critical, ); }); test('no V006 for valid battalion under brigade', () { final brigade = makeUnit(id: 'bde', echelon: Echelon.brigade); final battalion = makeUnit( id: 'bn', echelon: Echelon.battalion, parentId: 'bde', ); final findings = engine.validate( units: [brigade, battalion], slots: [], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V006'), isFalse); }); test('no V006 for root units (no parent)', () { final brigade = makeUnit(id: 'bde', echelon: Echelon.brigade); final findings = engine.validate( units: [brigade], slots: [], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V006'), isFalse); }); }); // --------------------------------------------------------------------------- // V007 — Manning Below 70% // --------------------------------------------------------------------------- group('V007 — manning below 70%', () { test('flags unit with fill rate below 70% as warning', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); // 1 filled out of 5 = 20% final slots = [ makeSlot(slotId: 's1', unitId: 'u1', assignedPersonnelId: 'p1'), makeSlot(slotId: 's2', unitId: 'u1'), makeSlot(slotId: 's3', unitId: 'u1'), makeSlot(slotId: 's4', unitId: 'u1'), makeSlot(slotId: 's5', unitId: 'u1'), ]; final findings = engine.validate( units: [unit], slots: slots, personnel: [], ); expect(findings.any((f) => f.findingCode == 'V007'), isTrue); expect( findings.firstWhere((f) => f.findingCode == 'V007').severity, FindingSeverity.warning, ); }); test('no V007 when fill rate is exactly 70%', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); // 7 filled out of 10 = 70% final slots = List.generate( 10, (i) => makeSlot( slotId: 's$i', unitId: 'u1', assignedPersonnelId: i < 7 ? 'p$i' : null, ), ); final findings = engine.validate( units: [unit], slots: slots, personnel: [], ); expect(findings.any((f) => f.findingCode == 'V007'), isFalse); }); test('no V007 for units with no slots', () { final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final findings = engine.validate( units: [unit], slots: [], personnel: [], ); expect(findings.any((f) => f.findingCode == 'V007'), isFalse); }); }); // --------------------------------------------------------------------------- // Multiple findings // --------------------------------------------------------------------------- group('multiple findings', () { test('returns multiple findings for a poorly configured unit', () { final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); // Vacant key leader + no 68W + no 13F + low manning final slot = makeSlot( slotId: 's1', unitId: 'u1', isKeyLeader: true, ); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); // V003 (vacant key leader), V004 (no medic), V005 (no FSO), V007 (manning) final codes = findings.map((f) => f.findingCode).toSet(); expect(codes, containsAll(['V003', 'V004', 'V005', 'V007'])); }); test('finding unitId is set correctly', () { final unit = makeUnit(id: 'my-unit', echelon: Echelon.platoon); final slot = makeSlot( slotId: 's1', unitId: 'my-unit', isKeyLeader: true, ); final findings = engine.validate( units: [unit], slots: [slot], personnel: [], ); final v003 = findings.firstWhere((f) => f.findingCode == 'V003'); expect(v003.unitId, 'my-unit'); }); }); }