using System; using System.Collections.Generic; using System.IO; using CounterDrone.Core; using CounterDrone.Core.Models; using CounterDrone.Core.Repository; using CounterDrone.Core.Services; using SQLite; using Xunit; using ScenarioStatus = CounterDrone.Core.Models.ScenarioStatus; namespace CounterDrone.Core.Tests { public class ScenarioServiceTests : IDisposable { private readonly string _testDir; private readonly SQLiteConnection _db; private readonly IScenarioService _service; public ScenarioServiceTests() { _testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}"); var paths = new TestPathProvider(_testDir); var dbManager = new DatabaseManager(paths); _db = dbManager.OpenMainDb(); _service = new ScenarioService( new ScenarioRepository(_db), new CombatSceneRepository(_db), new ControlZoneRepository(_db), new ScenarioDroneRepository(_db), new ScenarioUnitRepository(_db), new CloudDispersalRepository(_db), new RoutePlanRepository(_db), new WaypointRepository(_db)); } public void Dispose() { _db?.Close(); if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); } // ========== scenario CRUD ========== [Fact] public void CreateScenario_Valid_ReturnsScenario() { var scenario = _service.CreateScenario("测试任务", "SIM-20260611-001"); Assert.NotNull(scenario); Assert.Equal("测试任务", scenario.Name); Assert.Equal("SIM-20260611-001", scenario.ScenarioNumber); Assert.Equal((int)ScenarioStatus.Configuring, scenario.Status); Assert.Equal(1, scenario.CurrentStep); Assert.NotEmpty(scenario.Id); } [Fact] public void CreateScenario_AutoGeneratesScenarioNumber() { var scenario = _service.CreateScenario("自动编号", ""); Assert.StartsWith("SIM-", scenario.ScenarioNumber); } [Fact] public void CreateScenario_EmptyName_Throws() { Assert.Throws(() => _service.CreateScenario("", "SIM-001")); } [Fact] public void CreateScenario_DuplicateNumber_Throws() { _service.CreateScenario("scenario A", "SIM-001"); Assert.Throws(() => _service.CreateScenario("scenario B", "SIM-001")); } [Fact] public void DeleteScenario_RemovesScenario() { var scenario = _service.CreateScenario("ToDelete", ""); _service.DeleteScenario(scenario.Id); var detail = _service.GetScenarioDetail(scenario.Id); Assert.Null(detail); } [Fact] public void GetScenarioDetail_ReturnsFullConfig() { var scenario = _service.CreateScenario("Full Config", ""); // 保存各步骤配置 _service.SaveScene(scenario.Id, new CombatScene { SceneType = (int)SceneType.Mountain, WindSpeed = 10.0, Temperature = 25.0, }); _service.SaveScenarioDrone(scenario.Id, TestData.CreateScenarioDrone("default", 3)); _service.SaveCloudDispersal(scenario.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, Duration = 45.0, }); _service.UpdateStep(scenario.Id, 4); // 读取完整配置 var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal("Full Config", detail.Info.Name); Assert.Equal(4, detail.Info.CurrentStep); Assert.Equal((int)SceneType.Mountain, detail.Scene.SceneType); Assert.Equal(10.0, detail.Scene.WindSpeed); Assert.Equal(3, detail.Drones[0].Quantity); Assert.Equal((int)AerosolType.ActiveMaterial, detail.Cloud.AerosolType); } // ========== Step Save ========== [Fact] public void SaveScene_FirstTime_Inserts() { var scenario = _service.CreateScenario("Scene Test", ""); _service.SaveScene(scenario.Id, new CombatScene { WeatherType = (int)WeatherType.Fog, Visibility = 2000.0, TimeOfDay = "03:00", }); var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal((int)WeatherType.Fog, detail.Scene.WeatherType); Assert.Equal(2000.0, detail.Scene.Visibility); Assert.Equal("03:00", detail.Scene.TimeOfDay); } [Fact] public void SaveScene_SecondTime_Updates() { var scenario = _service.CreateScenario("Update Scene", ""); _service.SaveScene(scenario.Id, new CombatScene { WindSpeed = 5.0 }); _service.SaveScene(scenario.Id, new CombatScene { WindSpeed = 15.0 }); var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal(15.0, detail.Scene.WindSpeed); } [Fact] public void SaveControlZones_SavesAndRetrieves() { var scenario = _service.CreateScenario("Zone Test", ""); var zones = new List { new ControlZone { Name = "禁区A", VerticesJson = "[{\"x\":0,\"y\":0,\"z\":0},{\"x\":100,\"y\":0,\"z\":0},{\"x\":100,\"y\":100,\"z\":0}]", MinAltitude = 0, MaxAltitude = 500 }, new ControlZone { Name = "禁区B", VerticesJson = "[{\"x\":200,\"y\":200,\"z\":0}]", MinAltitude = 100, MaxAltitude = 300 }, }; _service.SaveControlZones(scenario.Id, zones); var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal(2, detail.ControlZones.Count); Assert.Equal("禁区A", detail.ControlZones[0].Name); Assert.Equal(0, detail.ControlZones[0].OrderIndex); Assert.Equal("禁区B", detail.ControlZones[1].Name); Assert.Equal(1, detail.ControlZones[1].OrderIndex); } [Fact] public void SaveControlZones_ReplaceExisting() { var scenario = _service.CreateScenario("Zone Replace", ""); _service.SaveControlZones(scenario.Id, new List { new ControlZone { Name = "Old" } }); _service.SaveControlZones(scenario.Id, new List { new ControlZone { Name = "New1" }, new ControlZone { Name = "New2" }, }); var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal(2, detail.ControlZones.Count); } [Fact] public void SaveScenarioDrone_SavesCorrectly() { var scenario = _service.CreateScenario("Drone Test", ""); _service.SaveScenarioDrone(scenario.Id, TestData.CreateScenarioDrone("default", 5)); var detail = _service.GetScenarioDetail(scenario.Id); var target = detail.Drones[0]; Assert.Equal((int)DroneType.FixedWing, TestData.DefaultDroneSpec.DroneType); Assert.Equal(5, target.Quantity); Assert.Equal(200.0, TestData.DefaultDroneSpec.TypicalSpeed); } [Fact] public void SaveDeployment_SavesMultiple() { var scenario = _service.CreateScenario("Equip Test", ""); var equips = new List { TestData.CreateScenarioUnit(1000, 0, 50, 0, 3), TestData.CreateScenarioUnit(0, 0, 50, 0, null), }; _service.SaveDeployment(scenario.Id, equips); var detail = _service.GetScenarioDetail(scenario.Id); Assert.NotEmpty(detail.Units); var platform = detail.Units.Find(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform); Assert.NotNull(platform); Assert.Equal(1, platform.Quantity); Assert.Equal(800.0, TestData.DefaultFuSpec.MuzzleVelocity); } [Fact] public void SaveRoute_SavesWaypoints() { var scenario = _service.CreateScenario("Route Test", ""); var route = new RoutePlan { FormationMode = (int)FormationMode.Formation, LateralSpacing = 60.0, }; var waypoints = new List { new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 500, Speed = 100 }, new Waypoint { PosX = 5000, PosY = 0, PosZ = 100, Altitude = 500, Speed = 100 }, new Waypoint { PosX = 10000, PosY = 0, PosZ = 100, Altitude = 300, Speed = 80 }, }; _service.SaveRoute(scenario.Id, "default", route, waypoints); var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal((int)FormationMode.Formation, detail.Routes[0].FormationMode); Assert.Equal(3, detail.WaypointGroups["default"].Count); Assert.Equal(0, detail.WaypointGroups["default"][0].OrderIndex); Assert.Equal(2, detail.WaypointGroups["default"][2].OrderIndex); Assert.Equal(5000.0, detail.WaypointGroups["default"][1].PosX); } // ========== Search & Pagination ========== [Fact] public void SearchScenarios_KeywordFilter() { _service.CreateScenario("城市防御演习", ""); _service.CreateScenario("平原拦截测试", ""); _service.CreateScenario("海岸边防演练", ""); var result = _service.SearchScenarios("城市", null, null, 1, 10); Assert.Equal(1, result.TotalCount); Assert.Equal("城市防御演习", result.Items[0].Name); } [Fact] public void SearchScenarios_NoResults() { _service.CreateScenario("scenario A", ""); var result = _service.SearchScenarios("不存在的任务", null, null, 1, 10); Assert.Equal(0, result.TotalCount); } [Fact] public void SearchScenarios_Pagination() { for (int i = 0; i < 15; i++) _service.CreateScenario($"scenario {i:D2}", ""); var page1 = _service.SearchScenarios(null, null, null, 1, 5); Assert.Equal(21, page1.TotalCount); // 15 + 6 预设想定 Assert.Equal(5, page1.Items.Count); Assert.Equal(5, page1.TotalPages); // 21 ÷ 5 = 5 页 var page3 = _service.SearchScenarios(null, null, null, 3, 5); Assert.Equal(5, page3.Items.Count); } [Fact] public void SearchScenarios_DateRangeFilter() { // All scenarios created today, so date filter should include them _service.CreateScenario("Recent scenario", ""); var yesterday = DateTime.UtcNow.AddDays(-1).ToString("yyyy-MM-dd"); var tomorrow = DateTime.UtcNow.AddDays(1).ToString("yyyy-MM-dd"); var result = _service.SearchScenarios(null, yesterday, tomorrow, 1, 10); Assert.Equal(7, result.TotalCount); // 1 + 6 预设想定 var oldResult = _service.SearchScenarios(null, "2020-01-01", "2020-12-31", 1, 10); Assert.Equal(0, oldResult.TotalCount); } // ========== UpdateStep ========== [Fact] public void UpdateStep_ValidSteps() { var scenario = _service.CreateScenario("Step Test", ""); Assert.Equal(1, scenario.CurrentStep); _service.UpdateStep(scenario.Id, 3); var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal(3, detail.Info.CurrentStep); _service.UpdateStep(scenario.Id, 5); detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal(5, detail.Info.CurrentStep); } [Fact] public void UpdateStep_ClampedToRange() { var scenario = _service.CreateScenario("Clamp Test", ""); _service.UpdateStep(scenario.Id, 0); Assert.Equal(1, _service.GetScenarioDetail(scenario.Id).Info.CurrentStep); _service.UpdateStep(scenario.Id, 10); Assert.Equal(5, _service.GetScenarioDetail(scenario.Id).Info.CurrentStep); } // ========== Full Workflow ========== [Fact] public void FullWorkflow_CreateConfigureSearch() { // 1. 创建任务 var scenario = _service.CreateScenario("完整流程测试", ""); Assert.NotNull(scenario); // 2. 步骤1:作战场景 _service.SaveScene(scenario.Id, new CombatScene { SceneType = (int)SceneType.Urban, WeatherType = (int)WeatherType.Rain, WindSpeed = 8.0, WindDirection = (int)WindDirection.SE, Temperature = 18.0, TimeOfDay = "14:00", }); _service.UpdateStep(scenario.Id, 1); // 3. 步骤1扩展:管控区域 _service.SaveControlZones(scenario.Id, new List { new ControlZone { Name = "核心区", VerticesJson = "[{\"x\":0,\"y\":0,\"z\":0},{\"x\":500,\"y\":0,\"z\":0},{\"x\":500,\"y\":500,\"z\":0},{\"x\":0,\"y\":500,\"z\":0}]", MinAltitude = 0, MaxAltitude = 1000, } }); // 4. 步骤2:目标配置 _service.SaveScenarioDrone(scenario.Id, TestData.CreateScenarioDrone("default", 2)); _service.UpdateStep(scenario.Id, 2); // 5. 步骤3:装备部署 _service.SaveDeployment(scenario.Id, new List { TestData.CreateScenarioUnit(0, 0, 50, 0, null), TestData.CreateScenarioUnit(800, 0, 50, 0, 2), }); _service.UpdateStep(scenario.Id, 3); // 6. 步骤4:云团抛撒 _service.SaveCloudDispersal(scenario.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, TriggerMode = (int)TriggerMode.Area, Duration = 50.0, PositionX = 3000, PositionY = 0, PositionZ = 300, }); _service.UpdateStep(scenario.Id, 4); // 7. 步骤5:航路规划 _service.SaveRoute(scenario.Id, "default", new RoutePlan { FormationMode = (int)FormationMode.Swarm, LateralSpacing = 30.0, }, new List { new Waypoint { PosX = 0, PosY = 100, PosZ = 200, Altitude = 600, Speed = 250 }, new Waypoint { PosX = 10000, PosY = 100, PosZ = 200, Altitude = 600, Speed = 250 }, }); _service.UpdateStep(scenario.Id, 5); // 8. 验证完整配置 var detail = _service.GetScenarioDetail(scenario.Id); Assert.Equal("完整流程测试", detail.Info.Name); Assert.Equal(5, detail.Info.CurrentStep); Assert.Equal((int)ScenarioStatus.Configuring, detail.Info.Status); // 场景 Assert.Equal((int)SceneType.Urban, detail.Scene.SceneType); Assert.Equal((int)WeatherType.Rain, detail.Scene.WeatherType); Assert.Equal("14:00", detail.Scene.TimeOfDay); // 管控区域 Assert.Single(detail.ControlZones); Assert.Equal("核心区", detail.ControlZones[0].Name); // 目标 Assert.Single(detail.Drones); Assert.Equal(2, detail.Drones[0].Quantity); Assert.Equal(200.0, TestData.DefaultDroneSpec.TypicalSpeed); // 装备 Assert.NotEmpty(detail.Units); // 云团 Assert.Equal((int)AerosolType.ActiveMaterial, detail.Cloud.AerosolType); Assert.Equal(50.0, detail.Cloud.Duration); // 航路 Assert.Equal((int)FormationMode.Swarm, detail.Routes[0].FormationMode); Assert.Equal(2, detail.WaypointGroups["default"].Count); Assert.Equal(250.0, detail.WaypointGroups["default"][0].Speed); // 9. 搜索验证 var searchResult = _service.SearchScenarios("完整流程", null, null, 1, 10); Assert.Equal(1, searchResult.TotalCount); } // ═══════════════════════════════════════ // 探测设备独立 CRUD // ═══════════════════════════════════════ [Fact] public void AddDetection_PersistsAndQueryable() { var scenario = _service.CreateScenario("探测设备测试", ""); _service.AddDetection(scenario.Id, TestData.CreateDetectionUnit(5000, 0, 0)); var detections = _service.GetDetections(scenario.Id); Assert.Single(detections); Assert.Equal((int)EquipmentRole.Detection, detections[0].EquipmentRole); Assert.Equal(5000.0, detections[0].PositionX); } [Fact] public void GetDetections_FiltersOutLaunchPlatforms() { var scenario = _service.CreateScenario("探测过滤测试", ""); // 一个火力单元 _service.SaveDeployment(scenario.Id, new List { TestData.CreateScenarioUnit(1000, 0, 50, 0, null), }); // 两个探测设备(独立添加) _service.AddDetection(scenario.Id, TestData.CreateScenarioUnit(0, 0, 50, 0, null)); _service.AddDetection(scenario.Id, TestData.CreateScenarioUnit(0, 0, 50, 0, null)); var detections = _service.GetDetections(scenario.Id); Assert.Equal(2, detections.Count); // 只有探测设备,不含火力单元 } [Fact] public void DeleteDetection_RemovesOnlyOne() { var scenario = _service.CreateScenario("探测删除测试", ""); _service.AddDetection(scenario.Id, TestData.CreateScenarioUnit(0, 0, 50, 0, null)); var det2 = TestData.CreateScenarioUnit(0, 0, 50, 0, null); _service.AddDetection(scenario.Id, det2); _service.DeleteDetection(det2.Id); var detections = _service.GetDetections(scenario.Id); Assert.Single(detections); Assert.Equal(5000.0, detections[0].PositionX); } [Fact] public void GetScenarioDetail_IncludesDetections() { var scenario = _service.CreateScenario("想定含探测", ""); _service.SaveDeployment(scenario.Id, new List { TestData.CreateScenarioUnit(0, 0, 50, 0, null), }); _service.AddDetection(scenario.Id, TestData.CreateScenarioUnit(0, 0, 50, 0, null)); var detail = _service.GetScenarioDetail(scenario.Id); // Equipment 包含火力单元 + 探测设备 Assert.NotEmpty(detail.Units); Assert.Contains(detail.Units, e => e.EquipmentRole == (int)EquipmentRole.Detection); Assert.Contains(detail.Units, e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform); } } }