using VRio.Core.Device;
using VRio.Core.Input;
using VRio.Core.Protocol;
using Xunit;

namespace VRio.Core.Tests;

public class BindingProfileFormatTests
{
    [Fact]
    public void Default_profile_parses_clean()
    {
        BindingProfile profile = BindingProfileFormat.Parse(BindingProfileFormat.DefaultText, out var errors);

        Assert.Empty(errors);
        Assert.Equal(73, profile.KeyButtons.Count);    // 40 MFD + 12 columns + 16 keypad + Space + 4 hat
        Assert.Empty(profile.KeyAxes);                 // axes are pad-only in the default profile
        Assert.Equal(10, profile.PadButtons.Count);
        Assert.Equal(5, profile.PadAxes.Count);
    }

    [Fact]
    public void Lines_parse_into_typed_bindings()
    {
        BindingProfile profile = BindingProfileFormat.Parse(
            "key SPACE button 0x40 toggle   # comment\n" +
            "key w axis throttle rate -0.5\n" +
            "pad dpadup button 81\n" +
            "padaxis lefttrigger axis LeftPedal invert deadzone 0.25 rate 2\n",
            out var errors);

        Assert.Empty(errors);

        KeyButtonBinding kb = Assert.Single(profile.KeyButtons);
        Assert.Equal(("SPACE", 0x40, true), (kb.Key, kb.Address, kb.Toggle));

        KeyAxisBinding ka = Assert.Single(profile.KeyAxes);
        Assert.Equal((RioAxis.Throttle, KeyAxisMode.Rate, -0.5f), (ka.Axis, ka.Mode, ka.Value));

        PadButtonBinding pb = Assert.Single(profile.PadButtons);
        Assert.Equal((PadButtons.DPadUp, 81), (pb.Button, pb.Address)); // decimal keypad address

        PadAxisBinding pa = Assert.Single(profile.PadAxes);
        Assert.Equal((PadAxis.LeftTrigger, RioAxis.LeftPedal, true, 0.25f, 2f),
            (pa.Source, pa.Axis, pa.Invert, pa.Deadzone, pa.Rate));
    }

    [Theory]
    [InlineData("key W button 0x48")]              // hole between buttons and keypads
    [InlineData("key W button zz")]
    [InlineData("key W axis Throttle deflect")]    // missing value
    [InlineData("key W axis Throttle wiggle 1")]   // unknown mode
    [InlineData("pad Guide button 0x00")]          // unknown pad button
    [InlineData("padaxis LeftStickX axis JoystickX sideways")]
    [InlineData("mouse X button 0x00")]            // unknown source
    public void Bad_lines_are_reported_and_skipped(string line)
    {
        BindingProfile profile = BindingProfileFormat.Parse("key A button 0x00\n" + line, out var errors);

        string error = Assert.Single(errors);
        Assert.StartsWith("line 2:", error);
        Assert.Equal(1, profile.Count); // the good line survived
    }
}

public class InputRouterTests
{
    private readonly VRioDevice _device = new();
    private readonly InputRouter _router;
    private readonly List<byte[]> _tx = new();

    public InputRouterTests()
    {
        _router = new InputRouter(_device);
        _device.Transmit += _tx.Add;
    }

    private void UseProfile(string text)
    {
        _router.Profile = BindingProfileFormat.Parse(text, out var errors);
        Assert.Empty(errors);
        _tx.Clear();
    }

    // ---- Buttons ------------------------------------------------------------

