using System.Buffers.Binary;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Sparkplug;
using TahuDataType = Org.Eclipse.Tahu.Protobuf.DataType;
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests;
///
/// Covers (lmxopcua#754). The golden vectors are the Sparkplug B 3.0
/// specification's own "Datatype details" examples, and an independent encoder (SparkplugNet 1.3.10,
/// PayloadConverter.ConvertVersionBMetric) was run against the same inputs and produced the same bytes
/// — so the bit order of a BooleanArray and the string terminator are not this repo agreeing with itself.
///
public sealed class SparkplugArrayCodecTests
{
/// Decodes and pins the WIRE element type the codec hands the ingestor.
private static T[] Decode(TahuDataType type, params byte[] packed)
{
SparkplugArrayCodec.TryDecode(type, packed, out var elements, out var error).ShouldBeTrue(error);
return elements.ShouldBeOfType();
}
private static string Refuse(TahuDataType type, params byte[] packed)
{
SparkplugArrayCodec.TryDecode(type, packed, out var elements, out var error).ShouldBeFalse();
elements.ShouldBeNull();
return error.ShouldNotBeNull();
}
// ---- golden vectors (spec examples; SparkplugNet reproduces them byte for byte) ----
/// uint32 LE count, then the bits MSB-first: 0x34 = F,F,T,T,F,T,F,F and 0xD0 = T,T,F,T + padding.
[Fact]
public void BooleanArray_IsCountPrefixed_AndPackedMostSignificantBitFirst()
=> Decode(TahuDataType.BooleanArray, 0x0C, 0x00, 0x00, 0x00, 0x34, 0xD0)
.ShouldBe(new[] { false, false, true, true, false, true, false, false, true, true, false, true });
[Fact]
public void Int8Array_IsOneSignedBytePerElement()
=> Decode(TahuDataType.Int8Array, 0xE9, 0x7B).ShouldBe(new sbyte[] { -23, 123 });
[Fact]
public void Int16Array_IsLittleEndian()
=> Decode(TahuDataType.Int16Array, 0xD0, 0x8A, 0x30, 0x75).ShouldBe(new short[] { -30000, 30000 });
[Fact]
public void Uint32Array_IsLittleEndian()
=> Decode(TahuDataType.Uint32Array, 0x34, 0x00, 0x00, 0x00, 0x49, 0xFB, 0x55, 0xC4)
.ShouldBe(new uint[] { 52, 3293969225 });
[Fact]
public void FloatArray_IsLittleEndianIeee754()
=> Decode(TahuDataType.FloatArray, 0xA4, 0x70, 0x9D, 0x3F, 0x98, 0xAE, 0xB2, 0x42)
.ShouldBe(new[] { 1.23f, 89.341f });
[Fact]
public void StringArray_IsNullTerminatedUtf8()
=> Decode(TahuDataType.StringArray, 0x41, 0x42, 0x43, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x00)
.ShouldBe(new[] { "ABC", "hello" });
/// Signed int64 epoch milliseconds — left as the wire long; the ingestor coerces it to a DateTime.
[Fact]
public void DateTimeArray_IsInt64EpochMilliseconds()
=> Decode(TahuDataType.DateTimeArray, 0xC7, 0xD0, 0x90, 0x75, 0x24, 0x01, 0x00, 0x00)
.ShouldBe(new[] { 1256102875335L });
// ---- the remaining widths, encoded with the BCL so the decoder is not checked against itself ----
[Fact]
public void RemainingWidths_RoundTripTheBclLittleEndianEncoding()
{
var bytes = new byte[16];
BinaryPrimitives.WriteInt64LittleEndian(bytes, -5L);
BinaryPrimitives.WriteInt64LittleEndian(bytes.AsSpan(8), long.MaxValue);
Decode(TahuDataType.Int64Array, bytes).ShouldBe(new[] { -5L, long.MaxValue });
BinaryPrimitives.WriteUInt64LittleEndian(bytes, ulong.MaxValue);
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(8), 7UL);
Decode(TahuDataType.Uint64Array, bytes).ShouldBe(new[] { ulong.MaxValue, 7UL });
BinaryPrimitives.WriteDoubleLittleEndian(bytes, -1.5);
BinaryPrimitives.WriteDoubleLittleEndian(bytes.AsSpan(8), 1e300);
Decode(TahuDataType.DoubleArray, bytes).ShouldBe(new[] { -1.5, 1e300 });
Decode(TahuDataType.Uint8Array, 0x00, 0xFF).ShouldBe(new byte[] { 0, 255 });
Decode(TahuDataType.Uint16Array, 0xFF, 0xFF, 0x01, 0x00).ShouldBe(new ushort[] { 65535, 1 });
Decode(TahuDataType.Int32Array, 0xD6, 0xFF, 0xFF, 0xFF).ShouldBe(new[] { -42 });
}
[Fact]
public void EmptyBodies_AreEmptyArrays_ExceptBooleanWhichNeedsItsCount()
{
Decode(TahuDataType.Int32Array).Length.ShouldBe(0);
Decode(TahuDataType.StringArray).Length.ShouldBe(0);
Decode(TahuDataType.BooleanArray, 0x00, 0x00, 0x00, 0x00).Length.ShouldBe(0);
Refuse(TahuDataType.BooleanArray);
}
// ---- malformed bodies: all-or-nothing, never a partial vector ----
[Fact]
public void ABodyThatIsNotAWholeNumberOfElements_IsRefused()
=> Refuse(TahuDataType.Int32Array, 1, 2, 3, 4, 5).ShouldContain("whole number");
[Fact]
public void ABooleanCountTheBodyCannotHold_IsRefused()
=> Refuse(TahuDataType.BooleanArray, 0x0C, 0x00, 0x00, 0x00, 0x34).ShouldContain("needs 2");
/// Exact, not "at least": a trailing byte means the count and the body disagree.
[Fact]
public void ABooleanBodyLongerThanItsCountNeeds_IsRefused()
=> Refuse(TahuDataType.BooleanArray, 0x08, 0x00, 0x00, 0x00, 0xFF, 0x00).ShouldContain("needs 1");
[Fact]
public void AnUnterminatedLastString_IsRefused()
=> Refuse(TahuDataType.StringArray, 0x41, 0x00, 0x42).ShouldContain("terminated");
[Fact]
public void InvalidUtf8_IsRefused_NotReplacementCharactered()
=> Refuse(TahuDataType.StringArray, 0xC0, 0xC0, 0x00).ShouldContain("UTF-8");
[Fact]
public void AScalarDatatype_IsNotAnArray()
=> Refuse(TahuDataType.Int32, 0, 0, 0, 0).ShouldContain("not a Sparkplug array");
}