using Mbproxy.Admin;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Admin;

/// <summary>
/// Unit tests for <see cref="PlcSubscriptionTracker"/> — the tab-keyed, reconnect-safe
/// record of which PLC detail pages are open. Includes a concurrency stress test, since
/// the tracker is mutated from multiple SignalR hub-dispatch threads.
/// </summary>
[Trait("Category", "Unit")]
public sealed class PlcSubscriptionTrackerTests
{
    [Fact]
    public void Subscribe_ThenRemoveLastConnection_ClearsViewer()
    {
        var t = new PlcSubscriptionTracker();
        t.SubscribePlc("c1", "tab", "plc");
        t.ActivePlcs().ShouldBe(["plc"]);

        t.RemoveConnection("c1");
        t.ActivePlcs().ShouldBeEmpty();
    }

    [Fact]
    public void SameTab_TwoConnections_StaysActiveUntilLastConnectionGone()
    {
        // Reconnect overlap: the same tab briefly holds two connections. Dropping the
        // old one must not release the tab — this is the leak C2 guards against.
        var t = new PlcSubscriptionTracker();
        t.SubscribePlc("c-old", "tab", "plc");
        t.SubscribePlc("c-new", "tab", "plc");

        t.RemoveConnection("c-old");
        t.ActivePlcs().ShouldContain("plc", "the tab is still alive on the second connection");

        t.RemoveConnection("c-new");
        t.ActivePlcs().ShouldBeEmpty();
    }

    [Fact]
    public void DistinctTabs_AreCountedSeparately()
    {
        var t = new PlcSubscriptionTracker();
        t.SubscribePlc("c1", "tab-A", "plc");
        t.SubscribePlc("c2", "tab-B", "plc");

        t.RemoveConnection("c1");
        t.ActivePlcs().ShouldContain("plc", "the second tab still views the PLC");

        t.RemoveConnection("c2");
        t.ActivePlcs().ShouldBeEmpty();
    }

    [Fact]
    public void RepeatedSubscribe_SameTabSamePlc_IsIdempotent()
    {
        var t = new PlcSubscriptionTracker();
        t.SubscribePlc("c1", "tab", "plc");
        t.SubscribePlc("c1", "tab", "plc"); // redundant repeat
        t.ActivePlcs().ShouldBe(["plc"]);

        t.RemoveConnection("c1");
        t.ActivePlcs().ShouldBeEmpty("a repeated subscribe must not inflate the viewer count");
    }

    [Fact]
    public void OneConnection_MultiplePlcs_AllReleasedTogether()
    {
        var t = new PlcSubscriptionTracker();
        t.SubscribePlc("c1", "tab", "plc-a");
        t.SubscribePlc("c1", "tab", "plc-b");
        t.ActivePlcs().Count.ShouldBe(2);

        t.RemoveConnection("c1");
        t.ActivePlcs().ShouldBeEmpty();
    }

    [Fact]
    public void RemoveConnection_Unknown_IsNoOp()
    {
        var t = new PlcSubscriptionTracker();

        Should.NotThrow(() => t.RemoveConnection("never-seen"));
        t.ActivePlcs().ShouldBeEmpty();
    }

    [Fact]
    public async Task ConcurrentSubscribeAndRemove_NeverLeaksOrThrows()
    {
        var t = new PlcSubscriptionTracker();
        const int tasks = 16;
        const int iterations = 5_000;

        await Task.WhenAll(Enumerable.Range(0, tasks).Select(taskNo => Task.Run(() =>
        {
            for (int i = 0; i < iterations; i++)
            {
                string conn = $"c{taskNo}-{i}";
                string tab  = $"tab{taskNo}-{i}";
                t.SubscribePlc(conn, tab, "plc");
                t.RemoveConnection(conn);
            }
        }, TestContext.Current.CancellationToken)));

        t.ActivePlcs().ShouldBeEmpty(
            "every subscribe was paired with a remove — no viewer count may leak");
    }
}
