using System.Diagnostics;
using RioJoy.Core;
using RioJoy.Core.Mapping;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;
using RioJoy.Core.Tests.Mapping;
using RioJoy.Core.Tests.Serial;
using Xunit;

namespace RioJoy.Core.Tests;

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

    private static async Task WaitUntilAsync(Func<bool> condition)
    {
        var sw = Stopwatch.StartNew();
        while (!condition())
        {
            if (sw.Elapsed > Timeout)
                throw new TimeoutException("Condition not met in time.");
            await Task.Delay(10);
        }
    }

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

        var map = new RioInputMap { [0x05] = new RioMapEntry(0x1009) }; // joystick button 9
        using var runtime = new RioRuntime(link, map, recorder, recorder);
        runtime.Start();

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

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

        await WaitUntilAsync(() => recorder.Snapshot().Contains("Joy(9,True)"));

        cts.Cancel();
        await run;
    }

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

        using var runtime = new RioRuntime(link, new RioInputMap(), recorder, recorder);
        runtime.Start();

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

        // All axes at zero → joystick X/Y centered.
        fake.Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, new byte[10]));

        await WaitUntilAsync(() => recorder.Snapshot().Count(e => e.StartsWith("Axis(")) == 6);

        string[] log = recorder.Snapshot();
        Assert.Contains("Axis(X,16383)", log);
        Assert.Contains("Axis(Y,16383)", log);

        cts.Cancel();
        await run;
    }

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

        // Keypad 1, index 2 → address 0x62; map a keyboard key there (VK 0x41 = 'A').
        var map = new RioInputMap { [0x62] = new RioMapEntry(0x0041) };
        using var runtime = new RioRuntime(link, map, recorder, recorder);
        runtime.Start();

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

        fake.Enqueue(PacketBuilder.Build(RioCommand.KeyPressed, new byte[] { 1, 2 }));

        await WaitUntilAsync(() => recorder.Snapshot().Contains("KeyDown(0x41,ext=False)"));

        cts.Cancel();
        await run;
    }
}
