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

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

public sealed class StaRuntimeTests
{
    /// <summary>Verifies that InvokeAsync executes commands on the STA thread.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_ExecutesCommandOnStaThread()
    {
        RecordingComApartmentInitializer initializer = new();
        using StaRuntime runtime = CreateRuntime(initializer);

        runtime.Start();

        StaCommandObservation observation = await runtime.InvokeAsync(
            () => new StaCommandObservation(
                Thread.CurrentThread.ManagedThreadId,
                Thread.CurrentThread.GetApartmentState()));

        Assert.Equal(runtime.StaThreadId, observation.ThreadId);
        Assert.Equal(initializer.InitializeThreadId, observation.ThreadId);
        Assert.Equal(ApartmentState.STA, observation.ApartmentState);
    }

    /// <summary>
    /// Verifies that InvokeAsync wakes the idle pump when a command is queued.
    /// The pump is configured with a 30-second idle period — far longer than
    /// any reasonable test run — so the awaited command completing at all proves
    /// the command wake event (not the idle pump tick) drove the dispatch. No
    /// wall-clock assertion is used: a loaded CI agent can stall an otherwise
    /// correct dispatch past an arbitrary millisecond budget, which would be a
    /// false failure.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_WakesIdlePumpForQueuedCommand()
    {
        RecordingComApartmentInitializer initializer = new();
        using StaRuntime runtime = new(
            initializer,
            new StaMessagePump(),
            TimeSpan.FromSeconds(30));
        runtime.Start();

        int threadId = await runtime.InvokeAsync(() => Thread.CurrentThread.ManagedThreadId);

        Assert.Equal(runtime.StaThreadId, threadId);
    }

    /// <summary>Verifies that Shutdown stops the thread and uninitializes the COM apartment.</summary>
    [Fact]
    public void Shutdown_StopsThreadAndUninitializesComApartment()
    {
        RecordingComApartmentInitializer initializer = new();
        using StaRuntime runtime = CreateRuntime(initializer);
        runtime.Start();

        bool stopped = runtime.Shutdown(TimeSpan.FromSeconds(2));

        Assert.True(stopped);
        Assert.False(runtime.IsRunning);
        Assert.Equal(1, initializer.InitializeCount);
        Assert.Equal(1, initializer.UninitializeCount);
        Assert.Equal(initializer.InitializeThreadId, initializer.UninitializeThreadId);
    }

    /// <summary>Verifies that LastActivityUtc updates while the pump is idle.</summary>
    [Fact]
    public void LastActivityUtc_UpdatesWhilePumpIsIdle()
    {
        RecordingComApartmentInitializer initializer = new();
        using StaRuntime runtime = CreateRuntime(initializer);
        runtime.Start();
        DateTimeOffset firstActivity = runtime.LastActivityUtc;

        bool updated = SpinWait.SpinUntil(
            () => runtime.LastActivityUtc > firstActivity,
            TimeSpan.FromSeconds(2));

        Assert.True(updated);
    }

    /// <summary>
    ///     Verifies that <see cref="StaRuntime.PumpPendingMessages"/> refreshes
    ///     <see cref="StaRuntime.LastActivityUtc"/>. A long synchronous STA
    ///     command (for example <c>ReadBulk</c> waiting <c>timeout_ms</c> for
    ///     the first <c>OnDataChange</c>) invokes the pump step on every wait
    ///     iteration while it legitimately holds the STA thread; refreshing
    ///     activity here keeps the watchdog from mistaking a busy STA for a hung
    ///     one. The runtime is deliberately left unstarted so the only
    ///     source of activity is the pump call under test, not the idle loop.
    /// </summary>
    [Fact]
    public void PumpPendingMessages_RefreshesLastActivity()
    {
        RecordingComApartmentInitializer initializer = new();
        using StaRuntime runtime = CreateRuntime(initializer);
        DateTimeOffset before = runtime.LastActivityUtc;

        bool refreshed = SpinWait.SpinUntil(
            () =>
            {
                runtime.PumpPendingMessages();
                return runtime.LastActivityUtc > before;
            },
            TimeSpan.FromSeconds(2));

        Assert.True(refreshed, "PumpPendingMessages must advance LastActivityUtc.");
    }

