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;

/// <summary>
/// Pure unit tests for <see cref="DebugTreeBuilder.BuildAttributeTree"/> — 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.
/// </summary>
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<AttributeValueChanged>(), 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.
    // ---------------------------------------------------------------------

    /// <summary>Computed alarm: <c>AlarmName</c> is the path-qualified canonical leaf path.</summary>
    private static AlarmStateChanged Computed(string alarmName, AlarmState state = AlarmState.Active)
        => new("Inst", alarmName, state, Priority: 500, DateTimeOffset.UtcNow);

    /// <summary>
    /// Native condition (or placeholder) under a source binding identified by
    /// <paramref name="bindingCanonical"/>.
    /// </summary>
    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<AlarmStateChanged>(), 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);
    }
}
