using Admin.NET.Plugin.AiDOP.Infrastructure; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.SmartOps; public class KpiAchievementEvaluationTests { private const string Higher = "higher_is_better"; private const string Lower = "lower_is_better"; [Theory] [InlineData(100, 100, "green", false)] [InlineData(99, 100, "yellow", false)] [InlineData(95, 100, "yellow", false)] [InlineData(94.99, 100, "red", false)] [InlineData(80, 100, "red", false)] [InlineData(79.99, 100, "red", true)] public void Higher_DefaultBoundaries(decimal current, decimal target, string status, bool blink) { AssertEval(current, target, Higher, status, blink); } [Theory] [InlineData(100, 100, "green", false)] [InlineData(100.01, 100, "yellow", false)] [InlineData(110, 100, "yellow", false)] [InlineData(110.01, 100, "red", false)] [InlineData(120, 100, "red", false)] [InlineData(120.01, 100, "red", true)] public void Lower_DefaultBoundaries(decimal current, decimal target, string status, bool blink) { AssertEval(current, target, Lower, status, blink); } [Theory] [InlineData(100, 100, "green", false)] [InlineData(90, 100, "yellow", false)] [InlineData(89.99, 100, "red", false)] [InlineData(70, 100, "red", false)] [InlineData(69.99, 100, "red", true)] [InlineData(80, 100, "red", false)] public void Higher_CustomYellow90Red70(decimal current, decimal target, string status, bool blink) { AssertEval(current, target, Higher, status, blink, 90m, 70m); } [Theory] [InlineData(100, 100, "green", false)] [InlineData(115, 100, "yellow", false)] [InlineData(115.01, 100, "red", false)] [InlineData(130, 100, "red", false)] [InlineData(130.01, 100, "red", true)] [InlineData(120, 100, "red", false)] public void Lower_CustomYellow115Red130(decimal current, decimal target, string status, bool blink) { AssertEval(current, target, Lower, status, blink, 115m, 130m); } [Fact] public void NullCurrent_SafeYellow() => AssertSafeYellow(null, 100m); [Fact] public void NullTarget_SafeYellow() => AssertSafeYellow(80m, null); [Fact] public void ZeroTarget_SafeYellow() => AssertSafeYellow(80m, 0m); private static void AssertSafeYellow(decimal? current, decimal? target) { var e = AidopS4KpiMerge.Evaluate(current, target, Higher); Assert.Equal("yellow", e.Status); Assert.Null(e.RatioPercent); Assert.False(e.IsBeyondRedThreshold); Assert.Equal("yellow", AidopS4KpiMerge.AchievementLevel(current, target, Higher)); } [Fact] public void IllegalHigher_RedGreaterThanYellow_FallsBackToDefaults() { var e = AidopS4KpiMerge.Evaluate(94.99m, 100m, Higher, 90m, 95m); Assert.Equal("red", e.Status); Assert.False(e.IsBeyondRedThreshold); Assert.Equal(95m, e.EffectiveYellowThreshold); Assert.Equal(80m, e.EffectiveRedThreshold); } [Fact] public void IllegalHigher_YellowAbove100_FallsBackToDefaults() { var e = AidopS4KpiMerge.Evaluate(96m, 100m, Higher, 101m, 80m); Assert.Equal("yellow", e.Status); Assert.Equal(95m, e.EffectiveYellowThreshold); Assert.Equal(80m, e.EffectiveRedThreshold); } [Fact] public void IllegalLower_YellowBelow100_FallsBackToDefaults() { var e = AidopS4KpiMerge.Evaluate(105m, 100m, Lower, 90m, 120m); Assert.Equal("yellow", e.Status); Assert.Equal(110m, e.EffectiveYellowThreshold); Assert.Equal(120m, e.EffectiveRedThreshold); } [Fact] public void IllegalLower_RedLessThanYellow_FallsBackToDefaults() { var e = AidopS4KpiMerge.Evaluate(112m, 100m, Lower, 115m, 110m); Assert.Equal("red", e.Status); Assert.False(e.IsBeyondRedThreshold); Assert.Equal(110m, e.EffectiveYellowThreshold); Assert.Equal(120m, e.EffectiveRedThreshold); } [Fact] public void SaveRejects_IllegalHigher_RedGreaterThanYellow() { Assert.False(AidopS4KpiMerge.TryValidateThresholds(Higher, 90m, 95m, out var error)); Assert.Equal("higher_is_better 阈值不合法,应满足 0 < RedThreshold <= YellowThreshold <= 100。", error); } [Fact] public void SaveRejects_IllegalHigher_YellowAbove100() { Assert.False(AidopS4KpiMerge.TryValidateThresholds(Higher, 101m, 80m, out var error)); Assert.Contains("higher_is_better", error); } [Fact] public void SaveRejects_IllegalLower_YellowBelow100() { Assert.False(AidopS4KpiMerge.TryValidateThresholds(Lower, 90m, 120m, out var error)); Assert.Equal("lower_is_better 阈值不合法,应满足 100 <= YellowThreshold <= RedThreshold。", error); } [Fact] public void SaveRejects_IllegalLower_RedLessThanYellow() { Assert.False(AidopS4KpiMerge.TryValidateThresholds(Lower, 115m, 110m, out var error)); Assert.Contains("lower_is_better", error); } [Fact] public void SaveAllows_BothThresholdsNull() { Assert.True(AidopS4KpiMerge.TryValidateThresholds(Higher, null, null, out var error)); Assert.Null(error); } [Fact] public void SaveAllows_SingleThresholdCombinedWithDefault() { Assert.True(AidopS4KpiMerge.TryValidateThresholds(Higher, 90m, null, out _)); Assert.True(AidopS4KpiMerge.TryValidateThresholds(Lower, null, 130m, out _)); } [Fact] public void SaveDoesNotSwap_IllegalPair() { Assert.False(AidopS4KpiMerge.TryValidateThresholds(Higher, 70m, 90m, out _)); var e = AidopS4KpiMerge.Evaluate(75m, 100m, Higher, 70m, 90m); Assert.Equal(95m, e.EffectiveYellowThreshold); Assert.Equal(80m, e.EffectiveRedThreshold); } [Fact] public void AchievementLevel_MatchesEvaluateStatus() { foreach (var (current, target, dir, y, r) in new (decimal, decimal, string, decimal?, decimal?)[] { (100m, 100m, Higher, null, null), (79.99m, 100m, Higher, null, null), (120.01m, 100m, Lower, null, null), (69.99m, 100m, Higher, 90m, 70m), (94.99m, 100m, Higher, 90m, 95m) }) { var eval = AidopS4KpiMerge.Evaluate(current, target, dir, y, r); Assert.Equal(eval.Status, AidopS4KpiMerge.AchievementLevel(current, target, dir, y, r)); } } private static void AssertEval( decimal current, decimal target, string direction, string status, bool blink, decimal? yellow = null, decimal? red = null) { var e = AidopS4KpiMerge.Evaluate(current, target, direction, yellow, red); Assert.Equal(status, e.Status); Assert.Equal(blink, e.IsBeyondRedThreshold); Assert.Equal(status == "red" && blink, e.Status == "red" && e.IsBeyondRedThreshold); Assert.Equal(e.Status, AidopS4KpiMerge.AchievementLevel(current, target, direction, yellow, red)); Assert.Equal(blink, AidopS4KpiMerge.IsBeyondRedThreshold(current, target, direction, yellow, red)); } }