using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.GalaxyRepository.Grpc;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Dashboard;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;

/// <summary>
///     Unit tests for the pure projection/formatting helpers behind the
///     dashboard Browse and Alarms tabs.
/// </summary>
public sealed class DashboardBrowseAndAlarmModelTests
{
    /// <summary>Verifies that the tree builder links children to parents and promotes orphans to roots.</summary>
    [Fact]
    public void BuildTree_LinksChildrenToParents_AndPromotesOrphansToRoots()
    {
        GalaxyObject area = new() { GobjectId = 1, BrowseName = "AreaA", IsArea = true, ParentGobjectId = 0 };
        GalaxyObject child = new() { GobjectId = 2, BrowseName = "Pump01", ParentGobjectId = 1 };
        GalaxyObject orphan = new() { GobjectId = 3, BrowseName = "Lost", ParentGobjectId = 99 };

        IReadOnlyList<DashboardBrowseNode> roots = DashboardBrowseTreeBuilder.Build([area, child, orphan]);

        // The area and the orphan (its parent id is absent) are both roots.
        Assert.Equal(2, roots.Count);
        DashboardBrowseNode areaNode = Assert.Single(roots, node => node.Object.GobjectId == 1);
        Assert.Single(areaNode.Children);
        Assert.Equal(2, areaNode.Children[0].Object.GobjectId);
        Assert.Contains(roots, node => node.Object.GobjectId == 3);
    }

    /// <summary>Verifies that the tree builder sorts areas before non-area objects.</summary>
    [Fact]
    public void BuildTree_SortsAreasBeforeObjects()
    {
        GalaxyObject instance = new() { GobjectId = 1, BrowseName = "Zeta", IsArea = false };
        GalaxyObject areaB = new() { GobjectId = 2, BrowseName = "Beta", IsArea = true };

        IReadOnlyList<DashboardBrowseNode> roots = DashboardBrowseTreeBuilder.Build([instance, areaB]);

        Assert.Equal(2, roots.Count);
        Assert.True(roots[0].IsArea);
        Assert.Equal("Beta", roots[0].DisplayName);
        Assert.False(roots[1].IsArea);
    }

    /// <summary>Verifies that the formatter renders boolean values correctly.</summary>
    /// <param name="input">The boolean input value.</param>
    /// <param name="expected">The expected formatted output.</param>
    [Theory]
    [InlineData(true, "true")]
    [InlineData(false, "false")]
    public void FormatValue_FormatsBooleans(bool input, string expected)
    {
        MxValue value = new() { DataType = MxDataType.Boolean, BoolValue = input };
        Assert.Equal(expected, DashboardMxValueFormatter.FormatValue(value));
    }

    /// <summary>Verifies that the formatter renders numbers and strings correctly.</summary>
    [Fact]
    public void FormatValue_FormatsNumbersAndStrings()
    {
        Assert.Equal("42", DashboardMxValueFormatter.FormatValue(new MxValue { Int32Value = 42 }));
        Assert.Equal("hello", DashboardMxValueFormatter.FormatValue(new MxValue { StringValue = "hello" }));
    }

    /// <summary>Verifies that the formatter handles null payloads and null references.</summary>
    [Fact]
    public void FormatValue_HandlesNullPayloadAndNullReference()
    {
        Assert.Equal("-", DashboardMxValueFormatter.FormatValue(null));
        Assert.Equal("(null)", DashboardMxValueFormatter.FormatValue(new MxValue { IsNull = true }));
    }

    /// <summary>Verifies that tag values from successful reads mark good quality.</summary>
    [Fact]
    public void TagValue_FromSuccessfulReadResult_MarksGoodQuality()
    {
        BulkReadResult result = new()
        {
            TagAddress = "Galaxy!Area.Tag",
            WasSuccessful = true,
            Quality = 192,
            Value = new MxValue { DataType = MxDataType.Double, DoubleValue = 1.5 },
        };

        DashboardTagValue value = DashboardTagValue.FromBulkReadResult(result);

        Assert.True(value.Ok);
        Assert.True(value.QualityGood);
        Assert.Equal("1.5", value.ValueText);
        Assert.Null(value.Error);
    }

    /// <summary>Verifies that tag values from failed reads carry the error message.</summary>
    [Fact]
    public void TagValue_FromFailedReadResult_CarriesError()
    {
        BulkReadResult result = new()
        {
            TagAddress = "Galaxy!Area.Bad",
            WasSuccessful = false,
            Quality = 0,
            ErrorMessage = "invalid handle",
        };

        DashboardTagValue value = DashboardTagValue.FromBulkReadResult(result);

        Assert.False(value.Ok);
        Assert.False(value.QualityGood);
        Assert.Equal("invalid handle", value.Error);
    }

    /// <summary>Verifies that active alarms parse provider and acknowledgement state from snapshots.</summary>
    [Fact]
    public void ActiveAlarm_FromSnapshot_ParsesProviderAndAcknowledgementState()
    {
        ActiveAlarmSnapshot unacked = new()
        {
            AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
            Category = "TestArea",
            CurrentState = AlarmConditionState.Active,
            Severity = 500,
        };
        ActiveAlarmSnapshot acked = new()
        {
            AlarmFullReference = "Galaxy!TestArea.TestMachine_002.TestAlarm001",
            CurrentState = AlarmConditionState.ActiveAcked,
        };

        DashboardActiveAlarm unackedRow = DashboardActiveAlarm.FromSnapshot(unacked);
        DashboardActiveAlarm ackedRow = DashboardActiveAlarm.FromSnapshot(acked);

        Assert.Equal("Galaxy", unackedRow.Provider);
        Assert.Equal("TestArea", unackedRow.Area);
        Assert.Equal(500, unackedRow.Severity);
        Assert.True(unackedRow.IsUnacknowledged);
        Assert.False(ackedRow.IsUnacknowledged);
    }

