📄 lib/tests/TestDlt.cs
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SharpDlt.Headers;

namespace SharpDlt.Tests;

/// <summary>Builds raw DLT byte sequences to feed the parser under test.</summary>
internal static class TestDlt
{
    public const uint TypeInfoBool = 0x00000011;
    public const uint TypeInfoUInt8 = 0x00000041;
    public const uint TypeInfoUInt16 = 0x00000042;
    public const uint TypeInfoUInt32 = 0x00000043;
    public const uint TypeInfoUInt64 = 0x00000044;
    public const uint TypeInfoSInt8 = 0x00000021;
    public const uint TypeInfoSInt16 = 0x00000022;
    public const uint TypeInfoSInt32 = 0x00000023;
    public const uint TypeInfoSInt64 = 0x00000024;
    public const uint TypeInfoFloat32 = 0x00000083;
    public const uint TypeInfoFloat64 = 0x00000084;
    public const uint TypeInfoStringAscii = 0x00000200;
    public const uint TypeInfoStringUtf8 = 0x00008200;
    public const uint TypeInfoRaw = 0x00000400;

    public static byte[] StorageHeader(uint seconds = 1_000_000, int microseconds = 123_456, string ecuId = "ECU1")
    {
        var data = new byte[16];
        "DLT\u0001"u8.CopyTo(data);
        BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(4), seconds);
        BinaryPrimitives.WriteInt32LittleEndian(data.AsSpan(8), microseconds);
        WriteAsciiField(data.AsSpan(12, 4), ecuId);
        return data;
    }

    public static byte[] StandardHeader(
        bool useExtendedHeader = false,
        bool msbFirst = false,
        int versionNumber = 1,
        byte messageCounter = 0,
        ushort length = 4,
        string? ecuId = null,
        uint? sessionId = null,
        uint? timestamp = null
    )
    {
        var headerType = (versionNumber << 5) & 0b1110_0000;
        if (useExtendedHeader)
        {
            headerType |= 0b0000_0001;
        }

        if (msbFirst)
        {
            headerType |= 0b0000_0010;
        }

        if (ecuId is not null)
        {
            headerType |= 0b0000_0100;
        }

        if (sessionId is not null)
        {
            headerType |= 0b0000_1000;
        }

        if (timestamp is not null)
        {
            headerType |= 0b0001_0000;
        }

        var data = new List<byte> { (byte)headerType, messageCounter };
        data.AddRange(BigEndian(length));
        if (ecuId is not null)
        {
            var field = new byte[4];
            WriteAsciiField(field, ecuId);
            data.AddRange(field);
        }

        if (sessionId is { } session)
        {
            data.AddRange(BigEndian(session));
        }

        if (timestamp is { } stamp)
        {
            data.AddRange(BigEndian(stamp));
        }

        return [.. data];
    }

    public static byte[] ExtendedHeader(
        bool verbose = true,
        DltMessageType messageType = DltMessageType.Log,
        byte messageTypeInfo = (byte)DltLogInfo.Info,
        byte numberOfArguments = 1,
        string applicationId = "APP1",
        string contextId = "CTX1"
    )
    {
        var messageInfo = verbose ? 0b0000_0001 : 0;
        messageInfo |= ((int)messageType << 1) & 0b0000_1110;
        messageInfo |= (messageTypeInfo << 4) & 0b1111_0000;

        var data = new byte[10];
        data[0] = (byte)messageInfo;
        data[1] = numberOfArguments;
        WriteAsciiField(data.AsSpan(2, 4), applicationId);
        WriteAsciiField(data.AsSpan(6, 4), contextId);
        return data;
    }

    public static byte[] Argument(uint typeInfo, ReadOnlySpan<byte> payload, bool msbFirst = false)
    {
        var data = new byte[4 + payload.Length];
        if (msbFirst)
        {
            BinaryPrimitives.WriteUInt32BigEndian(data, typeInfo);
        }
        else
        {
            BinaryPrimitives.WriteUInt32LittleEndian(data, typeInfo);
        }

        payload.CopyTo(data.AsSpan(4));
        return data;
    }

    public static byte[] StringArgument(string value, bool utf8 = false, bool msbFirst = false)
    {
        var encoded = (utf8 ? Encoding.UTF8 : Encoding.ASCII).GetBytes(value);
        var payload = new byte[2 + encoded.Length + 1];
        WriteUInt16(payload, (ushort)(encoded.Length + 1), msbFirst);
        encoded.CopyTo(payload.AsSpan(2));
        return Argument(utf8 ? TypeInfoStringUtf8 : TypeInfoStringAscii, payload, msbFirst);
    }

    public static byte[] RawArgument(ReadOnlySpan<byte> value, bool msbFirst = false)
    {
        var payload = new byte[2 + value.Length];
        WriteUInt16(payload, (ushort)value.Length, msbFirst);
        value.CopyTo(payload.AsSpan(2));
        return Argument(TypeInfoRaw, payload, msbFirst);
    }

    /// <summary>Composes a complete message and fills in the length field of the Standard Header.</summary>
    public static byte[] Message(
        byte[]? storageHeader,
        byte[] standardHeader,
        byte[]? extendedHeader = null,
        byte[]? payload = null
    )
    {
        var length = standardHeader.Length + (extendedHeader?.Length ?? 0) + (payload?.Length ?? 0);
        var standard = (byte[])standardHeader.Clone();
        BinaryPrimitives.WriteUInt16BigEndian(standard.AsSpan(2), (ushort)length);

        return [.. storageHeader ?? [], .. standard, .. extendedHeader ?? [], .. payload ?? []];
    }

    /// <summary>Composes a verbose log message with the given arguments.</summary>
    public static byte[] VerboseMessage(
        IEnumerable<byte[]> arguments,
        bool msbFirst = false,
        byte messageCounter = 0,
        uint? timestamp = null,
        bool withStorageHeader = true
    )
    {
        var argumentList = arguments.ToArray();
        return Message(
            withStorageHeader ? StorageHeader() : null,
            StandardHeader(
                useExtendedHeader: true,
                msbFirst: msbFirst,
                messageCounter: messageCounter,
                timestamp: timestamp
            ),
            ExtendedHeader(numberOfArguments: (byte)argumentList.Length),
            [.. argumentList.SelectMany(argument => argument)]
        );
    }

    public static byte[] BigEndian(ushort value)
    {
        var data = new byte[2];
        BinaryPrimitives.WriteUInt16BigEndian(data, value);
        return data;
    }

    public static byte[] BigEndian(uint value)
    {
        var data = new byte[4];
        BinaryPrimitives.WriteUInt32BigEndian(data, value);
        return data;
    }

    public static byte[] LittleEndian(uint value)
    {
        var data = new byte[4];
        BinaryPrimitives.WriteUInt32LittleEndian(data, value);
        return data;
    }

    private static void WriteUInt16(Span<byte> destination, ushort value, bool msbFirst)
    {
        if (msbFirst)
        {
            BinaryPrimitives.WriteUInt16BigEndian(destination, value);
        }
        else
        {
            BinaryPrimitives.WriteUInt16LittleEndian(destination, value);
        }
    }

    private static void WriteAsciiField(Span<byte> destination, string value)
    {
        destination.Clear();
        Encoding.ASCII.GetBytes(value.AsSpan(0, Math.Min(value.Length, destination.Length)), destination);
    }
}