using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.MxGateway.Worker.Sta;

namespace ZB.MOM.WW.MxGateway.Worker.Tests.Sta;

/// <summary>
/// Tests for <see cref="StaMessagePump"/>.
/// </summary>
/// <remarks>
/// Boundary: the <c>MsgWaitFailed</c> failure branch of <c>WaitForWorkOrMessages</c>
/// is not exercised. Forcing <c>MsgWaitForMultipleObjectsEx</c> to fail requires
/// passing a deliberately invalid native handle, which is unsafe to construct in a
/// managed test and can corrupt the thread's wait state. The other behavior — null
/// argument validation, waking on a signalled event, returning on timeout, the
/// timeout conversion edge cases observable through wait latency, and the
/// pump's drain count — is covered directly here.
/// </remarks>
public sealed class StaMessagePumpTests
{
    /// <summary>
    /// Verifies that WaitForWorkOrMessages throws ArgumentNullException for a null wake event.
    /// </summary>
    [Fact]
    public void WaitForWorkOrMessages_NullWakeEvent_ThrowsArgumentNullException()
    {
        StaMessagePump pump = new();

        ArgumentNullException exception = Assert.Throws<ArgumentNullException>(
            () => pump.WaitForWorkOrMessages(null!, TimeSpan.FromMilliseconds(10)));

        Assert.Equal("commandWakeEvent", exception.ParamName);
    }

