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/services/validation_isolate.dart'; import 'package:uto_manager/domain/value_objects/duty_mos.dart'; void main() { // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- UnitNode makeUnit(String id, Echelon echelon, {String? parentId}) => UnitNode( unitId: id, echelon: echelon, branch: DoctrinalBranch.infantry, parentUnitId: parentId, name: 'Unit $id', isKeyLeader: false, ); PositionSlot makeSlot( String id, String unitId, { bool isKeyLeader = false, String? assignedPersonnelId, }) => PositionSlot( slotId: id, unitId: unitId, positionTitle: isKeyLeader ? 'Squad Leader' : 'Rifleman', dutyMos: DutyMos.unvalidated(isKeyLeader ? '11B' : '11B'), authorizedRank: isKeyLeader ? AuthorizedRank.e6 : AuthorizedRank.e3, isKeyLeader: isKeyLeader, assignedPersonnelId: assignedPersonnelId, ); // --------------------------------------------------------------------------- // IsolateValidationRunner // --------------------------------------------------------------------------- group('IsolateValidationRunner', () { late IsolateValidationRunner runner; setUp(() { runner = IsolateValidationRunner(); }); tearDown(() async { await runner.dispose(); }); test('isStarted is false before start()', () { expect(runner.isStarted, false); }); test('isStarted is true after start()', () async { await runner.start(); expect(runner.isStarted, true); }); test('start() is idempotent — calling twice does not throw', () async { await runner.start(); await runner.start(); // second call should be a no-op expect(runner.isStarted, true); }); test('validate() throws StateError if start() not called', () async { expect( () => runner.validate(units: const [], slots: const [], personnel: const []), throwsStateError, ); }); test('validate() returns empty findings for empty tree', () async { await runner.start(); final response = await runner.validate( units: const [], slots: const [], personnel: const [], ); expect(response.hasError, false); expect(response.findings, isEmpty); }); test('validate() detects vacant key leader (V003)', () async { await runner.start(); final unit = makeUnit('u1', Echelon.squad); final slot = makeSlot('sl1', 'u1', isKeyLeader: true); // vacant final response = await runner.validate( units: [unit], slots: [slot], personnel: const [], ); expect(response.hasError, false); expect(response.findings.any((f) => f.findingCode == 'V003'), true); }); test('validate() detects missing medic in platoon (V004)', () async { await runner.start(); final unit = makeUnit('u1', Echelon.platoon); final slot = makeSlot('sl1', 'u1'); // no 68W slot final response = await runner.validate( units: [unit], slots: [slot], personnel: const [], ); expect(response.hasError, false); expect(response.findings.any((f) => f.findingCode == 'V004'), true); }); test('validate() returns clean result for fully-staffed valid unit', () async { await runner.start(); // A squad with a filled key leader slot and no hierarchy violations. final unit = makeUnit('u1', Echelon.squad); final keyLeaderSlot = PositionSlot( slotId: 'sl1', unitId: 'u1', positionTitle: 'Squad Leader', dutyMos: DutyMos.unvalidated('11B'), authorizedRank: AuthorizedRank.e6, isKeyLeader: true, assignedPersonnelId: 's1', ); final soldier = PersonnelRecord( soldierId: 's1', name: 'SSG Smith', primaryMos: DutyMos.unvalidated('11B'), payGrade: AuthorizedRank.e6, readinessState: ReadinessState.fullyMissionCapable, ); final response = await runner.validate( units: [unit], slots: [keyLeaderSlot], personnel: [soldier], ); expect(response.hasError, false); // V003 should not fire (key leader is filled). expect(response.findings.any((f) => f.findingCode == 'V003'), false); }); test('validate() detects structural hierarchy violation (V006)', () async { await runner.start(); // Squad directly under Brigade — invalid. final brigade = makeUnit('bde', Echelon.brigade); final squad = makeUnit('sq1', Echelon.squad, parentId: 'bde'); final response = await runner.validate( units: [brigade, squad], slots: const [], personnel: const [], ); expect(response.hasError, false); expect(response.findings.any((f) => f.findingCode == 'V006'), true); expect( response.findings.firstWhere((f) => f.findingCode == 'V006').severity, FindingSeverity.critical, ); }); test('validate() results match synchronous engine output', () async { await runner.start(); final units = [ makeUnit('bde', Echelon.brigade), makeUnit('bn', Echelon.battalion, parentId: 'bde'), makeUnit('co', Echelon.company, parentId: 'bn'), makeUnit('plt', Echelon.platoon, parentId: 'co'), ]; final slots = [ makeSlot('sl1', 'plt', isKeyLeader: true), // vacant key leader ]; final isolateResponse = await runner.validate( units: units, slots: slots, personnel: const [], ); // Compare with synchronous engine. const syncEngine = MtoeValidationEngine(); final syncFindings = syncEngine.validate( units: units, slots: slots, personnel: const [], ); expect(isolateResponse.findings.length, syncFindings.length); for (var i = 0; i < syncFindings.length; i++) { expect(isolateResponse.findings[i].findingCode, syncFindings[i].findingCode); expect(isolateResponse.findings[i].severity, syncFindings[i].severity); } }); test('dispose() sets isStarted to false', () async { await runner.start(); await runner.dispose(); expect(runner.isStarted, false); }); test('can restart after dispose()', () async { await runner.start(); await runner.dispose(); await runner.start(); expect(runner.isStarted, true); final response = await runner.validate( units: const [], slots: const [], personnel: const [], ); expect(response.hasError, false); }); }); // --------------------------------------------------------------------------- // ValidationResponse // --------------------------------------------------------------------------- group('ValidationResponse', () { test('hasError is false when error is null', () { const response = ValidationResponse(findings: []); expect(response.hasError, false); }); test('hasError is true when error is set', () { const response = ValidationResponse(findings: [], error: 'Something went wrong'); expect(response.hasError, true); }); }); }