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); /// Verifies that a root-level attribute becomes a single leaf. [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); } /// Verifies two sibling leaves share single branch sorted by segment. [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); } /// Verifies that a deep path nests branches by accumulated prefix. [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); } /// Verifies that branches are deduplicated across deep paths sharing a prefix. [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); } /// Verifies that a bad-quality descendant marks all ancestor branches as bad. [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); } /// Verifies that a branch whose leaves are all good is not marked as bad quality. [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); } /// Verifies that any non-good quality counts as bad when rolling up. /// The test input. [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); } /// Verifies that filtering keeps only the matching leaves and their ancestors. [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); } /// Verifies that filtering is case insensitive. [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); } /// Verifies that an empty or whitespace filter returns the full forest. /// The test input. [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); } /// Verifies that a filter with no matches returns an empty forest. [Fact] public void Filter_NoMatches_ReturnsEmptyForest() { var tree = DebugTreeBuilder.BuildAttributeTree( new[] { Attr("Motor1.Temp") }, "nonexistent"); Assert.Empty(tree); } /// Verifies that the top-level forest is sorted by segment. [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); } /// Verifies that empty input returns an empty forest. [Fact] public void EmptyInput_ReturnsEmptyForest() { var tree = DebugTreeBuilder.BuildAttributeTree(Array.Empty(), null); Assert.Empty(tree); } /// Verifies that bad quality four levels deep rolls up to the root. [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); } /// Verifies that a filter matching a deep leaf retains all its ancestor branches. [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); } // --------------------------------------------------------------------- // 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, }; /// Verifies that a computed alarm with a path-qualified name becomes a leaf under a branch. [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); } /// Verifies that a computed alarm with a root-level name becomes a single leaf. [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); } /// Verifies that a native condition nests under its binding node with the source reference segment. [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); } /// Verifies that two native conditions share the same binding node. [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); } /// Verifies that a native placeholder yields a binding node without children. [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); } /// Verifies that real conditions win over a placeholder under the same native binding. [Fact] public void Native_PlaceholderAndRealConditions_RealConditionsWin() { // 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); } /// Verifies that an active descendant sets the worst state to active and is counted when rolling up. [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); } /// Verifies that when every alarm is normal the worst state is normal with a zero active count. [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); } /// Verifies that a placeholder never counts as active when rolling up. [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); } /// Verifies that filtering on the source reference keeps the condition under its binding and ancestors. [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); } /// Verifies that filtering on a binding canonical name keeps the binding node. [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); } /// Verifies that filtering on a binding name that matches a placeholder keeps the binding node. [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); } /// Verifies that filtering on a computed alarm name is case insensitive. [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); } /// Verifies that an empty or whitespace filter returns the full alarm forest. /// The test input. [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); } /// Verifies that a filter with no matches returns an empty alarm forest. [Fact] public void Filter_NoMatches_ReturnsEmptyAlarmForest() { var tree = DebugTreeBuilder.BuildAlarmTree( new[] { Computed("Motor1.HighTemp") }, "nonexistent"); Assert.Empty(tree); } /// Verifies that mixed computed and native alarms produce both subtrees. [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); } /// Verifies that empty alarm input returns an empty forest. [Fact] public void EmptyAlarmInput_ReturnsEmptyForest() { var tree = DebugTreeBuilder.BuildAlarmTree(Array.Empty(), null); Assert.Empty(tree); } /// Verifies that computed and native alarms sharing a prefix keep distinct binding and leaf nodes. [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); } }