using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;
using Xunit;

namespace RioJoy.Core.Tests.Serial;

public class RioSerialLinkTests
{
    private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);

    [Fact]
    public async Task AnalogReply_DecodesAndAcks()
    {
        var fake = new FakeTransport();
        var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });

        var gotAnalog = new TaskCompletionSource<AnalogReport>(TaskCreationOptions.RunContinuationsAsynchronously);
        link.AnalogReceived += r => gotAnalog.TrySetResult(r);

        using var cts = new CancellationTokenSource();
        Task run = link.RunAsync(cts.Token);

        byte[] frame = PacketBuilder.Build(RioCommand.AnalogReply, new byte[] { 1, 0, 2, 0, 3, 0, 4, 0, 5, 0 });
        fake.Enqueue(frame);

        AnalogReport report = await gotAnalog.Task.WaitAsync(Timeout);
        Assert.Equal(1, report.Throttle);
        Assert.Equal(5, report.JoystickX);

        // The link should have ACK'd the packet.
        byte[] reply = await fake.NextWriteAsync();
        Assert.Equal(new byte[] { (byte)RioControl.Ack }, reply);

        cts.Cancel();
        await run;
    }

    [Fact]
    public async Task BadChecksumButton_IsNakd_WhenVerificationEnabled()
    {
        var fake = new FakeTransport();
        var link = new RioSerialLink(fake, new RioSerialLinkOptions
        {
            AutoPollAnalog = false,
            VerifyInboundChecksum = true,
        });

        using var cts = new CancellationTokenSource();
        Task run = link.RunAsync(cts.Token);

        byte[] frame = PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 });
        frame[^1] ^= 0x01; // corrupt checksum
        fake.Enqueue(frame);

        byte[] reply = await fake.NextWriteAsync();
        Assert.Equal(new byte[] { (byte)RioControl.Nak }, reply);

        cts.Cancel();
        await run;
    }

    [Fact]
    public async Task BadChecksumButton_IsAckd_WhenVerificationDisabled()
    {
        var fake = new FakeTransport();
        var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });

        using var cts = new CancellationTokenSource();
        Task run = link.RunAsync(cts.Token);

        byte[] frame = PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 });
        frame[^1] ^= 0x01;
        fake.Enqueue(frame);

        byte[] reply = await fake.NextWriteAsync();
        Assert.Equal(new byte[] { (byte)RioControl.Ack }, reply);

        cts.Cancel();
        await run;
    }

    [Fact]
    public async Task VersionReply_DecodesAndRaisesEvent()
    {
        var fake = new FakeTransport();
        var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });

        var gotVersion = new TaskCompletionSource<VersionInfo>(TaskCreationOptions.RunContinuationsAsynchronously);
        link.VersionReceived += v => gotVersion.TrySetResult(v);

        using var cts = new CancellationTokenSource();
        Task run = link.RunAsync(cts.Token);

        fake.Enqueue(PacketBuilder.Build(RioCommand.VersionReply, new byte[] { 0, 3 }));

        VersionInfo version = await gotVersion.Task.WaitAsync(Timeout);
        Assert.Equal(0, version.Major);
        Assert.Equal(3, version.Minor);

        cts.Cancel();
        await run;
    }

    [Fact]
    public async Task ControlByte_RaisesControlReceived()
    {
        var fake = new FakeTransport();
        var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });

        var gotControl = new TaskCompletionSource<byte>(TaskCreationOptions.RunContinuationsAsynchronously);
        link.ControlReceived += b => gotControl.TrySetResult(b);

        using var cts = new CancellationTokenSource();
        Task run = link.RunAsync(cts.Token);

        fake.Enqueue((byte)RioControl.Ack);

        byte control = await gotControl.Task.WaitAsync(Timeout);
        Assert.Equal((byte)RioControl.Ack, control);

        cts.Cancel();
        await run;
    }

    [Fact]
    public async Task AutoPoll_SendsAnalogRequest()
    {
        var fake = new FakeTransport();
        var link = new RioSerialLink(fake, new RioSerialLinkOptions
        {
            AutoPollAnalog = true,
            AnalogPollInterval = TimeSpan.FromMilliseconds(20),
        });

        using var cts = new CancellationTokenSource();
        Task run = link.RunAsync(cts.Token);

        byte[] firstWrite = await fake.NextWriteAsync();
        Assert.Equal(PacketBuilder.AnalogRequest(), firstWrite);

        cts.Cancel();
        await run;
    }
}
