using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;

/// <summary>
///     Tests for <see cref="SubscriptionRegistry.TryResolveItemHandle"/> — the forward
///     fullRef → live item-handle lookup the Galaxy writer uses to skip a redundant
///     AddItem round-trip when an already-subscribed tag is written.
/// </summary>
public sealed class SubscriptionRegistryHandleResolveTests
{
    /// <summary>
    ///     Verifies that after registering a binding, TryResolveItemHandle returns the correct handle.
    /// </summary>
    [Fact]
    public void Register_ThenResolve_ReturnsHandle()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.A", 5)]);

        registry.TryResolveItemHandle("Tag.A").ShouldBe(5);
    }

    /// <summary>
    ///     Verifies that TryResolveItemHandle is case-insensitive on the full reference.
    /// </summary>
    [Fact]
    public void Register_ThenResolve_IsCaseInsensitive()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.A", 5)]);

        registry.TryResolveItemHandle("tag.a").ShouldBe(5);
    }

    /// <summary>
    ///     Verifies that a full reference that was never registered resolves to null.
    /// </summary>
    [Fact]
    public void NeverRegistered_ReturnsNull()
    {
        var registry = new SubscriptionRegistry();

        registry.TryResolveItemHandle("Tag.NotHere").ShouldBeNull();
    }

    /// <summary>
    ///     Verifies that after Remove(), the forward lookup returns null.
    /// </summary>
    [Fact]
    public void Remove_ThenResolve_ReturnsNull()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.A", 5)]);

        registry.Remove(1);

        registry.TryResolveItemHandle("Tag.A").ShouldBeNull();
    }

    /// <summary>
    ///     Verifies that after Rebind() the forward lookup returns the new handle, not the old one.
    /// </summary>
    [Fact]
    public void Rebind_ThenResolve_ReturnsNewHandle()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.A", 5)]);

        registry.Rebind(1, [new TagBinding("Tag.A", 99)]);

        registry.TryResolveItemHandle("Tag.A").ShouldBe(99);
    }

    /// <summary>
    ///     Verifies that a binding with ItemHandle <= 0 (gateway-rejected) is not resolvable.
    /// </summary>
    [Fact]
    public void FailedBinding_ZeroHandle_IsNotResolvable()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.Failed", 0)]);

        registry.TryResolveItemHandle("Tag.Failed").ShouldBeNull();
    }

    /// <summary>
    ///     Verifies that a binding with a negative ItemHandle is not resolvable.
    /// </summary>
    [Fact]
    public void FailedBinding_NegativeHandle_IsNotResolvable()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.Failed", -1)]);

        registry.TryResolveItemHandle("Tag.Failed").ShouldBeNull();
    }

    /// <summary>
    ///     Verifies the liveness guard: after the only subscriber of a handle is removed,
    ///     the forward lookup returns null even if a stale forward-map entry lingers.
    /// </summary>
    [Fact]
    public void Remove_OnlySubscriber_LivenessGuard_ReturnsNull()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.A", 5)]);

        registry.Remove(1);

        // After removal the subscriber set for handle 5 is gone, so TryResolveItemHandle
        // must return null regardless of whether the forward entry was cleaned up.
        registry.TryResolveItemHandle("Tag.A").ShouldBeNull();
    }

    /// <summary>
    ///     A tag may legitimately appear in multiple driver subscriptions (separate OPC UA monitored
    ///     items on the same Galaxy attribute) — they share one gw item handle. Removing ONE of them
    ///     must keep the tag resolvable while another subscription still binds it, so the writer keeps
    ///     borrowing instead of falling back to AddItem.
    /// </summary>
    [Fact]
    public void Remove_OneOfTwoSubscribersForSameRef_StillResolves()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.A", 5)]);
        registry.Register(2, [new TagBinding("Tag.A", 5)]);

        registry.Remove(1);

        // sub2 still binds Tag.A -> 5, so the borrow must still be offered.
        registry.TryResolveItemHandle("Tag.A").ShouldBe(5);
    }

    /// <summary>
    ///     Authoritative resolution: if two DIFFERENT refs ever map to the same numeric handle and the
    ///     subscription for one is removed, that ref must NOT resolve to the handle that now belongs to
    ///     the other ref — a wrong-tag write would be the worst outcome. Resolution confirms a live
    ///     subscription genuinely binds fullRef -> handle, not just that the handle is alive.
    /// </summary>
    [Fact]
    public void Remove_CrossRefSameHandle_DoesNotResolveToTheOtherRefsHandle()
    {
        var registry = new SubscriptionRegistry();
        registry.Register(1, [new TagBinding("Tag.A", 5)]);
        registry.Register(2, [new TagBinding("Tag.B", 5)]);

        registry.Remove(1);

        // Tag.A's subscription is gone; handle 5 is still alive but now only Tag.B binds it.
        registry.TryResolveItemHandle("Tag.A").ShouldBeNull();
        registry.TryResolveItemHandle("Tag.B").ShouldBe(5);
    }
}
