#nullable enable
using System.Collections.Concurrent;
using Xunit.Abstractions;

namespace UnitTests_Parallelizable.DriverTests;

/// <summary>
///     Parallelizable unit tests for IInputProcessor.EnqueueMouseEvent.
///     Tests validate the entire pipeline: MouseEventArgs → TInputRecord → Queue → ProcessQueue → Events.
///     fully implemented in InputProcessorImpl (base class). Only WindowsInputProcessor has a working implementation.
/// </summary>
[Trait ("Category", "Input")]
public class EnqueueMouseEventTests (ITestOutputHelper output)
{
    private readonly ITestOutputHelper _output = output;

    #region Mouse Event Sequencing Tests

    [Fact]
    public void FakeInput_EnqueueMouseEvent_HandlesCompleteClickSequence ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        List<MouseEventArgs> receivedEvents = [];
        processor.MouseEvent += (_, e) => receivedEvents.Add (e);

        // Act - Simulate a complete click: press → release → click
        processor.EnqueueMouseEvent (
                                     new()
                                     {
                                         Position = new (10, 5),
                                         Flags = MouseFlags.Button1Pressed
                                     });

        processor.EnqueueMouseEvent (
                                     new()
                                     {
                                         Position = new (10, 5),
                                         Flags = MouseFlags.Button1Released
                                     });