    /// <summary>
    /// Verifies that WaitForWorkOrMessages returns promptly when the wake event is already signalled.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WaitForWorkOrMessages_WakeEventAlreadySignalled_ReturnsImmediately()
    {
        StaMessagePump pump = new();
        using ManualResetEventSlim wakeEvent = new(initialState: true);

        await RunOnStaThreadAsync(() =>
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            pump.WaitForWorkOrMessages(wakeEvent.WaitHandle, TimeSpan.FromSeconds(30));
            stopwatch.Stop();

            // A 30s timeout was supplied; returning quickly proves the signalled
            // wake handle — not the timeout — ended the wait.
            Assert.True(
                stopwatch.Elapsed < TimeSpan.FromSeconds(5),
                $"Wait took {stopwatch.Elapsed}; a pre-signalled wake event should return immediately.");
        });
    }

    /// <summary>
    /// Verifies that WaitForWorkOrMessages wakes when the wake event is signalled from another thread.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WaitForWorkOrMessages_WakeEventSignalledDuringWait_Returns()
    {
        StaMessagePump pump = new();
        using ManualResetEventSlim wakeEvent = new(initialState: false);

        Task signalTask = Task.Run(async () =>
        {
            await Task.Delay(150, CancellationToken.None);
            wakeEvent.Set();
        });

        await RunOnStaThreadAsync(() =>
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            pump.WaitForWorkOrMessages(wakeEvent.WaitHandle, TimeSpan.FromSeconds(30));
            stopwatch.Stop();

            Assert.True(
                stopwatch.Elapsed < TimeSpan.FromSeconds(10),
                $"Wait took {stopwatch.Elapsed}; signalling the wake event should end the 30s wait early.");
        });

        await signalTask;
    }

    /// <summary>
    /// Verifies that WaitForWorkOrMessages returns on timeout when the wake event is never signalled.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WaitForWorkOrMessages_WakeEventNeverSignalled_ReturnsAfterTimeout()
    {
        StaMessagePump pump = new();
        using ManualResetEventSlim wakeEvent = new(initialState: false);

        await RunOnStaThreadAsync(() =>
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            pump.WaitForWorkOrMessages(wakeEvent.WaitHandle, TimeSpan.FromMilliseconds(150));
            stopwatch.Stop();

            // The wait must end of its own accord (timeout). Lower bound is loose
            // because the timeout converts via Math.Ceiling and the OS scheduler
            // adds slack; upper bound proves it is not waiting indefinitely.
            Assert.True(
                stopwatch.Elapsed < TimeSpan.FromSeconds(10),
                $"Wait took {stopwatch.Elapsed}; a 150ms timeout should end the wait without a signal.");
        });
    }

    /// <summary>
    /// Verifies that a zero timeout (the TimeSpan.Zero conversion branch) returns without blocking.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WaitForWorkOrMessages_ZeroTimeout_ReturnsWithoutBlocking()
    {
        StaMessagePump pump = new();
        using ManualResetEventSlim wakeEvent = new(initialState: false);

        await RunOnStaThreadAsync(() =>
        {
            Stopwatch stopwatch = Stopwatch.StartNew();

            // TimeSpan.Zero exercises the "<= Zero -> 0 ms" conversion branch:
            // MsgWaitForMultipleObjectsEx polls and returns immediately.
            pump.WaitForWorkOrMessages(wakeEvent.WaitHandle, TimeSpan.Zero);
            stopwatch.Stop();

            Assert.True(
                stopwatch.Elapsed < TimeSpan.FromSeconds(2),
                $"Wait took {stopwatch.Elapsed}; a zero timeout must not block.");
        });
    }

    /// <summary>
    /// Verifies that PumpPendingMessages returns zero when the STA thread message queue is empty.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task PumpPendingMessages_NoMessagesPosted_ReturnsZero()
    {
        StaMessagePump pump = new();

        int pumped = await RunOnStaThreadAsync(() =>
        {
            // Drain anything the apartment/thread start posted, then measure a clean queue.
            pump.PumpPendingMessages();
            return pump.PumpPendingMessages();
        });

        Assert.Equal(0, pumped);
    }

    /// <summary>
    /// Verifies that PumpPendingMessages dispatches and counts messages posted to the STA thread.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task PumpPendingMessages_MessagesPostedToStaThread_ReturnsCountProcessed()
    {
        StaMessagePump pump = new();

        int pumped = await RunOnStaThreadAsync(() =>
        {
            // Clear any startup messages so the count reflects only what we post.
            pump.PumpPendingMessages();

            uint threadId = GetCurrentThreadId();
            Assert.True(PostThreadMessage(threadId, WmNull, UIntPtr.Zero, IntPtr.Zero));
            Assert.True(PostThreadMessage(threadId, WmNull, UIntPtr.Zero, IntPtr.Zero));
            Assert.True(PostThreadMessage(threadId, WmNull, UIntPtr.Zero, IntPtr.Zero));

            return pump.PumpPendingMessages();
        });

        Assert.Equal(3, pumped);
    }

    /// <summary>
    /// Verifies that WaitForWorkOrMessages returns once a Windows message is posted to the STA thread.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WaitForWorkOrMessages_WindowsMessagePosted_ReturnsForInputAvailable()
    {
        StaMessagePump pump = new();
        using ManualResetEventSlim wakeEvent = new(initialState: false);
        using ManualResetEventSlim threadReady = new(initialState: false);
        uint staThreadId = 0;

        Task staTask = RunOnStaThreadAsync(() =>
        {
            staThreadId = GetCurrentThreadId();
            pump.PumpPendingMessages();
            threadReady.Set();

            Stopwatch stopwatch = Stopwatch.StartNew();
            // The wake event is never signalled. Only the posted Windows message
            // (QS_ALLINPUT wake mask) can end this 30s wait early.
            pump.WaitForWorkOrMessages(wakeEvent.WaitHandle, TimeSpan.FromSeconds(30));
            stopwatch.Stop();

            Assert.True(
                stopwatch.Elapsed < TimeSpan.FromSeconds(10),
                $"Wait took {stopwatch.Elapsed}; a posted Windows message should wake the pump.");
        });

        Assert.True(threadReady.Wait(TimeSpan.FromSeconds(5)), "STA thread did not start.");
        await Task.Delay(100, CancellationToken.None);
        Assert.True(
            PostThreadMessage(staThreadId, WmNull, UIntPtr.Zero, IntPtr.Zero),
            "Failed to post a Windows message to the STA thread.");

        await staTask;
    }

    private const uint WmNull = 0x0000;

    /// <summary>Runs an action on a dedicated STA thread and returns when it completes.</summary>
    private static Task RunOnStaThreadAsync(Action action)
    {
        return RunOnStaThreadAsync(() =>
        {
            action();
            return 0;
        });
    }

    /// <summary>Runs a function on a dedicated STA thread and returns its result.</summary>
    private static Task<T> RunOnStaThreadAsync<T>(Func<T> function)
    {
        TaskCompletionSource<T> completion = new();
        Thread thread = new(() =>
        {
            try
            {
                completion.SetResult(function());
            }
            catch (Exception exception)
            {
                completion.SetException(exception);
            }
        })
        {
            IsBackground = true,
        };
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        return completion.Task;
    }

    [System.Runtime.InteropServices.DllImport("kernel32.dll")]
    private static extern uint GetCurrentThreadId();

    [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
    private static extern bool PostThreadMessage(
        uint threadId,
        uint message,
        UIntPtr wParam,
        IntPtr lParam);
}
