using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
///
/// Round-trip tests for . Each scenario constructs a
/// gateway-style , decodes, and asserts the boxed CLR value
/// matches the expected type and value.
///
public sealed class MxValueDecoderTests
{
/// Verifies that decoding null returns null.
[Fact]
public void Decode_Null_ReturnsNull()
{
MxValueDecoder.Decode(null).ShouldBeNull();
}
/// Verifies that decoding IsNull flag returns null.
[Fact]
public void Decode_IsNullFlag_ReturnsNull()
{
var v = new MxValue { IsNull = true };
MxValueDecoder.Decode(v).ShouldBeNull();
}
/// Verifies that bool values decode correctly.
[Fact]
public void Decode_Bool() => MxValueDecoder.Decode(new MxValue { BoolValue = true }).ShouldBe(true);
/// Verifies that Int32 values decode correctly.
[Fact]
public void Decode_Int32() => MxValueDecoder.Decode(new MxValue { Int32Value = -42 }).ShouldBe(-42);
/// Verifies that Int64 values decode correctly.
[Fact]
public void Decode_Int64() => MxValueDecoder.Decode(new MxValue { Int64Value = 123456789012L }).ShouldBe(123456789012L);
/// Verifies that float values decode correctly.
[Fact]
public void Decode_Float() => MxValueDecoder.Decode(new MxValue { FloatValue = 3.14f }).ShouldBe(3.14f);
/// Verifies that double values decode correctly.
[Fact]
public void Decode_Double() => MxValueDecoder.Decode(new MxValue { DoubleValue = 2.71828 }).ShouldBe(2.71828);
/// Verifies that string values decode correctly.
[Fact]
public void Decode_String() => MxValueDecoder.Decode(new MxValue { StringValue = "hello" }).ShouldBe("hello");
/// Verifies that timestamp values decode to UTC datetime.
[Fact]
public void Decode_Timestamp_ReturnsUtcDateTime()
{
var when = new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Utc);
var v = new MxValue { TimestampValue = Timestamp.FromDateTime(when) };
MxValueDecoder.Decode(v).ShouldBe(when);
}
/// Verifies that bool arrays decode correctly.
[Fact]
public void Decode_BoolArray()
{
var v = new MxValue
{
ArrayValue = new MxArray
{
BoolValues = new BoolArray(),
},
};
v.ArrayValue.BoolValues.Values.AddRange(new[] { true, false, true });
var decoded = MxValueDecoder.Decode(v);
decoded.ShouldBe(new[] { true, false, true });
}
/// Verifies that double arrays decode correctly.
[Fact]
public void Decode_DoubleArray()
{
var v = new MxValue
{
ArrayValue = new MxArray { DoubleValues = new DoubleArray() },
};
v.ArrayValue.DoubleValues.Values.AddRange(new[] { 1.0, 2.0, 3.5 });
var decoded = MxValueDecoder.Decode(v);
decoded.ShouldBe(new[] { 1.0, 2.0, 3.5 });
}
/// Verifies that string arrays decode correctly.
[Fact]
public void Decode_StringArray()
{
var v = new MxValue
{
ArrayValue = new MxArray { StringValues = new StringArray() },
};
v.ArrayValue.StringValues.Values.AddRange(new[] { "a", "b" });
var decoded = MxValueDecoder.Decode(v);
decoded.ShouldBe(new[] { "a", "b" });
}
/// Verifies that raw byte values decode correctly.
[Fact]
public void Decode_RawValue_ReturnsBytes()
{
var bytes = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
var v = new MxValue { RawValue = ByteString.CopyFrom(bytes) };
var decoded = (byte[])MxValueDecoder.Decode(v)!;
decoded.ShouldBe(bytes);
}
// ---------------------------------------------------------------------------------
// Sparse arrays (MxGateway 0.2.0).
//
// The gateway contract makes these write-only — it expands a sparse write into a full
// MxArray and "the worker never receives or produces it" — so inbound is a contract
// violation rather than an expected path. These pin the behaviour if one ever arrives:
// the correct dense array, never a silent null and never a throw. The fill semantics
// mirror the gateway's own SparseArrayExpander, so a value that round-trips through a
// write and back reads as the same CLR array.
// ---------------------------------------------------------------------------------
private static MxValue Sparse(MxDataType elementType, uint totalLength, params (uint Index, MxValue Value)[] elements)
{
var sparse = new MxSparseArray { ElementDataType = elementType, TotalLength = totalLength };
foreach (var (index, value) in elements)
{
sparse.Elements.Add(new MxSparseElement { Index = index, Value = value });
}
return new MxValue { SparseArrayValue = sparse };
}
/// Verifies unmentioned indices are reset to the element default, not dropped.
[Fact]
public void Decode_SparseInt32Array_FillsUnmentionedIndicesWithTheDefault()
{
var v = Sparse(MxDataType.Integer, 5,
(1, new MxValue { Int32Value = 42 }),
(4, new MxValue { Int32Value = -7 }));
MxValueDecoder.Decode(v).ShouldBe(new[] { 0, 42, 0, 0, -7 });
}
///
/// Verifies an Integer sparse array widens to long[] when any element carries an int64.
/// The gateway picks its Int32/Int64 sub-array the same way — a sparse payload has no CLR
/// array to infer the width from — so decoding narrow here would change the published
/// element type across a write round-trip.
///
[Fact]
public void Decode_SparseIntegerArray_WidensToInt64WhenAnyElementIs64Bit()
{
var v = Sparse(MxDataType.Integer, 3,
(0, new MxValue { Int32Value = 1 }),
(2, new MxValue { Int64Value = 8_000_000_000L }));
MxValueDecoder.Decode(v).ShouldBe(new[] { 1L, 0L, 8_000_000_000L });
}
/// Verifies bool / float / double / string sparse arrays fill with their own defaults.
[Fact]
public void Decode_SparseArrays_UseThePerTypeDefaultForUnmentionedIndices()
{
MxValueDecoder.Decode(Sparse(MxDataType.Boolean, 3, (1, new MxValue { BoolValue = true })))
.ShouldBe(new[] { false, true, false });
MxValueDecoder.Decode(Sparse(MxDataType.Float, 2, (0, new MxValue { FloatValue = 1.5f })))
.ShouldBe(new[] { 1.5f, 0f });
MxValueDecoder.Decode(Sparse(MxDataType.Double, 2, (1, new MxValue { DoubleValue = 2.5d })))
.ShouldBe(new[] { 0d, 2.5d });
// string defaults to empty, NOT null — matching the expander, so a consumer never
// has to null-check individual elements of an array it was handed whole.
MxValueDecoder.Decode(Sparse(MxDataType.String, 3, (2, new MxValue { StringValue = "z" })))
.ShouldBe(new[] { string.Empty, string.Empty, "z" });
}
///
/// Verifies a Time sparse array defaults to the Unix epoch — the expander fills
/// Timestamp{0,0}, and default(DateTime) would decode a different instant.
///
[Fact]
public void Decode_SparseTimeArray_DefaultsToTheUnixEpochNotDefaultDateTime()
{
var when = new DateTime(2026, 8, 12, 3, 4, 5, DateTimeKind.Utc);
var v = Sparse(MxDataType.Time, 2, (1, new MxValue { TimestampValue = Timestamp.FromDateTime(when) }));
var decoded = (DateTime[])MxValueDecoder.Decode(v)!;
decoded[0].ShouldBe(DateTime.UnixEpoch);
decoded[0].ShouldNotBe(default);
decoded[1].ShouldBe(when);
}
///
/// Verifies a malformed sparse payload degrades instead of throwing. The decoder runs
/// inside the pump's dispatch loop, where an escaping exception kills dispatch for the
/// life of the pump — so an out-of-range index must be skipped, not raised.
///
[Fact]
public void Decode_SparseArray_WithAnOutOfRangeIndex_SkipsItRatherThanThrowing()
{
var v = Sparse(MxDataType.Integer, 2,
(0, new MxValue { Int32Value = 5 }),
(99, new MxValue { Int32Value = 6 }));
object? decoded = null;
Should.NotThrow(() => decoded = MxValueDecoder.Decode(v));
decoded.ShouldBe(new[] { 5, 0 });
}
/// Verifies an unsupported element type and a zero length report as unrepresentable.
[Fact]
public void TryDecode_SparseArray_WithAnUnsupportedShape_ReportsUnrepresentable()
{
// A struct element type is not a scalar the expander supports either — there is no
// array to build, so this must NOT masquerade as a successfully decoded null.
MxValueDecoder.TryDecode(Sparse(MxDataType.QualifiedStruct, 2), out var structDecoded).ShouldBeFalse();
structDecoded.ShouldBeNull();
MxValueDecoder.TryDecode(Sparse(MxDataType.Integer, 0), out _).ShouldBeFalse();
}
// ---------------------------------------------------------------------------------
// Representable-vs-null. Decode() alone collapses both to null, which is how a value
// kind this driver cannot represent would be published as a good null and lost.
// ---------------------------------------------------------------------------------
/// Verifies a genuine null decodes successfully rather than reporting failure.
[Fact]
public void TryDecode_DistinguishesAGenuineNullFromAnUnrepresentableValue()
{
MxValueDecoder.TryDecode(null, out var fromNullRef).ShouldBeTrue();
fromNullRef.ShouldBeNull();
MxValueDecoder.TryDecode(new MxValue { IsNull = true }, out var fromIsNull).ShouldBeTrue();
fromIsNull.ShouldBeNull();
// An MxValue with no kind set at all is not a null value — it is a payload this
// driver cannot interpret, and the two must not report identically.
MxValueDecoder.TryDecode(new MxValue(), out var fromEmpty).ShouldBeFalse();
fromEmpty.ShouldBeNull();
}
/// Verifies an array whose element oneof is unset reports as unrepresentable.
[Fact]
public void TryDecode_ArrayWithNoElementsOneofSet_ReportsUnrepresentable()
{
MxValueDecoder.TryDecode(new MxValue { ArrayValue = new MxArray() }, out var decoded).ShouldBeFalse();
decoded.ShouldBeNull();
}
}