import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ground_suite/features/command/presentation/tactical_command_page.dart'; import 'package:ground_suite/features/command/presentation/tactical_controller.dart'; import 'package:ground_suite/features/fleet/domain/fleet_unit.dart'; import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart'; void main() { ProviderContainer container() { final value = ProviderContainer(); addTearDown(value.dispose); return value; } test('selection and cursor modes have one deterministic owner', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.selectUnit('DEMO-1022'); expect(scope.read(tacticalControllerProvider).selectedUnitIds, { 'DEMO-1022', }); controller.selectUnit('DEMO-1023', toggle: true); expect(scope.read(tacticalControllerProvider).selectedUnitIds, { 'DEMO-1022', 'DEMO-1023', }); controller.beginPencil(); expect( scope.read(tacticalControllerProvider).mode, isA(), ); expect(scope.read(tacticalControllerProvider).selectedUnitIds, isEmpty); controller.showUnitHangar(); expect( scope.read(tacticalControllerProvider).panel, TacticalPanel.unitHangar, ); expect(scope.read(tacticalControllerProvider).unitDraft, isNull); final definition = supportedVehicleDefinitions.first; controller.chooseUnitDefinition(definition, displayName: 'QUADCOPTER-01'); expect( scope.read(tacticalControllerProvider).unitDraft?.definition, definition, ); controller.beginUnitSetup(); expect( scope.read(tacticalControllerProvider).panel, TacticalPanel.unitSetup, ); controller.completeUnitPlacement('local-unit-1'); expect(scope.read(tacticalControllerProvider).selectedUnitIds, { 'local-unit-1', }); expect(scope.read(tacticalControllerProvider).unitDraft, isNull); controller.showUnitSetupForSelectedLocalUnit('local-unit-1'); expect( scope.read(tacticalControllerProvider).panel, TacticalPanel.unitSetup, ); }); test( 'hangar drafts preserve the selected type and do not mutate map state', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); final before = scope.read(tacticalControllerProvider); controller.showUnitHangar(); controller.chooseUnitDefinition( supportedVehicleDefinitions[1], displayName: 'Fixed wing scout', ); controller.beginUnitSetup(); controller.updateUnitDraftName('Fixed wing north'); final editing = scope.read(tacticalControllerProvider); expect( identical(editing.selectedUnitIds, before.selectedUnitIds), isTrue, ); expect(identical(editing.drawings, before.drawings), isTrue); expect(identical(editing.markers, before.markers), isTrue); expect(editing.panel, TacticalPanel.unitSetup); controller.returnToUnitHangar(); final returned = scope.read(tacticalControllerProvider); expect(returned.panel, TacticalPanel.unitHangar); expect(returned.unitDraft?.definition, supportedVehicleDefinitions[1]); expect(returned.unitDraft?.displayName, 'Fixed wing north'); controller.showUnitSetupForSelectedLocalUnit('local-unit-1'); expect( scope.read(tacticalControllerProvider).panel, TacticalPanel.unitHangar, reason: 'A catalog draft cannot produce an unrelated blank setup panel.', ); }, ); test( 'editing a hangar draft does not notify the tactical render projection', () async { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); scope.read(tacticalSceneRenderStateProvider); var renderNotifications = 0; final subscription = scope.listen( tacticalSceneRenderStateProvider, (previous, next) => renderNotifications += 1, fireImmediately: true, ); addTearDown(subscription.close); renderNotifications = 0; controller.showUnitHangar(); await scope.pump(); controller.chooseUnitDefinition( supportedVehicleDefinitions.first, displayName: 'Quad local', ); await scope.pump(); controller.updateUnitDraftName('Quad north'); await scope.pump(); expect( renderNotifications, 1, reason: 'Opening the hangar changes its panel visibility.', ); controller.updateUnitDraftName('Quad north one'); await scope.pump(); expect( renderNotifications, 1, reason: 'Typing only updates the isolated local setup draft.', ); }, ); test( 'direct pencil strokes are session state and require complete geometry', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.beginPencil(); expect( controller.commitPencilStroke(const [LocalScenePoint(x: .1, y: .2)]), isFalse, ); expect( controller.commitPencilStroke(const [ LocalScenePoint(x: .1, y: .2), LocalScenePoint(x: .3, y: .4), ]), isTrue, ); final state = scope.read(tacticalControllerProvider); expect(state.mode, isA()); expect(state.drawings, hasLength(1)); expect(state.drawings.single.thickness, 3); controller.clearSceneObjects(); expect(scope.read(tacticalControllerProvider).drawings, isEmpty); }, ); test('view toggles remain separate from selection and pencil tools', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.beginPencil(); controller.toggleOverlay(TacticalMapOverlay.drones, false); final state = scope.read(tacticalControllerProvider); expect(state.mode, isA()); expect(state.overlays, isNot(contains(TacticalMapOverlay.drones))); }); test('fleet panel is separate from a local map mode', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.showFleetRoster(); expect( scope.read(tacticalControllerProvider).panel, TacticalPanel.fleetRoster, ); expect( scope.read(tacticalControllerProvider).mode, isA(), ); controller.beginPencil(); expect( scope.read(tacticalControllerProvider).mode, isA(), ); controller.closePanel(); expect(scope.read(tacticalControllerProvider).panel, isNull); }); test('drag selection is local only', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.selectUnits(['DEMO-1021', 'DEMO-1023']); final state = scope.read(tacticalControllerProvider); expect(state.selectedUnitIds, {'DEMO-1021', 'DEMO-1023'}); expect(state.mode, isA()); }); test('direct strokes are bounded before local scene commit', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.beginPencil(); final stroke = [ for (var index = 0; index < 256; index++) LocalScenePoint(x: index / 255, y: index.isEven ? .4 : .6), ]; expect(controller.commitPencilStroke(stroke), isTrue); expect( scope.read(tacticalControllerProvider).drawings.single.points.length, lessThanOrEqualTo(256), ); }); test( 'pencil style is retained for the current cursor and committed stroke', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.beginPencil(); controller.configurePencil( hue: 282, saturation: .22, brightness: .38, thickness: 8, ); expect( controller.commitPencilStroke(const [ LocalScenePoint(x: .2, y: .2), LocalScenePoint(x: .4, y: .4), ]), isTrue, ); final state = scope.read(tacticalControllerProvider); expect(state.mode, isA()); final mode = state.mode as TacticalPencilMode; expect(mode.hue, 282); expect(mode.saturation, .22); expect(mode.brightness, .38); expect(mode.thickness, 8); expect(state.drawings.single.hue, 282); expect(state.drawings.single.saturation, .22); expect(state.drawings.single.brightness, .38); expect(state.drawings.single.thickness, 8); }, ); test('marker names and colors are independent from pencil strokes', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.beginPencil(); controller.configurePencil(hue: 282, thickness: 8); controller.beginMarker(); controller.configureMarker(hue: 26, saturation: .41, brightness: .56); controller.chooseMarkerPoint(const LocalScenePoint(x: .4, y: .6)); expect(controller.commitMarkerName('Landing review'), isTrue); final state = scope.read(tacticalControllerProvider); expect(state.markers, hasLength(1)); expect(state.markers.single.name, 'Landing review'); expect(state.markers.single.hue, 26); expect(state.markers.single.saturation, .41); expect(state.markers.single.brightness, .56); expect((state.mode as TacticalMarkerMode).hue, 26); expect(state.drawings, isEmpty); }); test( 'blank marker labels and pending marker cancellation create no marker', () { final scope = container(); final controller = scope.read(tacticalControllerProvider.notifier); controller.beginMarker(); controller.chooseMarkerPoint(const LocalScenePoint(x: .2, y: .2)); expect(controller.commitMarkerName(' '), isFalse); expect(controller.cancelActiveTool(), isFalse); expect(scope.read(tacticalControllerProvider).markers, isEmpty); expect(controller.cancelActiveTool(), isTrue); expect( scope.read(tacticalControllerProvider).mode, isA(), ); }, ); }