using System.Collections.Frozen;
using Mbproxy.Bcd;
using Mbproxy.Proxy;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Proxy;

/// <summary>
/// Unit tests for <see cref="TagCaptureRegistry"/> — the shared seam holding per-PLC
/// <see cref="TagValueCapture"/> instances. Arm state is reconciled in bulk against the
/// live viewer set (not toggled per PLC) so the broadcaster is the single authority.
/// </summary>
[Trait("Category", "Unit")]
public sealed class TagCaptureRegistryTests
{
    private static BcdTagMap Map(params (ushort addr, byte width)[] tags)
    {
        if (tags.Length == 0)
            return BcdTagMap.Empty;
        var frozen = tags
            .Select(t => BcdTag.Create(t.addr, t.width))
            .ToDictionary(t => t.Address)
            .ToFrozenDictionary();
        return new BcdTagMap(frozen);
    }

    [Fact]
    public void GetOrCreate_ReturnsLiveInstance_OnRepeatCall()
    {
        var registry = new TagCaptureRegistry();
        registry.GetOrCreate("plc-1", Map((100, 16)));
        var second = registry.GetOrCreate("plc-1", Map((100, 16)));

        second.TagCount.ShouldBe(1);
        registry.TryGet("plc-1", out var current).ShouldBeTrue();
        current.ShouldBeSameAs(second);
    }

    [Fact]
    public void GetOrCreate_Rebuild_ProducesDisarmedCapture_AndReconcileReArms()
    {
        // The rebuilt capture is intentionally disarmed: ReconcileArmed re-arms it within
        // one push cycle if the PLC still has a viewer, so arm state is never carried
        // across the rebuild — which removes any arm-vs-rebuild race.
        var registry = new TagCaptureRegistry();
        registry.GetOrCreate("plc-1", Map((100, 16)));
        registry.ReconcileArmed(["plc-1"]);
        registry.TryGet("plc-1", out var armed).ShouldBeTrue();
        armed.IsArmed.ShouldBeTrue();

        // Hot-reload reseat: same PLC, changed tag set.
        var rebuilt = registry.GetOrCreate("plc-1", Map((100, 16), (200, 32)));
        rebuilt.ShouldNotBeSameAs(armed);
        rebuilt.IsArmed.ShouldBeFalse("a rebuilt capture starts disarmed");
        rebuilt.TagCount.ShouldBe(2);

        // The next reconcile re-arms it because the PLC is still viewed.
        registry.ReconcileArmed(["plc-1"]);
        rebuilt.IsArmed.ShouldBeTrue();
    }

    [Fact]
    public void ReconcileArmed_ArmsActivePlcs_DisarmsTheRest()
    {
        var registry = new TagCaptureRegistry();
        registry.GetOrCreate("plc-1", Map((100, 16)));
        registry.GetOrCreate("plc-2", Map((100, 16)));

        registry.ReconcileArmed(["plc-1"]);
        registry.TryGet("plc-1", out var c1).ShouldBeTrue();
        registry.TryGet("plc-2", out var c2).ShouldBeTrue();
        c1.IsArmed.ShouldBeTrue();
        c2.IsArmed.ShouldBeFalse();

        // plc-1's viewer leaves, plc-2 gains one.
        registry.ReconcileArmed(["plc-2"]);
        c1.IsArmed.ShouldBeFalse();
        c2.IsArmed.ShouldBeTrue();
    }

    [Fact]
    public void ReconcileArmed_EmptyActiveSet_DisarmsEverything()
    {
        var registry = new TagCaptureRegistry();
        registry.GetOrCreate("plc-1", Map((100, 16)));
        registry.ReconcileArmed(["plc-1"]);

        registry.ReconcileArmed(Array.Empty<string>());

        registry.TryGet("plc-1", out var c1).ShouldBeTrue();
        c1.IsArmed.ShouldBeFalse();
    }

    [Fact]
    public void DisarmAll_DisarmsEveryCapture()
    {
        var registry = new TagCaptureRegistry();
        registry.GetOrCreate("plc-1", Map((100, 16)));
        registry.GetOrCreate("plc-2", Map((100, 16)));
        registry.ReconcileArmed(["plc-1", "plc-2"]);

        registry.DisarmAll();

        registry.TryGet("plc-1", out var c1).ShouldBeTrue();
        registry.TryGet("plc-2", out var c2).ShouldBeTrue();
        c1.IsArmed.ShouldBeFalse();
        c2.IsArmed.ShouldBeFalse();
    }

    [Fact]
    public void UnknownPlc_Operations_AreSafeNoOps()
    {
        var registry = new TagCaptureRegistry();

        Should.NotThrow(() => registry.ReconcileArmed(["ghost"]));
        Should.NotThrow(() => registry.Remove("ghost"));
        registry.TryGet("ghost", out _).ShouldBeFalse();
    }

    [Fact]
    public void Remove_DropsTheCapture()
    {
        var registry = new TagCaptureRegistry();
        registry.GetOrCreate("plc-1", Map((100, 16)));
        registry.TryGet("plc-1", out _).ShouldBeTrue();

        registry.Remove("plc-1");

        registry.TryGet("plc-1", out _).ShouldBeFalse();
    }
}
