using RioJoy.Core.Mapping;
using Xunit;

namespace RioJoy.Core.Tests.Mapping;

public class InputRouterTests
{
    private const byte SolidDim = 0x14;
    private const byte SolidBright = 0x3C;

    private static (InputRouter router, RecordingSink sink) Build(
        int address, ushort raw, int mouseStep = 50)
    {
        var map = new RioInputMap { [address] = new RioMapEntry(raw) };
        var sink = new RecordingSink();
        var router = new InputRouter(map, sink, sink, sink, sink, mouseStep);
        return (router, sink);
    }

    [Fact]
    public void Keyboard_WithModifiers_PressesInOrderAndLights()
    {
        // 0x8749 = hasLamp + alt + ctrl + shift, VK 0x49
        (InputRouter router, RecordingSink sink) = Build(0x05, 0x8749);

        router.Press(0x05);
        Assert.Equal(new[]
        {
            "KeyDown(0x10,ext=False)", // SHIFT
            "KeyDown(0x11,ext=False)", // CTRL
            "KeyDown(0x12,ext=False)", // ALT
            "KeyDown(0x49,ext=False)", // key
            $"Lamp(0x05,0x{SolidBright:X2})",
        }, sink.Log);

        sink.Log.Clear();
        router.Release(0x05);
        Assert.Equal(new[]
        {
            "KeyUp(0x49,ext=False)", // key first
            "KeyUp(0x12,ext=False)", // then ALT, CTRL, SHIFT (reverse)
            "KeyUp(0x11,ext=False)",
            "KeyUp(0x10,ext=False)",
            $"Lamp(0x05,0x{SolidDim:X2})",
        }, sink.Log);
    }

    [Fact]
    public void Keyboard_Extended_NoLamp()
    {
        // 0x08BE = extended, VK 0xBE, no lamp
        (InputRouter router, RecordingSink sink) = Build(0x10, 0x08BE);

        router.Press(0x10);
        Assert.Equal(new[] { "KeyDown(0xBE,ext=True)" }, sink.Log);

        sink.Log.Clear();
        router.Release(0x10);
        Assert.Equal(new[] { "KeyUp(0xBE,ext=True)" }, sink.Log);
    }

    [Fact]
    public void Joystick_PressReleaseWithLamp()
    {
        // 0x9009 = hasLamp + joy, button 9
        (InputRouter router, RecordingSink sink) = Build(0x03, 0x9009);

        router.Press(0x03);
        Assert.Equal(new[] { "Joy(9,True)", $"Lamp(0x03,0x{SolidBright:X2})" }, sink.Log);

        sink.Log.Clear();
        router.Release(0x03);
        Assert.Equal(new[] { "Joy(9,False)", $"Lamp(0x03,0x{SolidDim:X2})" }, sink.Log);
    }

    [Fact]
    public void Hat_PressSetsDirection_ReleaseCenters()
    {
        // 0x2002 = hat, value 2 = Down
        (InputRouter router, RecordingSink sink) = Build(0x04, 0x2002);

        router.Press(0x04);
        Assert.Equal(new[] { "Hat(Down)" }, sink.Log);

        sink.Log.Clear();
        router.Release(0x04);
        Assert.Equal(new[] { "Hat(Centered)" }, sink.Log);
    }

    [Theory]
    [InlineData(0x4000, "MouseMove(0,-50)")] // up
    [InlineData(0x4001, "MouseMove(50,0)")]  // right
    [InlineData(0x4002, "MouseMove(0,50)")]  // down
    [InlineData(0x4003, "MouseMove(-50,0)")] // left
    public void Mouse_MovePressOnly(ushort raw, string expectedMove)
    {
        (InputRouter router, RecordingSink sink) = Build(0x06, raw);

        router.Press(0x06);
        Assert.Equal(new[] { expectedMove }, sink.Log);

        sink.Log.Clear();
        router.Release(0x06); // moves do nothing on release
        Assert.Empty(sink.Log);
    }

    [Fact]
    public void Mouse_ClickDownAndUp()
    {
        (InputRouter router, RecordingSink sink) = Build(0x06, 0x4004); // left click

        router.Press(0x06);
        router.Release(0x06);
        Assert.Equal(new[] { "MouseButton(Left,True)", "MouseButton(Left,False)" }, sink.Log);
    }

    [Fact]
    public void Mouse_MoveStepIsConfigurable()
    {
        (InputRouter router, RecordingSink sink) = Build(0x06, 0x4001, mouseStep: 10);
        router.Press(0x06);
        Assert.Equal(new[] { "MouseMove(10,0)" }, sink.Log);
    }

    [Fact]
    public void RioCommand_Executes_AndSkipsLampEvenIfFlagged()
    {
        // 0xF006 = hasLamp + joy + hat + mouse (command), value 6 = GeneralReset
        (InputRouter router, RecordingSink sink) = Build(0x07, 0xF006);

        router.Press(0x07);
        Assert.Equal(new[] { "Cmd(GeneralReset)" }, sink.Log); // no lamp

        sink.Log.Clear();
        router.Release(0x07);
        Assert.Empty(sink.Log); // release does nothing
    }

    [Fact]
    public void Unmapped_Entry_DoesNothing()
    {
        (InputRouter router, RecordingSink sink) = Build(0x08, 0x0000);
        router.Press(0x08);
        router.Release(0x08);
        Assert.Empty(sink.Log);
    }

    [Fact]
    public void InitializeLamps_DimsOnlyLampEntries()
    {
        var map = new RioInputMap
        {
            [0x00] = new RioMapEntry(0x8041), // hasLamp keyboard
            [0x01] = new RioMapEntry(0x0041), // no lamp
            [0x02] = new RioMapEntry(0x9009), // hasLamp joystick
        };
        var sink = new RecordingSink();
        var router = new InputRouter(map, sink, sink, sink, sink);

        router.InitializeLamps();

        Assert.Equal(new[]
        {
            $"Lamp(0x00,0x{SolidDim:X2})",
            $"Lamp(0x02,0x{SolidDim:X2})",
        }, sink.Log);
    }
}
