📄 lib/tests/DltArgumentTests.cs
using System;
using System.Buffers.Binary;
using System.Text;
using SharpDlt.Payloads;
using Shouldly;
using Xunit;

namespace SharpDlt.Tests;

public class DltArgumentTests
{
    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    [InlineData(255, true)]
    public void Parse_Bool(byte raw, bool expected)
    {
        var data = TestDlt.Argument(TestDlt.TypeInfoBool, [raw]);

        var argument = DltArgument.Parse(data, msbFirst: false, encoding: null, out var bytesRead);

        argument.ShouldBe(new DltBoolArgument(expected));
        bytesRead.ShouldBe(5);
    }

    [Fact]
    public void Parse_UnsignedIntegers()
    {
        AssertUnsigned(TestDlt.TypeInfoUInt8, [0xFF], 255, DltIntegerWidth.Bits8);
        AssertUnsigned(TestDlt.TypeInfoUInt16, [0x00, 0x01], 256, DltIntegerWidth.Bits16);
        AssertUnsigned(TestDlt.TypeInfoUInt32, [0x00, 0x00, 0x01, 0x00], 65536, DltIntegerWidth.Bits32);
        AssertUnsigned(
            TestDlt.TypeInfoUInt64,
            [0, 0, 0, 0, 0, 0, 0, 0x80],
            0x8000_0000_0000_0000,
            DltIntegerWidth.Bits64
        );

        static void AssertUnsigned(uint typeInfo, byte[] payload, ulong expected, DltIntegerWidth width)
        {
            var data = TestDlt.Argument(typeInfo, payload);

            var argument = DltArgument.Parse(data, false, null, out var bytesRead);

            argument.ShouldBe(new DltUnsignedArgument(expected, width));
            bytesRead.ShouldBe(4 + payload.Length);
        }
    }

    [Fact]
    public void Parse_SignedIntegers()
    {
        AssertSigned(TestDlt.TypeInfoSInt8, [0xFF], -1, DltIntegerWidth.Bits8);
        AssertSigned(TestDlt.TypeInfoSInt16, [0xFF, 0xFF], -1, DltIntegerWidth.Bits16);
        AssertSigned(TestDlt.TypeInfoSInt32, [0xFE, 0xFF, 0xFF, 0xFF], -2, DltIntegerWidth.Bits32);
        AssertSigned(
            TestDlt.TypeInfoSInt64,
            [0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
            -3,
            DltIntegerWidth.Bits64
        );

        static void AssertSigned(uint typeInfo, byte[] payload, long expected, DltIntegerWidth width)
        {
            var data = TestDlt.Argument(typeInfo, payload);

            var argument = DltArgument.Parse(data, false, null, out var bytesRead);

            argument.ShouldBe(new DltSignedArgument(expected, width));
            bytesRead.ShouldBe(4 + payload.Length);
        }
    }

    [Fact]
    public void Parse_BigEndianInteger()
    {
        var data = TestDlt.Argument(TestDlt.TypeInfoUInt32, [0x00, 0x01, 0x00, 0x00], msbFirst: true);

        var argument = DltArgument.Parse(data, msbFirst: true, encoding: null, out _);

        argument.ShouldBe(new DltUnsignedArgument(65536, DltIntegerWidth.Bits32));
    }

    [Fact]
    public void Parse_Float32()
    {
        var payload = new byte[4];
        BinaryPrimitives.WriteSingleLittleEndian(payload, 1.5f);
        var data = TestDlt.Argument(TestDlt.TypeInfoFloat32, payload);

        var argument = DltArgument.Parse(data, false, null, out var bytesRead);

        argument.ShouldBe(new DltFloatArgument(1.5, DltFloatWidth.Bits32));
        bytesRead.ShouldBe(8);
    }

    [Fact]
    public void Parse_Float64()
    {
        var payload = new byte[8];
        BinaryPrimitives.WriteDoubleBigEndian(payload, -2.25);
        var data = TestDlt.Argument(TestDlt.TypeInfoFloat64, payload, msbFirst: true);

        var argument = DltArgument.Parse(data, true, null, out var bytesRead);

        argument.ShouldBe(new DltFloatArgument(-2.25, DltFloatWidth.Bits64));
        bytesRead.ShouldBe(12);
    }

    [Fact]
    public void Parse_AsciiString()
    {
        var data = TestDlt.StringArgument("hello");

        var argument = DltArgument.Parse(data, false, null, out var bytesRead);

        argument.ShouldBe(new DltStringArgument("hello", DltStringCoding.Ascii));
        bytesRead.ShouldBe(data.Length);
    }

    [Fact]
    public void Parse_Utf8String()
    {
        var data = TestDlt.StringArgument("hΓ€llΓΆ", utf8: true);

        var argument = DltArgument.Parse(data, false, null, out var bytesRead);

        argument.ShouldBe(new DltStringArgument("hΓ€llΓΆ", DltStringCoding.Utf8));
        bytesRead.ShouldBe(data.Length);
    }

    [Fact]
    public void Parse_EmptyString()
    {
        var data = TestDlt.StringArgument(string.Empty);

        DltArgument
            .Parse(data, false, null, out _)
            .ShouldBe(new DltStringArgument(string.Empty, DltStringCoding.Ascii));
    }

    [Fact]
    public void Parse_EightBitStringUsesConfiguredEncoding()
    {
        var latin1 = Encoding.Latin1;
        var encoded = latin1.GetBytes("Grüße");
        var payload = new byte[2 + encoded.Length + 1];
        BinaryPrimitives.WriteUInt16LittleEndian(payload, (ushort)(encoded.Length + 1));
        encoded.CopyTo(payload.AsSpan(2));
        var data = TestDlt.Argument(TestDlt.TypeInfoStringAscii, payload);

        DltArgument.Parse(data, false, latin1, out _).ShouldBe(new DltStringArgument("Grüße", DltStringCoding.Ascii));
    }

    [Fact]
    public void Parse_Raw()
    {
        var data = TestDlt.RawArgument([0xDE, 0xAD, 0xBE, 0xEF]);

        var argument = DltArgument.Parse(data, false, null, out var bytesRead);

        argument.ShouldBe(new DltRawArgument(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }));
        argument.ToString().ShouldBe("deadbeef");
        bytesRead.ShouldBe(10);
    }

