using System.Runtime.CompilerServices;
using Shouldly;
using Xunit;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Health;
///
/// Review 2026-10-21 G-4: the watcher's probe handles must live in the driver's
/// under the reserved id, follow the platform set, be
/// rebound on , and leave on stop.
///
public sealed class PerPlatformProbeWatcherRegistryTests
{
private sealed class FakeSubscriber : IGalaxySubscriber
{
private int _next;
public List Unsubscribes { get; } = [];
public Task> SubscribeBulkAsync(
IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
=> Task.FromResult>(
[.. fullReferences.Select(r => new SubscribeResult { TagAddress = r, ItemHandle = ++_next, WasSuccessful = true })]);
public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken)
{
Unsubscribes.Add([.. itemHandles]);
return Task.CompletedTask;
}
public async IAsyncEnumerable StreamEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Delay(Timeout.Infinite, cancellationToken);
yield break;
}
}
private static (long Id, string Ref)[] Resolve(SubscriptionRegistry registry, int handle) =>
[.. registry.ResolveSubscribers(handle)];
/// Synced platforms resolve through the registry under the reserved id; removed ones stop resolving.
[Fact]
public async Task Sync_registers_probe_handles_under_the_reserved_id_and_tracks_removals()
{
var registry = new SubscriptionRegistry();
using var watcher = new PerPlatformProbeWatcher(new FakeSubscriber(), new HostStatusAggregator(), registry);
await watcher.SyncPlatformsAsync(["A", "B"], CancellationToken.None);
registry.Contains(SubscriptionRegistry.ProbeSubscriptionId).ShouldBeTrue();
var bindings = watcher.CurrentBindings;
bindings.Count.ShouldBe(2);
foreach (var b in bindings)
{
Resolve(registry, b.ItemHandle).ShouldBe([(SubscriptionRegistry.ProbeSubscriptionId, b.FullReference)]);
}
var bHandle = bindings.Single(b => b.FullReference == "B.ScanState").ItemHandle;
await watcher.SyncPlatformsAsync(["A"], CancellationToken.None);
Resolve(registry, bHandle).ShouldBeEmpty("a removed platform's handle must leave the fan-out map");
var aHandle = bindings.Single(b => b.FullReference == "A.ScanState").ItemHandle;
Resolve(registry, aHandle).ShouldHaveSingleItem("positive control: the kept platform still resolves");
}
/// Resubscribe replaces the handles and rebinds: old handles stop resolving, new ones resolve.
[Fact]
public async Task Resubscribe_rebinds_the_registry_to_the_fresh_handles()
{
var registry = new SubscriptionRegistry();
using var watcher = new PerPlatformProbeWatcher(new FakeSubscriber(), new HostStatusAggregator(), registry);
await watcher.SyncPlatformsAsync(["A"], CancellationToken.None);
var oldHandle = watcher.CurrentBindings.Single().ItemHandle;
(await watcher.ResubscribeAsync(CancellationToken.None)).ShouldBe(1);
var newHandle = watcher.CurrentBindings.Single().ItemHandle;
newHandle.ShouldNotBe(oldHandle);
Resolve(registry, oldHandle).ShouldBeEmpty();
Resolve(registry, newHandle).ShouldBe([(SubscriptionRegistry.ProbeSubscriptionId, "A.ScanState")]);
registry.TrackedSubscriptionCount.ShouldBe(1);
}
/// StopAsync unsubscribes the live handles and removes the reserved entry.
[Fact]
public async Task StopAsync_unsubscribes_and_removes_the_reserved_entry()
{
var registry = new SubscriptionRegistry();
var subscriber = new FakeSubscriber();
var watcher = new PerPlatformProbeWatcher(subscriber, new HostStatusAggregator(), registry);
await watcher.SyncPlatformsAsync(["A", "B"], CancellationToken.None);
var handles = watcher.CurrentBindings.Select(b => b.ItemHandle).OrderBy(h => h).ToArray();
await watcher.StopAsync(CancellationToken.None);
subscriber.Unsubscribes.ShouldHaveSingleItem().OrderBy(h => h).ShouldBe(handles);
registry.Contains(SubscriptionRegistry.ProbeSubscriptionId).ShouldBeFalse();
registry.TrackedItemHandleCount.ShouldBe(0);
await Should.NotThrowAsync(() => watcher.StopAsync(CancellationToken.None)); // idempotent
}
/// A watcher without a registry (the decoding-table unit tests) still works and registers nothing.
[Fact]
public async Task Without_a_registry_the_watcher_keeps_its_handles_private()
{
using var watcher = new PerPlatformProbeWatcher(new FakeSubscriber(), new HostStatusAggregator());
await watcher.SyncPlatformsAsync(["A"], CancellationToken.None);
watcher.CurrentBindings.ShouldHaveSingleItem();
(await watcher.ResubscribeAsync(CancellationToken.None)).ShouldBe(1);
}
}