        // The MouseInterpreter in the processor should generate a clicked event

        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        // We should see at least the pressed and released events
        Assert.True (receivedEvents.Count >= 2);
        Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Pressed));
        Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Released));
    }

    #endregion

    #region Thread Safety Tests

    [Fact]
    public void FakeInput_EnqueueMouseEvent_IsThreadSafe ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        ConcurrentBag<MouseEventArgs> receivedEvents = [];
        processor.MouseEvent += (_, e) => receivedEvents.Add (e);

        const int threadCount = 10;
        const int eventsPerThread = 100;
        Thread [] threads = new Thread [threadCount];

        // Act - Enqueue mouse events from multiple threads
        for (var t = 0; t < threadCount; t++)
        {
            int threadId = t;

            threads [t] = new (() =>
                               {
                                   for (var i = 0; i < eventsPerThread; i++)
                                   {
                                       processor.EnqueueMouseEvent (
                                                                    new()
                                                                    {
                                                                        Position = new (threadId, i),
                                                                        Flags = MouseFlags.Button1Clicked
                                                                    });
                                   }
                               });
            threads [t].Start ();
        }

        // Wait for all threads to complete
        foreach (Thread thread in threads)
        {
            thread.Join ();
        }

        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        Assert.Equal (threadCount * eventsPerThread, receivedEvents.Count);
    }

    #endregion

    #region Helper Methods

    /// <summary>
    ///     Simulates the input thread by manually draining FakeInput's internal queue
    ///     and moving items to the InputBuffer. This is needed because tests don't
    ///     start the actual input thread via Run().
    /// </summary>
    private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
    {
        // FakeInput's Peek() checks _testInput
        while (fakeInput.Peek ())
        {
            // Read() drains _testInput and returns items
            foreach (ConsoleKeyInfo item in fakeInput.Read ())
            {
                // Manually add to InputBuffer (simulating what Run() would do)
                inputBuffer.Enqueue (item);
            }
        }
    }

    #endregion

    #region FakeInputProcessor EnqueueMouseEvent Tests

    [Fact]
    public void FakeInput_EnqueueMouseEvent_AddsSingleMouseEventToQueue ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        List<MouseEventArgs> receivedEvents = [];
        processor.MouseEvent += (_, e) => receivedEvents.Add (e);

        MouseEventArgs mouseEvent = new ()
        {
            Position = new (10, 5),
            Flags = MouseFlags.Button1Clicked
        };

        // Act
        processor.EnqueueMouseEvent (mouseEvent);

        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert - Verify the mouse event made it through
        Assert.Single (receivedEvents);
        Assert.Equal (mouseEvent.Position, receivedEvents [0].Position);
        Assert.Equal (mouseEvent.Flags, receivedEvents [0].Flags);
    }

    [Fact]
    public void FakeInput_EnqueueMouseEvent_SupportsMultipleEvents ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        MouseEventArgs [] events =
        [
            new () { Position = new (10, 5), Flags = MouseFlags.Button1Pressed },
            new () { Position = new (10, 5), Flags = MouseFlags.Button1Released },
            new () { Position = new (15, 8), Flags = MouseFlags.ReportMousePosition },
            new () { Position = new (20, 10), Flags = MouseFlags.Button1Clicked }
        ];

        List<MouseEventArgs> receivedEvents = [];
        processor.MouseEvent += (_, e) => receivedEvents.Add (e);

        // Act
        foreach (MouseEventArgs mouseEvent in events)
        {
            processor.EnqueueMouseEvent (mouseEvent);
        }

        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        // The MouseInterpreter processes Button1Pressed followed by Button1Released and generates
        // an additional Button1Clicked event, so we expect 5 events total:
        // 1. Button1Pressed (original)
        // 2. Button1Released (original)
        // 3. Button1Clicked (generated by MouseInterpreter from press+release)
        // 4. ReportMousePosition (original)
        // 5. Button1Clicked (original)
        Assert.Equal (5, receivedEvents.Count);

        // Verify the original events are present
        Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Pressed && e.Position == new Point (10, 5));
        Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Released && e.Position == new Point (10, 5));
        Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.ReportMousePosition && e.Position == new Point (15, 8));
        
        // There should be two clicked events: one generated, one original
        var clickedEvents = receivedEvents.Where (e => e.Flags == MouseFlags.Button1Clicked).ToList ();
        Assert.Equal (2, clickedEvents.Count);
        Assert.Contains (clickedEvents, e => e.Position == new Point (10, 5)); // Generated from press+release
        Assert.Contains (clickedEvents, e => e.Position == new Point (20, 10)); // Original
    }

    [Theory]
    [InlineData (MouseFlags.Button1Clicked)]
    [InlineData (MouseFlags.Button2Clicked)]
    [InlineData (MouseFlags.Button3Clicked)]
    [InlineData (MouseFlags.Button4Clicked)]
    [InlineData (MouseFlags.Button1DoubleClicked)]
    [InlineData (MouseFlags.Button1TripleClicked)]
    public void FakeInput_EnqueueMouseEvent_SupportsAllButtonClicks (MouseFlags flags)
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        MouseEventArgs mouseEvent = new ()
        {
            Position = new (10, 5),
            Flags = flags
        };

        MouseEventArgs? receivedEvent = null;
        processor.MouseEvent += (_, e) => receivedEvent = e;

        // Act
        processor.EnqueueMouseEvent (mouseEvent);
        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        Assert.NotNull (receivedEvent);
        Assert.Equal (flags, receivedEvent.Flags);
    }

    [Theory]
    [InlineData (0, 0)]
    [InlineData (10, 5)]
    [InlineData (79, 24)] // Near screen edge (assuming 80x25)
    [InlineData (100, 100)] // Beyond typical screen
    public void FakeInput_EnqueueMouseEvent_PreservesPosition (int x, int y)
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        MouseEventArgs mouseEvent = new ()
        {
            Position = new (x, y),
            Flags = MouseFlags.Button1Clicked
        };

        MouseEventArgs? receivedEvent = null;
        processor.MouseEvent += (_, e) => receivedEvent = e;

        // Act
        processor.EnqueueMouseEvent (mouseEvent);
        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        Assert.NotNull (receivedEvent);
        Assert.Equal (x, receivedEvent.Position.X);
        Assert.Equal (y, receivedEvent.Position.Y);
    }

    [Theory]
    [InlineData (MouseFlags.ButtonShift)]
    [InlineData (MouseFlags.ButtonCtrl)]
    [InlineData (MouseFlags.ButtonAlt)]
    [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl)]
    [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonAlt)]
    [InlineData (MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
    [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
    public void FakeInput_EnqueueMouseEvent_PreservesModifiers (MouseFlags modifiers)
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        MouseEventArgs mouseEvent = new ()
        {
            Position = new (10, 5),
            Flags = MouseFlags.Button1Clicked | modifiers
        };

        MouseEventArgs? receivedEvent = null;
        processor.MouseEvent += (_, e) => receivedEvent = e;

        // Act
        processor.EnqueueMouseEvent (mouseEvent);
        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        Assert.NotNull (receivedEvent);
        Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.Button1Clicked));

        if (modifiers.HasFlag (MouseFlags.ButtonShift))
        {
            Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonShift));
        }

        if (modifiers.HasFlag (MouseFlags.ButtonCtrl))
        {
            Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonCtrl));
        }

        if (modifiers.HasFlag (MouseFlags.ButtonAlt))
        {
            Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonAlt));
        }
    }

    [Theory]
    [InlineData (MouseFlags.WheeledUp)]
    [InlineData (MouseFlags.WheeledDown)]
    [InlineData (MouseFlags.WheeledLeft)]
    [InlineData (MouseFlags.WheeledRight)]
    public void FakeInput_EnqueueMouseEvent_SupportsMouseWheel (MouseFlags wheelFlag)
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        MouseEventArgs mouseEvent = new ()
        {
            Position = new (10, 5),
            Flags = wheelFlag
        };

        MouseEventArgs? receivedEvent = null;
        processor.MouseEvent += (_, e) => receivedEvent = e;

        // Act
        processor.EnqueueMouseEvent (mouseEvent);
        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        Assert.NotNull (receivedEvent);
        Assert.True (receivedEvent.Flags.HasFlag (wheelFlag));
    }

    [Fact]
    public void FakeInput_EnqueueMouseEvent_SupportsMouseMove ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        List<MouseEventArgs> receivedEvents = [];
        processor.MouseEvent += (_, e) => receivedEvents.Add (e);

        MouseEventArgs [] events =
        [
            new () { Position = new (0, 0), Flags = MouseFlags.ReportMousePosition },
            new () { Position = new (5, 5), Flags = MouseFlags.ReportMousePosition },
            new () { Position = new (10, 10), Flags = MouseFlags.ReportMousePosition }
        ];

        // Act
        foreach (MouseEventArgs mouseEvent in events)
        {
            processor.EnqueueMouseEvent (mouseEvent);
        }

        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert
        Assert.Equal (3, receivedEvents.Count);
        Assert.Equal (new (0, 0), receivedEvents [0].Position);
        Assert.Equal (new (5, 5), receivedEvents [1].Position);
        Assert.Equal (new (10, 10), receivedEvents [2].Position);
    }

    #endregion

    #region InputProcessor Pipeline Tests

    [Fact]
    public void InputProcessor_EnqueueMouseEvent_DoesNotThrow ()
    {
        // Arrange
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        var processor = new FakeInputProcessor (queue);

        // Don't set InputImpl (or set to non-testable)

        // Act & Assert - Should not throw even if not implemented
        Exception? exception = Record.Exception (() =>
                                                 {
                                                     processor.EnqueueMouseEvent (
                                                                                  new()
                                                                                  {
                                                                                      Position = new (10, 5),
                                                                                      Flags = MouseFlags.Button1Clicked
                                                                                  });
                                                     processor.ProcessQueue ();
                                                 });

        // The base implementation logs a critical message but doesn't throw
        Assert.Null (exception);
    }

    [Fact]
    public void InputProcessor_ProcessQueue_DrainsPendingMouseEvents ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        List<MouseEventArgs> receivedEvents = [];
        processor.MouseEvent += (_, e) => receivedEvents.Add (e);

        // Act - Enqueue multiple events before processing
        processor.EnqueueMouseEvent (new() { Position = new (1, 1), Flags = MouseFlags.Button1Pressed });
        processor.EnqueueMouseEvent (new() { Position = new (2, 2), Flags = MouseFlags.ReportMousePosition });
        processor.EnqueueMouseEvent (new() { Position = new (3, 3), Flags = MouseFlags.Button1Released });

        SimulateInputThread (fakeInput, queue);
        processor.ProcessQueue ();

        // Assert - After processing, all events should be received
        Assert.Empty (queue);
        Assert.Equal (3, receivedEvents.Count);
    }

    #endregion

    #region Error Handling Tests

    [Fact]
    public void FakeInput_EnqueueMouseEvent_WithInvalidEvent_DoesNotThrow ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        // Act & Assert - Empty/default mouse event should not throw
        Exception? exception = Record.Exception (() =>
                                                 {
                                                     processor.EnqueueMouseEvent (new ());
                                                     SimulateInputThread (fakeInput, queue);
                                                     processor.ProcessQueue ();
                                                 });

        Assert.Null (exception);
    }

    [Fact]
    public void FakeInput_EnqueueMouseEvent_WithNegativePosition_DoesNotThrow ()
    {
        // Arrange
        var fakeInput = new FakeInput ();
        ConcurrentQueue<ConsoleKeyInfo> queue = new ();
        fakeInput.Initialize (queue);

        var processor = new FakeInputProcessor (queue);
        processor.InputImpl = fakeInput;

        // Act & Assert - Negative positions should not throw
        Exception? exception = Record.Exception (() =>
                                                 {
                                                     processor.EnqueueMouseEvent (
                                                                                  new()
                                                                                  {
                                                                                      Position = new (-10, -5),
                                                                                      Flags = MouseFlags.Button1Clicked
                                                                                  });
                                                     SimulateInputThread (fakeInput, queue);
                                                     processor.ProcessQueue ();
                                                 });

        Assert.Null (exception);
    }

    #endregion
}