    [Fact]
    public void Parse_TruncatedInteger_FallsBackToSmallerWidth()
    {
        // A 64 bit unsigned integer with only 4 bytes of data is read as a 32 bit integer,
        // which mirrors the behavior of DLT Viewer.
        var data = TestDlt.Argument(TestDlt.TypeInfoUInt64, [0x00, 0x00, 0x01, 0x00]);

        var argument = DltArgument.Parse(data, false, null, out var bytesRead);

        argument.ShouldBe(new DltUnsignedArgument(65536, DltIntegerWidth.Bits32));
        bytesRead.ShouldBe(8);
    }

    [Fact]
    public void Parse_IntegerWithoutAnyData_Throws()
    {
        var data = TestDlt.Argument(TestDlt.TypeInfoUInt32, []);

        Should.Throw<DltParseException>(() => DltArgument.Parse(data, false, null, out _));
    }

    [Fact]
    public void Parse_TruncatedFloat_Throws()
    {
        var data = TestDlt.Argument(TestDlt.TypeInfoFloat64, [0x00, 0x00]);

        Should.Throw<DltParseException>(() => DltArgument.Parse(data, false, null, out _));
    }

    [Fact]
    public void Parse_TooShortForTypeInfo_Throws()
    {
        Should.Throw<DltParseException>(() => DltArgument.Parse(new byte[3], false, null, out _));
    }

    [Fact]
    public void Parse_UnsupportedType_Throws()
    {
        // TYPE_ARRAY is not supported.
        var data = TestDlt.Argument(0x00000100, [0x00, 0x00, 0x00, 0x00]);

        Should.Throw<DltParseException>(() => DltArgument.Parse(data, false, null, out _));
    }

    [Fact]
    public void ToString_UsesInvariantFormatting()
    {
        new DltBoolArgument(true).ToString().ShouldBe("True");
        new DltSignedArgument(-5, DltIntegerWidth.Bits32).ToString().ShouldBe("-5");
        new DltUnsignedArgument(7, DltIntegerWidth.Bits8).ToString().ShouldBe("7");
        new DltFloatArgument(1.5, DltFloatWidth.Bits32).ToString().ShouldBe("1.5");
        new DltStringArgument("text", DltStringCoding.Ascii).ToString().ShouldBe("text");
    }

    [Fact]
    public void RawArgument_EqualityComparesContent()
    {
        new DltRawArgument(new byte[] { 1, 2 }).ShouldBe(new DltRawArgument(new byte[] { 1, 2 }));
        new DltRawArgument(new byte[] { 1, 2 })
            .GetHashCode()
            .ShouldBe(new DltRawArgument(new byte[] { 1, 2 }).GetHashCode());
        new DltRawArgument(new byte[] { 1, 2 }).ShouldNotBe(new DltRawArgument(new byte[] { 1 }));
    }
}