    [Fact]
    public void Key_press_and_release_hit_the_wire_once_despite_repeats()
    {
        UseProfile("key Space button 0x40");

        _router.KeyDown("Space");
        _router.KeyDown("Space"); // auto-repeat
        _router.KeyDown("Space");
        _router.KeyUp("Space");

        Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x40), PacketBuilder.ButtonReleased(0x40) }, _tx);
    }

    [Fact]
    public void Keypad_addresses_go_out_as_key_events()
    {
        UseProfile("key NumPad7 button 0x67"); // external keypad, key 7

        _router.KeyDown("NumPad7");
        _router.KeyUp("NumPad7");

        Assert.Equal(new[] { PacketBuilder.KeyPressed(1, 7), PacketBuilder.KeyReleased(1, 7) }, _tx);
    }

    [Fact]
    public void Toggle_latches_across_key_up()
    {
        UseProfile("key T button 0x12 toggle");

        _router.KeyDown("T");
        _router.KeyUp("T");
        Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x12) }, _tx);

        _router.KeyDown("T"); // second press releases the latch
        _router.KeyUp("T");
        Assert.Equal(PacketBuilder.ButtonReleased(0x12), _tx[1]);
    }

    [Fact]
    public void Overlapping_sources_on_one_address_press_once_release_last()
    {
        UseProfile("key Space button 0x40\npad A button 0x40");
        var held = new List<(int Address, bool Held)>();
        _router.AddressHeldChanged += (a, h) => held.Add((a, h));

        _router.KeyDown("Space");
        _router.SetPadState(new PadState(PadButtons.A, 0, 0, 0, 0, 0, 0));
        _router.KeyUp("Space");
        Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x40) }, _tx); // still held by the pad

        _router.SetPadState(default);
        Assert.Equal(PacketBuilder.ButtonReleased(0x40), _tx[1]);
        Assert.Equal(new[] { (0x40, true), (0x40, false) }, held);
    }

    [Fact]
    public void Pad_button_edges_only_fire_on_change()
    {
        UseProfile("pad B button 0x3D");
        var down = new PadState(PadButtons.B, 0, 0, 0, 0, 0, 0);

        _router.SetPadState(down);
        _router.SetPadState(down); // unchanged snapshot
        _router.SetPadState(default);

        Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x3D), PacketBuilder.ButtonReleased(0x3D) }, _tx);
    }

    [Fact]
    public void ReleaseAllKeys_releases_momentaries_but_keeps_toggles()
    {
        UseProfile("key A button 0x01\nkey B button 0x02 toggle");

        _router.KeyDown("A");
        _router.KeyDown("B");
        _router.ReleaseAllKeys();

        Assert.Equal(0, _device.GetLamp(0)); // sanity: device untouched otherwise
        Assert.Equal(new[]
        {
            PacketBuilder.ButtonPressed(0x01),
            PacketBuilder.ButtonPressed(0x02),
            PacketBuilder.ButtonReleased(0x01), // toggle at 0x02 stays latched
        }, _tx);
    }

    // ---- Axes ---------------------------------------------------------------

    [Fact]
    public void Deflect_key_holds_full_travel_and_springs_back()
    {
        UseProfile("key Up axis JoystickY deflect 1\nkey Down axis JoystickY deflect -1");

        _router.KeyDown("Up");
        _router.Tick(0.016);
        Assert.Equal(RioAxisRange.JoystickExtent, _device.GetAxis(RioAxis.JoystickY));

        _router.KeyDown("Down"); // opposite deflects cancel
        _router.Tick(0.016);
        Assert.Equal(0, _device.GetAxis(RioAxis.JoystickY));

        _router.KeyUp("Up");
        _router.KeyUp("Down");
        _router.Tick(0.016);
        Assert.Equal(0, _device.GetAxis(RioAxis.JoystickY));
    }

    [Fact]
    public void Rate_key_walks_the_throttle_and_position_sticks()
    {
        UseProfile("key W axis Throttle rate 0.5");

        _router.KeyDown("W");
        _router.Tick(1.0); // half travel
        Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle));

        _router.KeyUp("W");
        _router.Tick(1.0); // released: stays put
        Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle));

        _router.KeyDown("W");
        _router.Tick(10.0); // clamps at full realistic travel
        Assert.Equal(RioAxisRange.ThrottleFull, _device.GetAxis(RioAxis.Throttle));
    }

    [Fact]
    public void Pad_stick_maps_through_deadzone_and_invert()
    {
        UseProfile("padaxis LeftStickX axis JoystickX invert deadzone 0.2");

        // Inside the deadzone: centered.
        _router.SetPadState(new PadState(PadButtons.None, 0.1f, 0, 0, 0, 0, 0));
        _router.Tick(0.016);
        Assert.Equal(0, _device.GetAxis(RioAxis.JoystickX));

        // Full right, inverted → raw negative full extent (the wire direction).
        _router.SetPadState(new PadState(PadButtons.None, 1f, 0, 0, 0, 0, 0));
        _router.Tick(0.016);
        Assert.Equal(-RioAxisRange.JoystickExtent, _device.GetAxis(RioAxis.JoystickX));

        // Pad gone → springs back.
        _router.SetPadState(default);
        _router.Tick(0.016);
        Assert.Equal(0, _device.GetAxis(RioAxis.JoystickX));
    }

    [Fact]
    public void Pad_trigger_drives_pedal_to_full_press()
    {
        UseProfile("padaxis RightTrigger axis RightPedal");

        _router.SetPadState(new PadState(PadButtons.None, 0, 0, 0, 0, 0, 1f));
        _router.Tick(0.016);
        Assert.Equal(RioAxisRange.PedalFull, _device.GetAxis(RioAxis.RightPedal));
    }

    [Fact]
    public void Rate_mode_pad_axis_integrates_and_holds()
    {
        UseProfile("padaxis RightStickY axis Throttle rate 0.5");

        _router.SetPadState(new PadState(PadButtons.None, 0, 0, 0, 1f, 0, 0));
        _router.Tick(1.0);
        Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle));

        _router.SetPadState(default); // stick released: throttle stays
        _router.Tick(1.0);
        Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle));
    }

    [Fact]
    public void ResetAxisState_forgets_integrated_position()
    {
        UseProfile("key W axis Throttle rate 1");

        _router.KeyDown("W");
        _router.Tick(0.5);
        _router.KeyUp("W");
        Assert.NotEqual(0, _device.GetAxis(RioAxis.Throttle));

        // Emulate the center button / host reset combination.
        _device.SetAxis(RioAxis.Throttle, 0);
        _router.ResetAxisState();
        _router.Tick(0.016);
        Assert.Equal(0, _device.GetAxis(RioAxis.Throttle));
    }

    [Fact]
    public void Idle_router_does_not_fight_other_axis_writers()
    {
        UseProfile("padaxis LeftStickX axis JoystickX");
        _router.Tick(0.016);

        _device.SetAxis(RioAxis.JoystickX, 42); // e.g. a mouse drag on the panel
        _router.Tick(0.016);                    // router value unchanged → no clobber

        Assert.Equal(42, _device.GetAxis(RioAxis.JoystickX));
    }

    [Fact]
    public void Profile_swap_releases_held_inputs()
    {
        UseProfile("key A button 0x05");
        _router.KeyDown("A");

        _router.Profile = BindingProfile.Empty;

        Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x05), PacketBuilder.ButtonReleased(0x05) }, _tx);
    }
}