    /// <summary>Verifies that a healthy alarmmgr provider status maps to a green badge.</summary>
    [Fact]
    public void FromProviderStatus_Alarmmgr_NotDegraded_GreenBadge()
    {
        AlarmProviderStatus status = new()
        {
            Mode = AlarmProviderMode.Alarmmgr,
            Degraded = false,
        };

        DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);

        Assert.False(model.IsDegraded);
        Assert.Contains("bg-success", model.BadgeCssClass, StringComparison.Ordinal);
        Assert.Equal(DashboardAlarmProviderStatus.AlarmManagerLabel, model.Label);
    }

    /// <summary>Verifies that a degraded subtag provider status maps to an amber warning badge.</summary>
    [Fact]
    public void FromProviderStatus_Subtag_Degraded_WarningBadge()
    {
        AlarmProviderStatus status = new()
        {
            Mode = AlarmProviderMode.Subtag,
            Degraded = true,
            Reason = "x",
        };

        DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);

        Assert.True(model.IsDegraded);
        Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal);
        Assert.Equal("x", model.Reason);
        Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label);
    }

    /// <summary>
    ///     An explicitly-degraded status whose mode is still Alarmmgr (the
    ///     <c>Degraded || Mode==Subtag</c> guard's second, independent branch) must still
    ///     map to the degraded amber badge.
    /// </summary>
    [Fact]
    public void FromProviderStatus_Alarmmgr_DegradedFlagSet_WarningBadge()
    {
        AlarmProviderStatus status = new()
        {
            Mode = AlarmProviderMode.Alarmmgr,
            Degraded = true,
            Reason = "independently degraded",
        };

        DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);

        Assert.True(model.IsDegraded);
        Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label);
        Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal);
    }

    /// <summary>
    ///     The <c>SinceUtc</c> field must carry the protobuf <c>Since</c>
    ///     timestamp converted to a <see cref="DateTimeOffset" />.
    /// </summary>
    [Fact]
    public void FromProviderStatus_WithSinceTimestamp_MapsSinceUtc()
    {
        DateTimeOffset since = new(2026, 6, 15, 12, 30, 0, TimeSpan.Zero);
        AlarmProviderStatus status = new()
        {
            Mode = AlarmProviderMode.Subtag,
            Degraded = true,
            Reason = "x",
            Since = Timestamp.FromDateTimeOffset(since),
        };

        DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);

        Assert.Equal(since, model.SinceUtc);
    }

    /// <summary>
    ///     <see cref="DashboardAlarmProviderStatus.FromFeed" /> — the entry the
    ///     dashboard SignalR snapshot path actually calls — projects a provider-status
    ///     feed message into the badge model.
    /// </summary>
    [Fact]
    public void FromFeed_ProviderStatusPayload_ProjectsBadge()
    {
        AlarmFeedMessage message = new()
        {
            ProviderStatus = new AlarmProviderStatus
            {
                Mode = AlarmProviderMode.Subtag,
                Degraded = true,
                Reason = "alarmmgr failed",
            },
        };

        DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromFeed(message);

        Assert.Equal(AlarmProviderMode.Subtag, model.Mode);
        Assert.True(model.IsDegraded);
        Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label);
        Assert.Equal("alarmmgr failed", model.Reason);
    }

    /// <summary>
    ///     <see cref="DashboardAlarmProviderStatus.FromFeed" /> throws
    ///     <see cref="ArgumentException" /> when the feed message does not carry a
    ///     provider-status payload.
    /// </summary>
    [Fact]
    public void FromFeed_NonProviderStatusPayload_Throws()
    {
        AlarmFeedMessage message = new()
        {
            SnapshotComplete = true,
        };

        Assert.Throws<ArgumentException>(() => DashboardAlarmProviderStatus.FromFeed(message));
    }

    /// <summary>
    ///     Verifies that a configured forced-subtag provider status renders the
    ///     distinct "forced" badge (cyan/info), not the amber failover-degraded one.
    /// </summary>
    [Fact]
    public void FromProviderStatus_Subtag_ForcedReason_ForcedBadge()
    {
        AlarmProviderStatus status = new()
        {
            Mode = AlarmProviderMode.Subtag,
            Degraded = true,
            Reason = ZB.MOM.WW.MxGateway.Server.Alarms.AlarmProviderReasons.ForcedSubtag,
        };

        DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);

        Assert.True(model.IsDegraded);
        Assert.Equal(DashboardAlarmProviderStatus.ForcedSubtagLabel, model.Label);
        Assert.Contains("bg-info", model.BadgeCssClass, StringComparison.Ordinal);
        Assert.DoesNotContain("bg-warning", model.BadgeCssClass, StringComparison.Ordinal);
    }

    /// <summary>Verifies that the formatter renders array elements and element type correctly.</summary>
    [Fact]
    public void FormatValue_AndDataType_RenderArrayElementsAndElementType()
    {
        MxArray array = new() { ElementDataType = MxDataType.Double };
        array.Dimensions.Add(3u);
        array.DoubleValues = new DoubleArray();
        array.DoubleValues.Values.Add(new[] { 1.5, 2.25, 3.0 });
        MxValue value = new() { ArrayValue = array };

        Assert.Equal("[1.5, 2.25, 3]", DashboardMxValueFormatter.FormatValue(value));
        Assert.Equal("Double[3]", DashboardMxValueFormatter.FormatDataType(value));
    }
}
