using System.Diagnostics;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
///
/// Review 07 P7 (High), wire level: one driver serving a live ab_server device AND a dead device (a
/// blackholed address, so every connect waits out the full timeout). Before the fix the dead device's 20 tags
/// cost 20 x Timeout per poll and the live device was read once per cycle; now the live device reads Good in
/// the same call, the dead device costs at most two timeouts, and the next poll does not touch it at all.
/// Read-only.
///
[Collection(AbLegacyServerCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Simulator", "ab_server-PCCC")]
public sealed class AbLegacyDeadDeviceIsolationTests(AbLegacyServerFixture sim)
{
// TEST-NET-1 (RFC 5737): never routed, so a connect blackholes until the driver's timeout.
private const string DeadUri = "ab://192.0.2.1:44818/1,0";
/// The live device is served promptly while a blackholed sibling is throttled.
/// The simulator profile.
/// The transport stack the driver runs on.
[AbLegacyTheory]
[MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))]
public async Task Dead_device_does_not_stall_the_live_one(AbLegacyServerProfile profile, AbLegacyTransport transport)
{
if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip);
var liveUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}";
var timeout = TimeSpan.FromSeconds(2);
var dead = Enumerable.Range(0, 20)
.Select(i => new AbLegacyTagDefinition($"Dead{i}", DeadUri, $"N7:{i}", AbLegacyDataType.Int))
.ToArray();
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions(liveUri, profile.Family), new AbLegacyDeviceOptions(DeadUri, profile.Family)],
RawTags = AbLegacyRawTags.Entries(
[new AbLegacyTagDefinition("Live", liveUri, "N7:0", AbLegacyDataType.Int), .. dead]),
Timeout = timeout,
Probe = new AbLegacyProbeOptions { Enabled = false },
Transport = transport,
}, driverInstanceId: $"ablegacy-p7-{profile.Family}-{transport}");
var ct = TestContext.Current.CancellationToken;
await drv.InitializeAsync("{}", ct);
string[] refs = ["Live", .. dead.Select(d => d.Name)];
// Two consecutive comms faults mark the device down. With coalescing on, the dead device's 20 tags are ONE
// block (one fault per poll), so the window can open on the second poll rather than the first — either way
// at most two timeouts are paid before the device stops being dialled.
var total = TimeSpan.Zero;
TimeSpan last = default;
for (var poll = 0; poll < 3; poll++)
{
var sw = Stopwatch.StartNew();
var r = await drv.ReadAsync(refs, ct);
sw.Stop();
total += last = sw.Elapsed;
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good, $"poll {poll}: the live device must be served in the same poll");
r.Skip(1).ShouldAllBe(s => s.StatusCode != AbLegacyStatusMapper.Good);
}
total.ShouldBeLessThan(timeout * 2.5, "a dead device may cost at most two timeouts per backoff window");
last.ShouldBeLessThan(timeout, "inside the backoff window the dead device is not dialled");
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
}
}