using Admin.NET.Plugin.AiDOP.Entity; using Admin.NET.Plugin.AiDOP.SmartOps; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.SmartOps; public class KpiTargetResolverTests { private static KpiTargetResolver Resolver(FakeKpiTargetStore store, FakeDailyReader daily) => new(store, new LegacyKpiTargetProvider(daily, NullLogger.Instance), NullLogger.Instance); [Fact] public async Task FactoryBeatsTenantDefault() { var store = new FakeKpiTargetStore(); store.Configs.Add(Cfg(1, 0, 10, new DateTime(2026, 1, 1))); store.Configs.Add(Cfg(2, 1, 3, new DateTime(2026, 1, 1))); var snap = await Resolver(store, new FakeDailyReader()).ResolveAsync(9, 1, "S1_L1_001", "S1", new DateTime(2026, 8, 1)); Assert.Equal(3m, snap.TargetValue); Assert.Equal(KpiTargetSnapshotSource.FormalFactory, snap.TargetSource); Assert.Equal(2, snap.TargetConfigId); } [Fact] public async Task InclusiveBounds_AndFutureNotActive() { var store = new FakeKpiTargetStore(); store.Configs.Add(Cfg(1, 1, 3, new DateTime(2026, 9, 1), new DateTime(2026, 9, 30))); var r = Resolver(store, new FakeDailyReader()); var onStart = await r.ResolveAsync(9, 1, "S1_L1_001", "S1", new DateTime(2026, 9, 1)); var onEnd = await r.ResolveAsync(9, 1, "S1_L1_001", "S1", new DateTime(2026, 9, 30)); var before = await r.ResolveAsync(9, 1, "S1_L1_001", "S1", new DateTime(2026, 8, 31)); Assert.Equal(3m, onStart.TargetValue); Assert.Equal(3m, onEnd.TargetValue); Assert.NotEqual(KpiTargetSnapshotSource.FormalFactory, before.TargetSource); } [Fact] public async Task SwitchDay_UsesNewConfig() { var store = new FakeKpiTargetStore(); store.Configs.Add(Cfg(1, 1, 3, new DateTime(2026, 1, 1), new DateTime(2026, 8, 31))); store.Configs.Add(Cfg(2, 1, 5, new DateTime(2026, 9, 1))); var r = Resolver(store, new FakeDailyReader()); var d1 = await r.ResolveAsync(9, 1, "S1_L1_001", "S1", new DateTime(2026, 8, 31)); var d2 = await r.ResolveAsync(9, 1, "S1_L1_001", "S1", new DateTime(2026, 9, 1)); Assert.Equal(3m, d1.TargetValue); Assert.Equal(5m, d2.TargetValue); } [Fact] public async Task DuplicateScope_Throws() { var store = new FakeKpiTargetStore(); store.Configs.Add(Cfg(1, 1, 3, new DateTime(2026, 1, 1))); store.Configs.Add(Cfg(2, 1, 4, new DateTime(2026, 1, 1))); await Assert.ThrowsAsync(() => Resolver(store, new FakeDailyReader()).ResolveAsync(9, 1, "S1_L1_001", "S1", new DateTime(2026, 8, 1))); } [Fact] public async Task Missing_ReturnsNull_NotZero() { var snap = await Resolver(new FakeKpiTargetStore(), new FakeDailyReader()) .ResolveAsync(9, 1, "S5_L1_001", "S5", DateTime.Today); Assert.Null(snap.TargetValue); Assert.Equal(KpiTargetSnapshotSource.Missing, snap.TargetSource); } [Fact] public async Task LegacyOnlyWhenNoFormal_S1() { var daily = new FakeDailyReader { Current = 8m }; var snap = await Resolver(new FakeKpiTargetStore(), daily) .ResolveAsync(9, 1, "S1_L1_001", "S1", DateTime.Today); Assert.Equal(8m, snap.TargetValue); Assert.Equal(KpiTargetSnapshotSource.LegacyDailyCurrent, snap.TargetSource); } [Fact] public async Task FormalBeatsLegacy() { var store = new FakeKpiTargetStore(); store.Configs.Add(Cfg(1, 1, 3, new DateTime(2026, 1, 1))); var daily = new FakeDailyReader { Current = 8m }; var snap = await Resolver(store, daily).ResolveAsync(9, 1, "S1_L1_001", "S1", DateTime.Today); Assert.Equal(3m, snap.TargetValue); Assert.Equal(KpiTargetSnapshotSource.FormalFactory, snap.TargetSource); } [Fact] public async Task S5_DoesNotUseLegacy() { var daily = new FakeDailyReader { Current = 8m, Prior = 7m }; var snap = await Resolver(new FakeKpiTargetStore(), daily) .ResolveAsync(9, 1, "S5_L1_001", "S5", DateTime.Today); Assert.Null(snap.TargetValue); } [Fact] public async Task CodeDefault_WhenNoDaily() { var snap = await Resolver(new FakeKpiTargetStore(), new FakeDailyReader()) .ResolveAsync(9, 1, "S1_L1_001", "S1", DateTime.Today); Assert.Equal(3m, snap.TargetValue); Assert.Equal(KpiTargetSnapshotSource.LegacyCode, snap.TargetSource); } [Fact] public void HistoricalDaily_NotMutatedByConfigInsert() { var store = new FakeKpiTargetStore(); var dailyValue = 9m; store.Configs.Add(Cfg(1, 1, 3, DateTime.Today)); Assert.Equal(9m, dailyValue); Assert.Single(store.Configs); } private static AdoSmartOpsKpiTargetConfig Cfg(long id, long factory, decimal value, DateTime from, DateTime? to = null) => new() { Id = id, TenantId = 9, FactoryId = factory, MetricCode = "S1_L1_001", TargetValue = value, EffectiveFrom = from, EffectiveTo = to, SourceType = KpiTargetConfigSource.Manual, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now }; }