using System.IO;
using System.Text;
using FluentAssertions;
using DragonISO.App.Services;

namespace DragonISO.App.Tests.Services;

/// <summary>
/// Tests for the minimal OSC 1.0 parser inside <see cref="OscBridge"/>.
/// We construct packets byte-by-byte so the tests verify the wire-format
/// parsing exactly: 4-byte address + null-terminated + padded, type tag
/// starting with ',', big-endian int/float arg encoding.
/// </summary>
public class OscMessageTests
{
    [Fact]
    public void TryParse_TooShortPacket_ReturnsNull()
    {
        OscMessage.TryParse(new byte[] { 0x01, 0x02 }).Should().BeNull();
    }

    [Fact]
    public void TryParse_BundleMarker_ReturnsNull()
    {
        // Bundles start with "#bundle\0..." â€” we don't support them.
        var bytes = Encoding.ASCII.GetBytes("#bundle\0");
        OscMessage.TryParse(bytes).Should().BeNull();
    }

    [Fact]
    public void TryParse_AddressOnly_ParsesAddress()
    {
        // "/foo\0\0\0\0" â€” 4-char address + null + 3 pad = 8 bytes
        var bytes = OscPacket("/foo");
        var msg = OscMessage.TryParse(bytes);
        msg.Should().NotBeNull();
        msg!.Address.Should().Be("/foo");
    }

    [Fact]
    public void TryParse_AddressMustStartWithSlash()
    {
        var bytes = OscPacket("foo");
        OscMessage.TryParse(bytes).Should().BeNull();
    }

    [Fact]
    public void TryParse_StringArg_RoundTrips()
    {
        var bytes = OscPacket("/Dragon-ISO/iso", ",s", "Jane");
        var msg = OscMessage.TryParse(bytes);
        msg.Should().NotBeNull();
        msg!.Address.Should().Be("/Dragon-ISO/iso");
        msg.GetStringArg(0).Should().Be("Jane");
    }

    [Fact]
    public void TryParse_IntArg_BigEndianInteger()
    {
        var bytes = OscPacket("/x", ",i", 42);
        var msg = OscMessage.TryParse(bytes);
        msg.Should().NotBeNull();
        msg!.GetBoolArg(0).Should().BeTrue(because: "non-zero int reads as true");
    }

    [Fact]
    public void TryParse_IntArg_Zero_ReadsAsFalse()
    {
        var bytes = OscPacket("/x", ",i", 0);
        var msg = OscMessage.TryParse(bytes);
        msg.Should().NotBeNull();
        msg!.GetBoolArg(0).Should().BeFalse();
    }

    [Fact]
    public void TryParse_TBoolArg_ReadsAsTrue()
    {
        // T type tag is 0-arg (the value is the type itself).
        var bytes = OscPacket("/x", ",T");
        var msg = OscMessage.TryParse(bytes);
        msg.Should().NotBeNull();
        msg!.GetBoolArg(0).Should().BeTrue();
    }

    [Fact]
    public void TryParse_FBoolArg_ReadsAsFalse()
    {
        var bytes = OscPacket("/x", ",F");
        var msg = OscMessage.TryParse(bytes);
        msg.Should().NotBeNull();
        msg!.GetBoolArg(0).Should().BeFalse();
    }

    [Fact]
    public void TryParse_StringArg_BoolHeuristic_True()
    {
        var bytes = OscPacket("/x", ",s", "true");
        var msg = OscMessage.TryParse(bytes);
        msg!.GetBoolArg(0).Should().BeTrue();
    }

    [Fact]
    public void TryParse_StringArg_BoolHeuristic_OneIsTrue()
    {
        var bytes = OscPacket("/x", ",s", "1");
        var msg = OscMessage.TryParse(bytes);
        msg!.GetBoolArg(0).Should().BeTrue();
    }

    [Fact]
    public void TryParse_StringArg_BoolHeuristic_NotMatching_IsFalse()
    {
        var bytes = OscPacket("/x", ",s", "no");
        var msg = OscMessage.TryParse(bytes);
        msg!.GetBoolArg(0).Should().BeFalse();
    }

