using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;

/// <summary>
/// Verifies <see cref="EventsHubViewerRegistry"/>, the seam
/// <see cref="DashboardEventBroadcaster"/> consults before cloning and sending
/// an event: a session is "watched" only while at least one hub connection
/// holds a subscription to it, and a dropped connection releases every
/// subscription it held.
/// </summary>
public sealed class EventsHubViewerRegistryTests
{
    /// <summary>A session with no subscriber is not watched.</summary>
    [Fact]
    public void HasViewers_WithNoSubscribers_IsFalse()
    {
        EventsHubViewerRegistry registry = new();

        Assert.False(registry.HasViewers("session-1"));
    }

    /// <summary>Adding then removing the only viewer flips the session back to unwatched.</summary>
    [Fact]
    public void AddViewer_ThenRemoveViewer_TogglesWatchedState()
    {
        EventsHubViewerRegistry registry = new();

        registry.AddViewer("conn-1", "session-1");
        Assert.True(registry.HasViewers("session-1"));

        registry.RemoveViewer("conn-1", "session-1");
        Assert.False(registry.HasViewers("session-1"));
    }

    /// <summary>Each connection counts once: the session stays watched until the last one leaves.</summary>
    [Fact]
    public void RemoveViewer_WithOtherConnectionsStillSubscribed_KeepsSessionWatched()
    {
        EventsHubViewerRegistry registry = new();

        registry.AddViewer("conn-1", "session-1");
        registry.AddViewer("conn-2", "session-1");

        registry.RemoveViewer("conn-1", "session-1");
        Assert.True(registry.HasViewers("session-1"));

        registry.RemoveViewer("conn-2", "session-1");
        Assert.False(registry.HasViewers("session-1"));
    }

    /// <summary>A repeated subscribe from the same connection is idempotent, so one unsubscribe clears it.</summary>
    [Fact]
    public void AddViewer_CalledTwiceForSameConnection_CountsOnce()
    {
        EventsHubViewerRegistry registry = new();

        registry.AddViewer("conn-1", "session-1");
        registry.AddViewer("conn-1", "session-1");

        registry.RemoveViewer("conn-1", "session-1");

        Assert.False(registry.HasViewers("session-1"));
    }

    /// <summary>Viewer counts are tracked per session; unrelated sessions stay unwatched.</summary>
    [Fact]
    public void AddViewer_TracksSessionsIndependently()
    {
        EventsHubViewerRegistry registry = new();

        registry.AddViewer("conn-1", "session-1");

        Assert.True(registry.HasViewers("session-1"));
        Assert.False(registry.HasViewers("session-2"));
    }

    /// <summary>A dropped connection releases every session it held.</summary>
    [Fact]
    public void ReleaseConnection_ReleasesEverySessionTheConnectionHeld()
    {
        EventsHubViewerRegistry registry = new();

        registry.AddViewer("conn-1", "session-1");
        registry.AddViewer("conn-1", "session-2");
        registry.AddViewer("conn-2", "session-2");

        registry.ReleaseConnection("conn-1");

        Assert.False(registry.HasViewers("session-1"));

        // conn-2 still watches session-2.
        Assert.True(registry.HasViewers("session-2"));
    }

    /// <summary>Releasing a connection twice does not double-decrement another connection's subscription.</summary>
    [Fact]
    public void ReleaseConnection_CalledTwice_DoesNotDropOtherViewers()
    {
        EventsHubViewerRegistry registry = new();

        registry.AddViewer("conn-1", "session-1");
        registry.AddViewer("conn-2", "session-1");

        registry.ReleaseConnection("conn-1");
        registry.ReleaseConnection("conn-1");

        Assert.True(registry.HasViewers("session-1"));
    }

    /// <summary>Unmatched removals cannot drive the count negative and strand a session as unwatched.</summary>
    [Fact]
    public void RemoveViewer_WithoutMatchingAdd_LeavesCountAtZero()
    {
        EventsHubViewerRegistry registry = new();

        registry.RemoveViewer("conn-1", "session-1");
        registry.RemoveViewer("conn-1", "session-1");
        registry.ReleaseConnection("conn-1");

        Assert.False(registry.HasViewers("session-1"));

        // A subsequent genuine subscribe must still register as exactly one viewer.
        registry.AddViewer("conn-1", "session-1");
        Assert.True(registry.HasViewers("session-1"));

        registry.RemoveViewer("conn-1", "session-1");
        Assert.False(registry.HasViewers("session-1"));
    }

    /// <summary>Blank connection or session ids are ignored rather than tracked.</summary>
    [Theory]
    [InlineData("", "session-1")]
    [InlineData("   ", "session-1")]
    [InlineData("conn-1", "")]
    [InlineData("conn-1", "   ")]
    public void AddViewer_WithBlankIdentifiers_IsIgnored(string connectionId, string sessionId)
    {
        EventsHubViewerRegistry registry = new();

        registry.AddViewer(connectionId, sessionId);

        Assert.False(registry.HasViewers(sessionId));
        Assert.False(registry.HasViewers("session-1"));
    }

    /// <summary>Concurrent add/remove pairs settle at zero viewers, never at a stuck-on count.</summary>
    [Fact]
    public void AddAndRemoveViewer_UnderConcurrency_SettlesAtZero()
    {
        EventsHubViewerRegistry registry = new();

        Parallel.For(0, 64, i =>
        {
            string connectionId = $"conn-{i}";
            for (int pass = 0; pass < 50; pass++)
            {
                registry.AddViewer(connectionId, "session-1");
                registry.RemoveViewer(connectionId, "session-1");
            }
        });

        Assert.False(registry.HasViewers("session-1"));
    }
}
