using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
// MxStatusCategory needed for Driver.Galaxy-003 regression tests.

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;

/// <summary>
///     Exhaustive table-driven tests for <see cref="StatusCodeMap"/>. Pinning the byte→uint
///     mapping here protects against accidental drift — every Galaxy deployment that
///     reaches the parity matrix in PR 5.2 depends on these specific OPC UA StatusCode
///     values matching the legacy <c>HistorianQualityMapper</c> output.
/// </summary>
public sealed class StatusCodeMapTests
{
    /// <summary>Verifies that known quality bytes map to the expected OPC UA status codes.</summary>
    /// <param name="input">The Galaxy quality byte to map.</param>
    /// <param name="expected">The expected OPC UA status code.</param>
    [Theory]
    [InlineData((byte)192, 0x00000000u)]   // Good
    [InlineData((byte)216, 0x00960000u)]   // Good_LocalOverride
    [InlineData((byte)64,  0x40000000u)]   // Uncertain
    [InlineData((byte)68,  0x40900000u)]   // Uncertain_LastUsableValue
    [InlineData((byte)80,  0x40930000u)]   // Uncertain_SensorNotAccurate
    [InlineData((byte)84,  0x40940000u)]   // Uncertain_EngineeringUnitsExceeded
    [InlineData((byte)88,  0x40950000u)]   // Uncertain_SubNormal
    [InlineData((byte)0,   0x80000000u)]   // Bad
    [InlineData((byte)4,   0x80890000u)]   // Bad_ConfigurationError
    [InlineData((byte)8,   0x808A0000u)]   // Bad_NotConnected
    [InlineData((byte)12,  0x808B0000u)]   // Bad_DeviceFailure
    [InlineData((byte)16,  0x808C0000u)]   // Bad_SensorFailure
    [InlineData((byte)20,  0x80050000u)]   // Bad_CommunicationError
    [InlineData((byte)24,  0x808D0000u)]   // Bad_OutOfService
    [InlineData((byte)32,  0x80320000u)]   // Bad_WaitingForInitialData
    public void FromQualityByte_KnownValues_MapToOpcUaStatusCode(byte input, uint expected)
    {
        StatusCodeMap.FromQualityByte(input).ShouldBe(expected);
    }

    /// <summary>Verifies that unknown quality bytes fall back to category bucket status codes.</summary>
    /// <param name="input">The unknown Galaxy quality byte to categorize.</param>
    [Theory]
    [InlineData((byte)200)]  // Unknown Good — falls back to category bucket
    [InlineData((byte)100)]  // Unknown Uncertain
    [InlineData((byte)40)]   // Unknown Bad
    public void FromQualityByte_UnknownValues_FallBackToCategoryBucket(byte input)
    {
        var mapped = StatusCodeMap.FromQualityByte(input);
        if (input >= 192) mapped.ShouldBe(StatusCodeMap.Good);
        else if (input >= 64) mapped.ShouldBe(StatusCodeMap.Uncertain);
        else mapped.ShouldBe(StatusCodeMap.Bad);
    }

    // ===== Driver.Galaxy-003 regression: FromMxStatus uses IsSuccess() (category + success) =====
    // The proto doc: "clients should branch on category, not on a specific success value."
    // IsSuccess() requires BOTH success != 0 AND category == Ok — checking success alone
    // would invert the mapping when the worker sets success=1 for an error code (the numeric
    // value is NOT a boolean).

    /// <summary>Verifies that null status maps to Good status code.</summary>
    [Fact]
    public void FromMxStatus_NullStatus_IsGood()
    {
        StatusCodeMap.FromMxStatus(null).ShouldBe(StatusCodeMap.Good);
    }

