using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
///
/// Tests for — the bookkeeping the EventPump
/// uses to fan one OnDataChange event out to every driver subscription that
/// observes the changed item handle.
///
public sealed class SubscriptionRegistryTests
{
[Fact]
public void NextSubscriptionId_IsMonotonic()
{
var registry = new SubscriptionRegistryAccess();
registry.NextSubscriptionId().ShouldBe(1);
registry.NextSubscriptionId().ShouldBe(2);
registry.NextSubscriptionId().ShouldBe(3);
}
[Fact]
public void Register_OneSubscription_OneTag_ResolvesSingleSubscriber()
{
var registry = new SubscriptionRegistryAccess();
registry.Register(42, [new TagBindingAccess("Tank.Level", 100)]);
var subs = registry.ResolveSubscribers(100);
subs.Count.ShouldBe(1);
subs[0].SubscriptionId.ShouldBe(42);
subs[0].FullReference.ShouldBe("Tank.Level");
}
[Fact]
public void Register_TwoSubscriptions_SameTag_FanOutToBoth()
{
var registry = new SubscriptionRegistryAccess();
registry.Register(1, [new TagBindingAccess("Tank.Level", 100)]);
registry.Register(2, [new TagBindingAccess("Tank.Level", 100)]);
var subs = registry.ResolveSubscribers(100);
subs.Count.ShouldBe(2);
subs.Select(s => s.SubscriptionId).OrderBy(x => x).ShouldBe(new[] { 1L, 2L });
}
[Fact]
public void Register_FailedItemHandle_NotIndexedForFanOut()
{
var registry = new SubscriptionRegistryAccess();
registry.Register(1, [
new TagBindingAccess("Good", 100),
new TagBindingAccess("Bad", 0), // gw rejected this tag
]);
registry.ResolveSubscribers(100).Count.ShouldBe(1);
registry.ResolveSubscribers(0).ShouldBeEmpty();
}
[Fact]
public void Remove_DropsAllBindings_AndReturnsThemForUnsubscribe()
{
var registry = new SubscriptionRegistryAccess();
registry.Register(1, [
new TagBindingAccess("A", 100),
new TagBindingAccess("B", 200),
]);
var removed = registry.Remove(1);
removed.ShouldNotBeNull();
removed!.Count.ShouldBe(2);
registry.ResolveSubscribers(100).ShouldBeEmpty();
registry.ResolveSubscribers(200).ShouldBeEmpty();
}
[Fact]
public void Remove_OneOfTwoSubscriptions_LeavesOtherIntact()
{
var registry = new SubscriptionRegistryAccess();
registry.Register(1, [new TagBindingAccess("A", 100)]);
registry.Register(2, [new TagBindingAccess("A", 100)]);
registry.Remove(1);
var subs = registry.ResolveSubscribers(100);
subs.Count.ShouldBe(1);
subs[0].SubscriptionId.ShouldBe(2);
}
[Fact]
public void Remove_UnknownSubscription_IsNullSentinel()
{
var registry = new SubscriptionRegistryAccess();
registry.Remove(999).ShouldBeNull();
}
[Fact]
public void TrackedCounts_ReflectAdditionsAndRemovals()
{
var registry = new SubscriptionRegistryAccess();
registry.TrackedSubscriptionCount.ShouldBe(0);
registry.Register(1, [new TagBindingAccess("A", 100)]);
registry.Register(2, [new TagBindingAccess("A", 100), new TagBindingAccess("B", 200)]);
registry.TrackedSubscriptionCount.ShouldBe(2);
registry.TrackedItemHandleCount.ShouldBe(2);
registry.Remove(1);
registry.TrackedSubscriptionCount.ShouldBe(1);
registry.TrackedItemHandleCount.ShouldBe(2); // sub 2 still observes both handles
registry.Remove(2);
registry.TrackedSubscriptionCount.ShouldBe(0);
registry.TrackedItemHandleCount.ShouldBe(0);
}
// Internal types are accessed via friend assembly (InternalsVisibleTo); these
// wrapper aliases keep the test code readable.
private sealed class SubscriptionRegistryAccess
{
private readonly SubscriptionRegistry _inner = new();
public int TrackedSubscriptionCount => _inner.TrackedSubscriptionCount;
public int TrackedItemHandleCount => _inner.TrackedItemHandleCount;
public long NextSubscriptionId() => _inner.NextSubscriptionId();
public void Register(long id, IReadOnlyList bindings)
=> _inner.Register(id, [.. bindings.Select(b => new TagBinding(b.FullReference, b.ItemHandle))]);
public IReadOnlyList? Remove(long id)
{
var removed = _inner.Remove(id);
return removed is null ? null : [.. removed.Select(b => new TagBindingAccess(b.FullReference, b.ItemHandle))];
}
public IReadOnlyList<(long SubscriptionId, string FullReference)> ResolveSubscribers(int handle)
=> _inner.ResolveSubscribers(handle);
}
private sealed record TagBindingAccess(string FullReference, int ItemHandle);
}