using Admin.NET.Plugin.AiDOP.Dto.SmartOps; using Admin.NET.Plugin.AiDOP.Entity; using Admin.NET.Plugin.AiDOP.SmartOps; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.SmartOps; public class KpiTargetServiceTests { private static KpiTargetService Svc(FakeKpiTargetStore store) { store.Kpis.Add(new AdoSmartOpsKpiMaster { TenantId = 9, MetricCode = "S1_L1_001", ModuleCode = "S1", IsEnabled = true }); var resolver = new KpiTargetResolver(store, new LegacyKpiTargetProvider(new FakeDailyReader(), new Microsoft.Extensions.Logging.Abstractions.NullLogger()), new Microsoft.Extensions.Logging.Abstractions.NullLogger()); return new KpiTargetService(store, resolver, null!); } [Fact] public void Overlap_InclusiveBounds() { Assert.True(KpiTargetRules.Overlaps(new DateTime(2026, 1, 1), new DateTime(2026, 1, 31), new DateTime(2026, 1, 31), null)); Assert.False(KpiTargetRules.Overlaps(new DateTime(2026, 1, 1), new DateTime(2026, 1, 30), new DateTime(2026, 1, 31), null)); } [Fact] public async Task Create_RejectsNonPositive() { var svc = Svc(new FakeKpiTargetStore()); await Assert.ThrowsAsync(() => svc.CreateCoreAsync(9, new KpiTargetCreateDto { MetricCode = "S1_L1_001", FactoryId = 1, TargetValue = 0, EffectiveFrom = new DateTime(2026, 9, 1) }, KpiTargetConfigSource.Manual, null, 1)); } [Fact] public async Task Create_RejectsCrossTenantMetric() { var svc = Svc(new FakeKpiTargetStore()); await Assert.ThrowsAsync(() => svc.CreateCoreAsync(99, new KpiTargetCreateDto { MetricCode = "S1_L1_001", FactoryId = 1, TargetValue = 3, EffectiveFrom = new DateTime(2026, 9, 1) }, KpiTargetConfigSource.Manual, null, 1)); } [Fact] public async Task Create_RejectsOverlap() { var store = new FakeKpiTargetStore(); var svc = Svc(store); await svc.CreateCoreAsync(9, new KpiTargetCreateDto { MetricCode = "S1_L1_001", FactoryId = 1, TargetValue = 3, EffectiveFrom = new DateTime(2026, 9, 1) }, KpiTargetConfigSource.Manual, null, 1); await Assert.ThrowsAsync(() => svc.CreateCoreAsync(9, new KpiTargetCreateDto { MetricCode = "S1_L1_001", FactoryId = 1, TargetValue = 4, EffectiveFrom = new DateTime(2026, 9, 15) }, KpiTargetConfigSource.Manual, null, 1)); } [Fact] public async Task Create_ClosePrevious_AllowsFuture() { var store = new FakeKpiTargetStore(); var svc = Svc(store); await svc.CreateCoreAsync(9, new KpiTargetCreateDto { MetricCode = "S1_L1_001", FactoryId = 1, TargetValue = 3, EffectiveFrom = new DateTime(2026, 1, 1) }, KpiTargetConfigSource.Manual, null, 1); var next = await svc.CreateCoreAsync(9, new KpiTargetCreateDto { MetricCode = "S1_L1_001", FactoryId = 1, TargetValue = 4, EffectiveFrom = new DateTime(2026, 9, 1), ClosePrevious = true }, KpiTargetConfigSource.Manual, null, 1); Assert.Equal(4m, next.TargetValue); Assert.Equal(new DateTime(2026, 8, 31), store.Configs[0].EffectiveTo); } [Fact] public async Task Update_EffectiveTargetValue_Rejected() { var store = new FakeKpiTargetStore(); var svc = Svc(store); var created = await svc.CreateCoreAsync(9, new KpiTargetCreateDto { MetricCode = "S1_L1_001", FactoryId = 1, TargetValue = 3, EffectiveFrom = DateTime.Today.AddDays(-1) }, KpiTargetConfigSource.Manual, null, 1); await Assert.ThrowsAsync(() => svc.UpdateCoreAsync(9, created.Id, new KpiTargetUpdateDto { TargetValue = 9 })); } }