    /// <summary>Verifies that non-zero success with OK category maps to Good.</summary>
    [Fact]
    public void FromMxStatus_SuccessNonZeroAndCategoryOk_IsGood()
    {
        // Both conditions required — this is the canonical OK path.
        var s = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok };
        StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Good);
    }

    /// <summary>Verifies that non-zero success with non-OK category does not map to Good.</summary>
    [Fact]
    public void FromMxStatus_SuccessNonZeroButCategoryNotOk_IsNotGood()
    {
        // Bug fixed by Driver.Galaxy-003: success != 0 alone used to return Good even when
        // category indicates an error. The proto says success is NOT a boolean — category wins.
        var s = new MxStatusProxy { Success = 1, Category = MxStatusCategory.CommunicationError };
        StatusCodeMap.FromMxStatus(s).ShouldNotBe(StatusCodeMap.Good);
    }

    /// <summary>Verifies that known detail codes map to their specific status codes.</summary>
    [Fact]
    public void FromMxStatus_SuccessZeroAndCategoryNotOk_DetailKnown_MapsToSpecificCode()
    {
        var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.OperationalError, Detail = 8 /* Bad_NotConnected */ };
        StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadNotConnected);
    }

    /// <summary>Verifies that non-zero DetectedBy maps to communication error when detail is zero.</summary>
    [Fact]
    public void FromMxStatus_SuccessZero_DetailZero_DetectedByNonZero_IsCommunicationError()
    {
        var s = new MxStatusProxy { Success = 0, Detail = 0, RawDetectedBy = 3 };
        StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadCommunicationError);
    }

    /// <summary>Verifies that all-zero status maps to Bad status code.</summary>
    [Fact]
    public void FromMxStatus_SuccessZero_AllZero_IsBad()
    {
        var s = new MxStatusProxy();
        StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Bad);
    }

    /// <summary>Verifies that top two bits of status codes follow OPC UA convention.</summary>
    [Fact]
    public void TopByteCategoryBits_StayWithinOpcUaConvention()
    {
        // Sanity check that every Bad code we mint actually has the Bad top byte (0x80…),
        // every Uncertain has 0x40…, every Good has 0x00…. Pins the OPC UA Part 4 invariant.
        StatusCodeMap.Good.ShouldBeLessThan(0x40000000u);
        StatusCodeMap.GoodLocalOverride.ShouldBeLessThan(0x40000000u);

        ((StatusCodeMap.Uncertain >> 30) & 0x3u).ShouldBe(1u);
        ((StatusCodeMap.UncertainLastUsableValue >> 30) & 0x3u).ShouldBe(1u);

        ((StatusCodeMap.Bad >> 30) & 0x3u).ShouldBe(2u);
        ((StatusCodeMap.BadNotConnected >> 30) & 0x3u).ShouldBe(2u);
        ((StatusCodeMap.BadOutOfService >> 30) & 0x3u).ShouldBe(2u);
    }

    // ===== Driver.Galaxy-004 regression: ToQualityCategoryByte lives next to its inverse =====

    /// <summary>Verifies that status codes extract to their OPC DA category byte equivalent.</summary>
    /// <param name="statusCode">The OPC UA status code to convert.</param>
    /// <param name="expected">The expected OPC DA category byte.</param>
    [Theory]
    [InlineData(0x00000000u, (byte)192)]  // Good
    [InlineData(0x00960000u, (byte)192)]  // GoodLocalOverride — still Good category
    [InlineData(0x40000000u, (byte)64)]   // Uncertain
    [InlineData(0x40950000u, (byte)64)]   // UncertainSubNormal — still Uncertain category
    [InlineData(0x80000000u, (byte)0)]    // Bad
    [InlineData(0x808A0000u, (byte)0)]    // BadNotConnected — still Bad category
    [InlineData(0x80020000u, (byte)0)]    // BadInternalError — still Bad category
    public void ToQualityCategoryByte_ExtractsTopTwoBitsAsOpcDaByte(uint statusCode, byte expected)
    {
        StatusCodeMap.ToQualityCategoryByte(statusCode).ShouldBe(expected);
    }

    /// <summary>Verifies that ToQualityCategoryByte is the right inverse of FromQualityByte for category bytes.</summary>
    [Fact]
    public void ToQualityCategoryByte_IsRightInverseOfFromQualityByte_ForCategoryBytes()
    {
        // The category bytes the probe watcher uses are 0, 64, 192. Round-trip: a value
        // that came FROM those bytes should map back to the same byte.
        StatusCodeMap.ToQualityCategoryByte(StatusCodeMap.FromQualityByte(0)).ShouldBe((byte)0);
        StatusCodeMap.ToQualityCategoryByte(StatusCodeMap.FromQualityByte(64)).ShouldBe((byte)64);
        StatusCodeMap.ToQualityCategoryByte(StatusCodeMap.FromQualityByte(192)).ShouldBe((byte)192);
    }
}
