import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:uto_manager/domain/services/command_event_store.dart'; void main() { // --------------------------------------------------------------------------- // CommandEvent // --------------------------------------------------------------------------- group('CommandEvent', () { test('toJson / fromJson round-trip preserves all fields', () { final event = CommandEvent( eventId: 'evt-001', type: CommandEventType.attachNode, payload: CommandPayloads.attachNode( unitId: 'unit-1', newParentId: 'unit-2', previousParentId: 'unit-0', ), occurredAt: DateTime.utc(2026, 8, 9, 10, 0, 0), isSynced: false, ); final json = event.toJson(); final restored = CommandEvent.fromJson(json); expect(restored.eventId, event.eventId); expect(restored.type, event.type); expect(restored.payload['unitId'], 'unit-1'); expect(restored.payload['newParentId'], 'unit-2'); expect(restored.payload['previousParentId'], 'unit-0'); expect(restored.occurredAt, event.occurredAt); expect(restored.isSynced, false); }); test('fromJson handles missing isSynced (defaults to false)', () { final map = { 'eventId': 'evt-002', 'type': 'detachNode', 'payload': {'unitId': 'u1', 'previousParentId': 'u0'}, 'occurredAt': '2026-08-09T10:00:00.000Z', // isSynced intentionally omitted }; final event = CommandEvent.fromJson(map); expect(event.isSynced, false); }); test('toString includes eventId, type, and synced flag', () { final event = CommandEvent( eventId: 'evt-003', type: CommandEventType.assignPersonnel, payload: {}, occurredAt: DateTime.now(), ); final str = event.toString(); expect(str, contains('evt-003')); expect(str, contains('assignPersonnel')); expect(str, contains('false')); }); }); // --------------------------------------------------------------------------- // CommandPayloads // --------------------------------------------------------------------------- group('CommandPayloads', () { test('attachNode includes optional previousParentId when provided', () { final p = CommandPayloads.attachNode( unitId: 'u1', newParentId: 'u2', previousParentId: 'u0', ); expect(p['unitId'], 'u1'); expect(p['newParentId'], 'u2'); expect(p['previousParentId'], 'u0'); }); test('attachNode omits previousParentId when null', () { final p = CommandPayloads.attachNode(unitId: 'u1', newParentId: 'u2'); expect(p.containsKey('previousParentId'), false); }); test('detachNode payload is correct', () { final p = CommandPayloads.detachNode(unitId: 'u1', previousParentId: 'u0'); expect(p['unitId'], 'u1'); expect(p['previousParentId'], 'u0'); }); test('assignPersonnel payload is correct', () { final p = CommandPayloads.assignPersonnel( soldierId: 's1', slotId: 'sl1', unitId: 'u1', ); expect(p['soldierId'], 's1'); expect(p['slotId'], 'sl1'); expect(p['unitId'], 'u1'); }); test('unassignPersonnel payload is correct', () { final p = CommandPayloads.unassignPersonnel( soldierId: 's1', slotId: 'sl1', unitId: 'u1', ); expect(p['soldierId'], 's1'); expect(p['slotId'], 'sl1'); }); test('modifySlot payload includes changes map', () { final p = CommandPayloads.modifySlot( slotId: 'sl1', unitId: 'u1', changes: {'positionTitle': 'Squad Leader'}, ); expect(p['slotId'], 'sl1'); expect((p['changes'] as Map)['positionTitle'], 'Squad Leader'); }); }); // --------------------------------------------------------------------------- // CommandEventStore // --------------------------------------------------------------------------- group('CommandEventStore', () { late CommandEventStore store; setUp(() => store = CommandEventStore()); test('starts empty', () { expect(store.eventCount, 0); expect(store.pendingCount, 0); expect(store.allEvents, isEmpty); expect(store.pendingEvents, isEmpty); }); test('append adds event to store', () { final event = _makeEvent('e1', CommandEventType.attachNode); store.append(event); expect(store.eventCount, 1); expect(store.pendingCount, 1); }); test('allEvents is unmodifiable', () { store.append(_makeEvent('e1', CommandEventType.attachNode)); expect(() => store.allEvents.add(_makeEvent('e2', CommandEventType.detachNode)), throwsUnsupportedError); }); test('pendingEvents excludes synced events', () { final e1 = _makeEvent('e1', CommandEventType.attachNode); final e2 = _makeEvent('e2', CommandEventType.detachNode); store.append(e1); store.append(e2); store.markSynced('e1'); expect(store.pendingCount, 1); expect(store.pendingEvents.first.eventId, 'e2'); }); test('markSynced returns true for existing event', () { store.append(_makeEvent('e1', CommandEventType.attachNode)); expect(store.markSynced('e1'), true); expect(store.pendingCount, 0); }); test('markSynced returns false for unknown eventId', () { expect(store.markSynced('nonexistent'), false); }); test('markAllSynced marks every event', () { store.append(_makeEvent('e1', CommandEventType.attachNode)); store.append(_makeEvent('e2', CommandEventType.assignPersonnel)); store.append(_makeEvent('e3', CommandEventType.modifySlot)); store.markAllSynced(); expect(store.pendingCount, 0); }); test('exportPendingToJson produces valid JSON array', () { store.append(_makeEvent('e1', CommandEventType.attachNode)); store.append(_makeEvent('e2', CommandEventType.detachNode)); store.markSynced('e1'); // e1 is synced, only e2 should appear final json = store.exportPendingToJson(); final decoded = jsonDecode(json) as List; expect(decoded.length, 1); expect((decoded.first as Map)['eventId'], 'e2'); }); test('clear removes all events', () { store.append(_makeEvent('e1', CommandEventType.attachNode)); store.append(_makeEvent('e2', CommandEventType.detachNode)); store.clear(); expect(store.eventCount, 0); }); test('multiple event types can be stored together', () { for (final type in CommandEventType.values) { store.append(_makeEvent(type.name, type)); } expect(store.eventCount, CommandEventType.values.length); }); }); // --------------------------------------------------------------------------- // DeltaSynchronizer // --------------------------------------------------------------------------- group('DeltaSynchronizer', () { late CommandEventStore store; late DeltaSynchronizer sync; setUp(() { store = CommandEventStore(); sync = DeltaSynchronizer(store: store); }); test('syncIfOnline returns 0 when no pending events', () async { final count = await sync.syncIfOnline(); expect(count, 0); }); test('syncIfOnline returns 0 when offline', () async { store.append(_makeEvent('e1', CommandEventType.attachNode)); sync.isOnline = false; final count = await sync.syncIfOnline(); expect(count, 0); expect(store.pendingCount, 1); // still pending }); test('syncIfOnline transmits pending events when online', () async { store.append(_makeEvent('e1', CommandEventType.attachNode)); store.append(_makeEvent('e2', CommandEventType.assignPersonnel)); sync.isOnline = true; final count = await sync.syncIfOnline(); expect(count, 2); expect(store.pendingCount, 0); // all marked synced }); test('isOnline getter reflects set value', () { sync.isOnline = false; expect(sync.isOnline, false); sync.isOnline = true; expect(sync.isOnline, true); }); test('already-synced events are not re-transmitted', () async { final e1 = _makeEvent('e1', CommandEventType.attachNode); final e2 = _makeEvent('e2', CommandEventType.detachNode); store.append(e1); store.append(e2); store.markSynced('e1'); final count = await sync.syncIfOnline(); expect(count, 1); // only e2 was pending }); }); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- CommandEvent _makeEvent(String id, CommandEventType type) { return CommandEvent( eventId: id, type: type, payload: {'test': id}, occurredAt: DateTime.utc(2026, 8, 9), ); }