using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment; /// /// Pure unit tests for — node /// model derivation, branch dedupe, child sort, bad-quality roll-up, and the /// name-contains filter. No bUnit / DI required; the builder is a pure function. /// public class DebugTreeBuilderTests { private static AttributeValueChanged Attr(string name, object? value = null, string quality = "Good") => new("Inst", name, name, value, quality, DateTimeOffset.UtcNow); [Fact] public void RootLevelAttribute_BecomesSingleLeaf() { var tree = DebugTreeBuilder.BuildAttributeTree(new[] { Attr("Speed") }, null); var node = Assert.Single(tree); Assert.Equal("Speed", node.Key); Assert.Equal("Speed", node.Segment); Assert.False(node.HasChildren); Assert.NotNull(node.Attribute); Assert.Equal("Speed", node.Attribute!.AttributeName); } [Fact] public void TwoSiblingLeaves_ShareSingleBranch_SortedBySegment() { // Provided out of order to prove children are sorted ordinally. var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Temp"), Attr("Motor1.Speed") }, null); var branch = Assert.Single(tree); Assert.Equal("Motor1", branch.Key); Assert.Equal("Motor1", branch.Segment); Assert.True(branch.HasChildren); Assert.Null(branch.Attribute); Assert.Equal(2, branch.Children.Count); Assert.Equal("Motor1.Speed", branch.Children[0].Key); Assert.Equal("Speed", branch.Children[0].Segment); Assert.Equal("Motor1.Temp", branch.Children[1].Key); Assert.Equal("Temp", branch.Children[1].Segment); } [Fact] public void DeepPath_NestsBranchesByAccumulatedPrefix() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Compressor.Pump") }, null); var motor = Assert.Single(tree); Assert.Equal("Motor1", motor.Key); Assert.True(motor.HasChildren); var compressor = Assert.Single(motor.Children); Assert.Equal("Motor1.Compressor", compressor.Key); Assert.Equal("Compressor", compressor.Segment); Assert.True(compressor.HasChildren); Assert.Null(compressor.Attribute); var pump = Assert.Single(compressor.Children); Assert.Equal("Motor1.Compressor.Pump", pump.Key); Assert.Equal("Pump", pump.Segment); Assert.False(pump.HasChildren); Assert.NotNull(pump.Attribute); } [Fact] public void BranchDedupe_SharedPrefixAcrossDeepPaths() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Compressor.Pump"), Attr("Motor1.Compressor.Valve"), Attr("Motor1.Speed"), }, null); var motor = Assert.Single(tree); Assert.Equal("Motor1", motor.Key); // Two direct children: "Compressor" (branch) and "Speed" (leaf), sorted. Assert.Equal(2, motor.Children.Count); Assert.Equal("Motor1.Compressor", motor.Children[0].Key); Assert.Equal("Motor1.Speed", motor.Children[1].Key); var compressor = motor.Children[0]; Assert.Equal(2, compressor.Children.Count); Assert.Equal("Motor1.Compressor.Pump", compressor.Children[0].Key); Assert.Equal("Motor1.Compressor.Valve", compressor.Children[1].Key); } [Fact] public void RollUp_BadQualityDescendant_MarksAllAncestorBranches() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Compressor.Pump", quality: "Bad"), Attr("Motor1.Speed", quality: "Good"), }, null); var motor = Assert.Single(tree); Assert.True(motor.HasBadQuality); var compressor = motor.Children.Single(c => c.Key == "Motor1.Compressor"); Assert.True(compressor.HasBadQuality); } [Fact] public void RollUp_AllGood_LeavesBranchHasBadQualityFalse() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Speed"), Attr("Motor1.Temp") }, null); var motor = Assert.Single(tree); Assert.False(motor.HasBadQuality); } [Theory] [InlineData("Uncertain")] [InlineData("Bad")] public void RollUp_NonGoodQuality_CountsAsBad(string quality) { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Speed", quality: quality) }, null); Assert.True(Assert.Single(tree).HasBadQuality); } [Fact] public void Filter_KeepsOnlyMatchingLeaves_AndTheirAncestors() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Temp"), Attr("Motor1.Speed"), Attr("Motor2.Pressure"), }, "temp"); // Only Motor1.Temp matches → just the Motor1 branch with one Temp leaf. var motor = Assert.Single(tree); Assert.Equal("Motor1", motor.Key); var leaf = Assert.Single(motor.Children); Assert.Equal("Motor1.Temp", leaf.Key); } [Fact] public void Filter_IsCaseInsensitive() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Temp"), Attr("Motor1.Speed") }, "TEMP"); var motor = Assert.Single(tree); Assert.Equal("Motor1.Temp", Assert.Single(motor.Children).Key); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Filter_EmptyOrWhitespace_ReturnsFullForest(string? filter) { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Temp"), Attr("Motor2.Pressure") }, filter); Assert.Equal(2, tree.Count); Assert.Equal("Motor1", tree[0].Key); Assert.Equal("Motor2", tree[1].Key); } [Fact] public void Filter_NoMatches_ReturnsEmptyForest() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Temp") }, "nonexistent"); Assert.Empty(tree); } [Fact] public void TopLevelForest_SortedBySegment() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Zeta"), Attr("Alpha"), Attr("Motor1.Speed") }, null); Assert.Equal(3, tree.Count); Assert.Equal("Alpha", tree[0].Key); Assert.Equal("Motor1", tree[1].Key); Assert.Equal("Zeta", tree[2].Key); } [Fact] public void EmptyInput_ReturnsEmptyForest() { var tree = DebugTreeBuilder.BuildAttributeTree(Array.Empty(), null); Assert.Empty(tree); } [Fact] public void RollUp_FourLevelDeepBadQuality_ReachesRoot() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("A.B.C.D", quality: "Bad") }, null); var a = Assert.Single(tree); Assert.True(a.HasBadQuality); var b = Assert.Single(a.Children); Assert.True(b.HasBadQuality); var c = Assert.Single(b.Children); Assert.True(c.HasBadQuality); var d = Assert.Single(c.Children); Assert.True(d.HasBadQuality); } [Fact] public void Filter_DeepLeafMatch_RetainsAllAncestorBranches() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Compressor.Pump"), Attr("Motor1.Speed") }, "Pump"); var motor = Assert.Single(tree); Assert.Equal("Motor1", motor.Key); var compressor = Assert.Single(motor.Children); Assert.Equal("Motor1.Compressor", compressor.Key); Assert.Equal("Motor1.Compressor.Pump", Assert.Single(compressor.Children).Key); } // --------------------------------------------------------------------- // DV-4: BuildAlarmTree — computed leaves, native condition groupings, // configured-placeholder binding nodes, roll-up, filter. // --------------------------------------------------------------------- /// Computed alarm: AlarmName is the path-qualified canonical leaf path. private static AlarmStateChanged Computed(string alarmName, AlarmState state = AlarmState.Active) => new("Inst", alarmName, state, Priority: 500, DateTimeOffset.UtcNow); /// /// Native condition (or placeholder) under a source binding identified by /// . /// private static AlarmStateChanged Native( string bindingCanonical, string sourceReference, AlarmState state = AlarmState.Active, bool placeholder = false, AlarmKind kind = AlarmKind.NativeOpcUa) => new("Inst", sourceReference, state, Priority: 500, DateTimeOffset.UtcNow) { Kind = kind, SourceReference = sourceReference, NativeSourceCanonicalName = bindingCanonical, IsConfiguredPlaceholder = placeholder, }; [Fact] public void Computed_PathQualifiedName_BecomesLeafUnderBranch() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Motor1.HighTemp") }, null); var motor = Assert.Single(tree); Assert.Equal("Motor1", motor.Key); Assert.Equal("Motor1", motor.Segment); Assert.True(motor.HasChildren); Assert.False(motor.IsNativeBinding); Assert.Null(motor.Alarm); var leaf = Assert.Single(motor.Children); Assert.Equal("Motor1.HighTemp", leaf.Key); Assert.Equal("HighTemp", leaf.Segment); Assert.False(leaf.HasChildren); Assert.NotNull(leaf.Alarm); Assert.Equal("Motor1.HighTemp", leaf.Alarm!.AlarmName); } [Fact] public void Computed_RootLevelName_BecomesSingleLeaf() { var tree = DebugTreeBuilder.BuildAlarmTree(new[] { Computed("Overheat") }, null); var node = Assert.Single(tree); Assert.Equal("Overheat", node.Key); Assert.Equal("Overheat", node.Segment); Assert.False(node.HasChildren); Assert.NotNull(node.Alarm); } [Fact] public void Native_Condition_NestsUnderBindingNode_WithSourceReferenceSegment() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "Tank1.Level.HiHi") }, null); var tank = Assert.Single(tree); Assert.Equal("Tank1", tank.Key); Assert.False(tank.IsNativeBinding); var binding = Assert.Single(tank.Children); Assert.Equal("Tank1.Levels", binding.Key); Assert.Equal("Levels", binding.Segment); Assert.True(binding.IsNativeBinding); Assert.Null(binding.Alarm); var condition = Assert.Single(binding.Children); Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", condition.Key); Assert.Equal("Tank1.Level.HiHi", condition.Segment); Assert.False(condition.HasChildren); Assert.NotNull(condition.Alarm); Assert.Equal("Tank1.Level.HiHi", condition.Alarm!.SourceReference); } [Fact] public void Native_TwoConditions_ShareSameBindingNode() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "Tank1.Level.HiHi"), Native("Tank1.Levels", "Tank1.Level.LoLo"), }, null); var tank = Assert.Single(tree); var binding = Assert.Single(tank.Children); Assert.Equal("Tank1.Levels", binding.Key); Assert.True(binding.IsNativeBinding); // ONE binding node carrying two condition children (sorted by SourceReference). Assert.Equal(2, binding.Children.Count); Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", binding.Children[0].Key); Assert.Equal("Tank1.Levels::Tank1.Level.LoLo", binding.Children[1].Key); } [Fact] public void Native_Placeholder_YieldsChildlessBindingNode() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "ignored", placeholder: true) }, null); var tank = Assert.Single(tree); var binding = Assert.Single(tank.Children); Assert.Equal("Tank1.Levels", binding.Key); Assert.True(binding.IsNativeBinding); Assert.False(binding.HasChildren); Assert.Empty(binding.Children); } [Fact] public void Native_PlaceholderAndRealConditions_RealConditionsWin() { // DV-2 shouldn't emit both, but be safe: placeholder adds nothing. var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "ignored", placeholder: true), Native("Tank1.Levels", "Tank1.Level.HiHi"), }, null); var tank = Assert.Single(tree); var binding = Assert.Single(tank.Children); Assert.True(binding.IsNativeBinding); var condition = Assert.Single(binding.Children); Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", condition.Key); } [Fact] public void RollUp_ActiveDescendant_SetsWorstStateActiveAndCounts() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "Tank1.Level.HiHi", AlarmState.Active), Native("Tank1.Levels", "Tank1.Level.LoLo", AlarmState.Normal), Computed("Tank1.Overflow", AlarmState.Active), }, null); var tank = Assert.Single(tree); Assert.Equal(AlarmState.Active, tank.WorstState); Assert.Equal(2, tank.ActiveCount); // one native HiHi + one computed Overflow var binding = tank.Children.Single(c => c.IsNativeBinding); Assert.Equal(AlarmState.Active, binding.WorstState); Assert.Equal(1, binding.ActiveCount); } [Fact] public void RollUp_AllNormal_WorstStateNormalAndZeroCount() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Tank1.Overflow", AlarmState.Normal) }, null); var tank = Assert.Single(tree); Assert.Equal(AlarmState.Normal, tank.WorstState); Assert.Equal(0, tank.ActiveCount); } [Fact] public void RollUp_Placeholder_NeverCountsAsActive() { // A placeholder must not contribute to ActiveCount or WorstState even // though its underlying State is Active (the factory default). var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "ignored", AlarmState.Active, placeholder: true) }, null); var tank = Assert.Single(tree); Assert.Equal(AlarmState.Normal, tank.WorstState); Assert.Equal(0, tank.ActiveCount); var binding = Assert.Single(tank.Children); Assert.Equal(AlarmState.Normal, binding.WorstState); Assert.Equal(0, binding.ActiveCount); } [Fact] public void Filter_OnSourceReference_KeepsConditionUnderBindingAndAncestors() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "Tank1.Level.HiHi"), Native("Tank1.Levels", "Tank1.Level.LoLo"), }, "HiHi"); var tank = Assert.Single(tree); Assert.Equal("Tank1", tank.Key); var binding = Assert.Single(tank.Children); Assert.Equal("Tank1.Levels", binding.Key); Assert.True(binding.IsNativeBinding); var condition = Assert.Single(binding.Children); Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", condition.Key); } [Fact] public void Filter_OnBindingCanonicalName_KeepsBindingNode() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "Tank1.Level.HiHi"), Computed("Motor1.HighTemp"), }, "Tank1.Levels"); // Only the native binding matches (via NativeSourceCanonicalName). var tank = Assert.Single(tree); Assert.Equal("Tank1", tank.Key); var binding = Assert.Single(tank.Children); Assert.Equal("Tank1.Levels", binding.Key); Assert.True(binding.IsNativeBinding); Assert.Single(binding.Children); } [Fact] public void Filter_OnBindingName_MatchingPlaceholder_KeepsBindingNode() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Native("Tank1.Levels", "ignored", placeholder: true) }, "Levels"); var tank = Assert.Single(tree); var binding = Assert.Single(tank.Children); Assert.Equal("Tank1.Levels", binding.Key); Assert.True(binding.IsNativeBinding); Assert.False(binding.HasChildren); } [Fact] public void Filter_OnComputedAlarmName_IsCaseInsensitive() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Motor1.HighTemp"), Computed("Motor1.LowPressure") }, "hightemp"); var motor = Assert.Single(tree); Assert.Equal("Motor1.HighTemp", Assert.Single(motor.Children).Key); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Filter_EmptyOrWhitespace_ReturnsFullAlarmForest(string? filter) { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Motor1.HighTemp"), Native("Tank1.Levels", "Tank1.Level.HiHi") }, filter); Assert.Equal(2, tree.Count); Assert.Equal("Motor1", tree[0].Key); Assert.Equal("Tank1", tree[1].Key); } [Fact] public void Filter_NoMatches_ReturnsEmptyAlarmForest() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Motor1.HighTemp") }, "nonexistent"); Assert.Empty(tree); } [Fact] public void MixedComputedAndNative_ProduceBothSubtrees() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Motor1.HighTemp"), Native("Tank1.Levels", "Tank1.Level.HiHi", kind: AlarmKind.NativeMxAccess), }, null); Assert.Equal(2, tree.Count); var motor = tree.Single(n => n.Key == "Motor1"); var computedLeaf = Assert.Single(motor.Children); Assert.Equal("Motor1.HighTemp", computedLeaf.Key); Assert.False(computedLeaf.HasChildren); var tank = tree.Single(n => n.Key == "Tank1"); var binding = Assert.Single(tank.Children); Assert.True(binding.IsNativeBinding); Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", Assert.Single(binding.Children).Key); } [Fact] public void EmptyAlarmInput_ReturnsEmptyForest() { var tree = DebugTreeBuilder.BuildAlarmTree(Array.Empty(), null); Assert.Empty(tree); } [Fact] public void Native_ComputedAndNativeSharePrefix_DistinctBindingAndLeaf() { // Tank1 has a computed alarm AND a native binding — both under one Tank1 branch. var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Tank1.Overflow"), Native("Tank1.Levels", "Tank1.Level.HiHi"), }, null); var tank = Assert.Single(tree); Assert.Equal(2, tank.Children.Count); var binding = tank.Children.Single(c => c.IsNativeBinding); Assert.Equal("Tank1.Levels", binding.Key); var computed = tank.Children.Single(c => !c.IsNativeBinding); Assert.Equal("Tank1.Overflow", computed.Key); Assert.NotNull(computed.Alarm); } }