    [Fact]
    public void TryParse_StringPlusInt_BothParse()
    {
        var bytes = OscPacket("/Dragon-ISO/iso", ",si", "Jane", 1);
        var msg = OscMessage.TryParse(bytes);
        msg.Should().NotBeNull();
        msg!.GetStringArg(0).Should().Be("Jane");
        msg.GetBoolArg(1).Should().BeTrue();
    }

    [Fact]
    public void TryParse_UnknownTypeTag_ReturnsNull()
    {
        // 'b' (blob) isn't supported; bail rather than mis-align subsequent args.
        var bytes = OscPacket("/x", ",b");
        OscMessage.TryParse(bytes).Should().BeNull();
    }

    [Fact]
    public void TryParse_MissingTypeTagComma_ReturnsNull()
    {
        // Type tag must start with ','. "ix" has no leading comma â€” invalid.
        var bytes = OscPacket("/x", "ix");
        OscMessage.TryParse(bytes).Should().BeNull();
    }

    [Fact]
    public void GetStringArg_OutOfRange_ReturnsNull()
    {
        var bytes = OscPacket("/x", ",s", "Jane");
        var msg = OscMessage.TryParse(bytes);
        msg!.GetStringArg(5).Should().BeNull();
    }

    [Fact]
    public void GetBoolArg_OutOfRange_ReturnsNull()
    {
        var bytes = OscPacket("/x", ",T");
        var msg = OscMessage.TryParse(bytes);
        msg!.GetBoolArg(5).Should().BeNull();
    }

    /// <summary>
    /// Build an OSC packet by hand. Address is null-terminated + 4-byte padded;
    /// optional type tag + args follow. Args must match the tag in count + type.
    /// Supported tags here for arg emission: 'i' (int32 big-endian), 's'
    /// (null-padded string), 'f' (float32 BE), 'T' / 'F' (no payload).
    ///
    /// For tags the helper doesn't recognize (e.g. 'b' blob, or a deliberately
    /// malformed tag without leading comma), we just emit the address + tag
    /// bytes and skip arg emission. The test caller is responsible for asking
    /// the parser to reject the result; the helper isn't a validator.
    /// </summary>
    private static byte[] OscPacket(string address, string? typeTag = null, params object[] args)
    {
        var ms = new MemoryStream();
        WriteOscString(ms, address);
        if (typeTag is null) return ms.ToArray();

        WriteOscString(ms, typeTag);
        // Only emit args when the tag string starts with ',' (a valid OSC type
        // tag); malformed tags get no arg payload, which is what the tests
        // exercising rejection paths want.
        if (!typeTag.StartsWith(',')) return ms.ToArray();

        var argIdx = 0;
        for (var i = 1; i < typeTag.Length; i++)
        {
            switch (typeTag[i])
            {
                case 'i': WriteInt32BE(ms, (int)args[argIdx++]); break;
                case 'f': WriteFloat32BE(ms, (float)args[argIdx++]); break;
                case 's': WriteOscString(ms, (string)args[argIdx++]); break;
                case 'T': case 'F': break; // no payload
                default:
                    // Unknown tag â€” emit no payload. Mirrors how a hostile sender
                    // might construct a malformed packet; the parser should reject.
                    break;
            }
        }
        return ms.ToArray();
    }

    private static void WriteOscString(MemoryStream ms, string s)
    {
        var bytes = Encoding.ASCII.GetBytes(s);
        ms.Write(bytes, 0, bytes.Length);
        ms.WriteByte(0); // null terminator
        // Pad to 4-byte boundary (the +1 above for the null is included in count).
        var written = bytes.Length + 1;
        var pad = (4 - written % 4) % 4;
        for (var i = 0; i < pad; i++) ms.WriteByte(0);
    }

    private static void WriteInt32BE(MemoryStream ms, int v)
    {
        ms.WriteByte((byte)(v >> 24));
        ms.WriteByte((byte)(v >> 16));
        ms.WriteByte((byte)(v >> 8));
        ms.WriteByte((byte)v);
    }

    private static void WriteFloat32BE(MemoryStream ms, float v)
    {
        var bytes = BitConverter.GetBytes(v);
        if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
        ms.Write(bytes, 0, 4);
    }
}
