using Moq;
using Opc.Ua;
using Opc.Ua.Client;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
///
/// Review 2026-10-21 R06:O-8 — AcknowledgeAsync swallowed every non-cancellation transport failure, so
/// the host logged the Part 9 ack as applied while upstream still showed the condition unacknowledged. A
/// transport failure (no reply) now propagates with its status; a reply — even one carrying per-ack refusals —
/// still returns normally (that is the server answering, logged per ack).
///
[Trait("Category", "Unit")]
public sealed class OpcUaClientAcknowledgeFailureTests
{
private const string Condition = "ns=2;s=Pump17";
private static Mock NewSession()
{
var mock = new Mock(MockBehavior.Loose);
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
return mock;
}
private static AlarmAcknowledgeRequest[] Ack() => [new(Condition, Condition, "operator ack")];
/// A transport failure on the Acknowledge call reaches the caller with the transport status.
[Fact]
public async Task A_transport_failure_propagates_with_its_status()
{
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8");
var session = NewSession();
session.Setup(s => s.CallAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ThrowsAsync(new ServiceResultException(StatusCodes.BadConnectionClosed));
drv.SetSessionForTest(session.Object);
var ex = await Should.ThrowAsync(() =>
drv.AcknowledgeAsync(Ack(), TestContext.Current.CancellationToken));
ex.StatusCode.ShouldBe(StatusCodes.BadConnectionClosed);
}
/// Positive control: a reply returns normally and the call reached the wire.
[Fact]
public async Task Positive_control_a_reply_returns_normally()
{
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8-ok");
var session = NewSession();
session.Setup(s => s.CallAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new CallResponse
{
ResponseHeader = new ResponseHeader(),
Results = [new CallMethodResult { StatusCode = StatusCodes.BadConditionBranchAlreadyAcked }],
DiagnosticInfos = [],
});
drv.SetSessionForTest(session.Object);
await drv.AcknowledgeAsync(Ack(), TestContext.Current.CancellationToken);
session.Verify(s => s.CallAsync(It.IsAny(), It.IsAny(), It.IsAny()),
Times.Once);
}
/// A session lost while the ack queued on the gate fails the ack rather than reporting it applied.
[Fact]
public async Task A_session_lost_while_queued_fails_the_ack()
{
var ct = TestContext.Current.CancellationToken;
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8-lost");
drv.SetSessionForTest(NewSession().Object);
await drv.Gate.WaitAsync(ct);
var call = drv.AcknowledgeAsync(Ack(), ct);
drv.SetSessionForTest(null);
drv.Gate.Release();
await Should.ThrowAsync(() => call);
}
}