using Mbproxy.Admin;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Admin;

/// <summary>
/// Unit tests for <see cref="StatusHub"/> — group joins and subscription tracking.
/// Capture arming is the broadcaster's job; the hub only mutates the
/// <see cref="PlcSubscriptionTracker"/>. Uses hand-written SignalR test doubles
/// (see <see cref="SignalRFakes"/>); no SignalR host is started.
/// </summary>
[Trait("Category", "Unit")]
public sealed class StatusHubTests
{
    private static StatusHub MakeHub(
        string connectionId, PlcSubscriptionTracker tracker, out FakeGroupManager groups)
    {
        groups = new FakeGroupManager();
        return new StatusHub(tracker)
        {
            Context = new FakeHubCallerContext(connectionId),
            Groups  = groups,
        };
    }

    [Fact]
    public async Task SubscribeFleet_JoinsFleetGroup()
    {
        var hub = MakeHub("conn-1", new PlcSubscriptionTracker(), out var groups);

        await hub.SubscribeFleet();

        groups.Added.ShouldContain(("conn-1", StatusHub.FleetGroup));
    }

    [Fact]
    public async Task SubscribePlc_JoinsPlcGroup_AndTracksViewer()
    {
        var tracker = new PlcSubscriptionTracker();
        var hub = MakeHub("conn-1", tracker, out var groups);

        await hub.SubscribePlc("plc-1", "tab-A");

        groups.Added.ShouldContain(("conn-1", StatusHub.PlcGroup("plc-1")));
        tracker.ActivePlcs().ShouldContain("plc-1");
    }

    [Fact]
    public async Task Reconnect_SameTab_NewConnection_DoesNotLeakViewer()
    {
        // A transport reconnect: the same browser tab acquires a new ConnectionId and
        // re-subscribes; the old connection's OnDisconnectedAsync then fires late. The
        // PLC must not be left with a stranded viewer once the tab finally closes.
        var tracker = new PlcSubscriptionTracker();

        var first = MakeHub("conn-old", tracker, out _);
        await first.SubscribePlc("plc-1", "tab-A");

        var second = MakeHub("conn-new", tracker, out _);
        await second.SubscribePlc("plc-1", "tab-A");

        await first.OnDisconnectedAsync(null); // late disconnect of the old connection
        tracker.ActivePlcs().ShouldContain("plc-1",
            "the tab is still open on the reconnected connection");

        await second.OnDisconnectedAsync(null); // the tab finally closes
        tracker.ActivePlcs().ShouldBeEmpty("no viewer may be stranded after the tab closes");
    }

    [Fact]
    public async Task TwoTabs_FirstCloseKeepsActive_LastCloseClears()
    {
        var tracker = new PlcSubscriptionTracker();

        var tabA = MakeHub("conn-a", tracker, out _);
        var tabB = MakeHub("conn-b", tracker, out _);
        await tabA.SubscribePlc("plc-1", "tab-A");
        await tabB.SubscribePlc("plc-1", "tab-B");

        await tabA.OnDisconnectedAsync(null);
        tracker.ActivePlcs().ShouldContain("plc-1", "a second tab is still viewing the PLC");

        await tabB.OnDisconnectedAsync(null);
        tracker.ActivePlcs().ShouldBeEmpty();
    }

    [Fact]
    public async Task SubscribePlc_UnknownPlc_DoesNotThrow()
    {
        var hub = MakeHub("conn-1", new PlcSubscriptionTracker(), out var groups);

        await Should.NotThrowAsync(async () => await hub.SubscribePlc("ghost", "tab-A"));

        groups.Added.ShouldContain(("conn-1", StatusHub.PlcGroup("ghost")));
    }
}