    /// <summary>Verifies that InvokeAsync faults the returned task when a command raises an exception without stopping the runtime.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_CommandException_FaultsReturnedTaskWithoutStoppingRuntime()
    {
        RecordingComApartmentInitializer initializer = new();
        using StaRuntime runtime = CreateRuntime(initializer);
        runtime.Start();

        InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => runtime.InvokeAsync<int>(() => throw new InvalidOperationException("command failed")));

        int threadId = await runtime.InvokeAsync(() => Thread.CurrentThread.ManagedThreadId);
        Assert.Equal("command failed", exception.Message);
        Assert.Equal(runtime.StaThreadId, threadId);
    }

    /// <summary>
    ///     Verifies that InvokeAsync returns a faulted task when called after
    ///     Shutdown. <see cref="StaRuntimeShutdownException"/> is
    ///     a dedicated subtype of <see cref="InvalidOperationException"/> so
    ///     callers — notably <c>MxAccessStaSession.RunAlarmPollLoopAsync</c> —
    ///     can distinguish the graceful shutdown signal from a vanilla
    ///     <see cref="InvalidOperationException"/> such as an STA-affinity
    ///     assertion. The test pins the exact type so a regression that
    ///     reverts to a plain <c>InvalidOperationException</c> fails here.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_AfterShutdown_ReturnsFaultedTask()
    {
        RecordingComApartmentInitializer initializer = new();
        using StaRuntime runtime = CreateRuntime(initializer);
        runtime.Start();
        runtime.Shutdown(TimeSpan.FromSeconds(2));

        StaRuntimeShutdownException exception = await Assert.ThrowsAsync<StaRuntimeShutdownException>(
            () => runtime.InvokeAsync(() => Thread.CurrentThread.ManagedThreadId));

        Assert.Contains("shutting down", exception.Message);
    }

    private static StaRuntime CreateRuntime(RecordingComApartmentInitializer initializer)
    {
        return new StaRuntime(
            initializer,
            new StaMessagePump(),
            TimeSpan.FromMilliseconds(25));
    }

    /// <summary>Records the thread ID and apartment state of an STA command execution.</summary>
    private sealed class StaCommandObservation
    {
        /// <summary>Initializes a new instance of the StaCommandObservation class.</summary>
        /// <param name="threadId">Managed thread ID where the command executed.</param>
        /// <param name="apartmentState">COM apartment state of the thread.</param>
        public StaCommandObservation(int threadId, ApartmentState apartmentState)
        {
            ThreadId = threadId;
            ApartmentState = apartmentState;
        }

        /// <summary>The thread ID where the command executed.</summary>
        public int ThreadId { get; }

        /// <summary>The apartment state of the thread.</summary>
        public ApartmentState ApartmentState { get; }
    }

    private sealed class RecordingComApartmentInitializer : IStaComApartmentInitializer
    {
        /// <summary>The number of times Initialize was called.</summary>
        public int InitializeCount { get; private set; }

        /// <summary>The number of times Uninitialize was called.</summary>
        public int UninitializeCount { get; private set; }

        /// <summary>The thread ID where Initialize was called.</summary>
        public int? InitializeThreadId { get; private set; }

        /// <summary>The thread ID where Uninitialize was called.</summary>
        public int? UninitializeThreadId { get; private set; }

        /// <inheritdoc />
        public void Initialize()
        {
            InitializeCount++;
            InitializeThreadId = Thread.CurrentThread.ManagedThreadId;
        }

        /// <inheritdoc />
        public void Uninitialize()
        {
            UninitializeCount++;
            UninitializeThreadId = Thread.CurrentThread.ManagedThreadId;
        }
    }
}
