using System.Reflection;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Server.Redundancy;
using ConfigRedundancyMode = ZB.MOM.WW.OtOpcUa.Configuration.Enums.RedundancyMode;
namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
///
/// Unit coverage for . Uses a
/// stand-in for — the writer only needs ServerObject +
/// DefaultSystemContext, so we stub just those and let every other member return
/// null (the writer never touches anything else).
///
public sealed class ServerRedundancyNodeWriterTests
{
[Fact]
public void ApplyServiceLevel_sets_node_value_and_dedupes_unchanged()
{
var env = BuildEnv();
env.Writer.ApplyServiceLevel(200);
env.ServerObject.ServiceLevel.Value.ShouldBe((byte)200);
var timestampAfterFirst = env.ServerObject.ServiceLevel.Timestamp;
// Same value — writer should early-out without touching Timestamp.
Thread.Sleep(5);
env.Writer.ApplyServiceLevel(200);
env.ServerObject.ServiceLevel.Timestamp.ShouldBe(timestampAfterFirst);
env.Writer.ApplyServiceLevel(150);
env.ServerObject.ServiceLevel.Value.ShouldBe((byte)150);
env.ServerObject.ServiceLevel.Timestamp.ShouldBeGreaterThan(timestampAfterFirst);
}
[Fact]
public void ApplyRedundancySupport_maps_config_enum()
{
var env = BuildEnv();
env.Writer.ApplyRedundancySupport(ConfigRedundancyMode.Warm);
env.ServerObject.ServerRedundancy.RedundancySupport.Value.ShouldBe(RedundancySupport.Warm);
env.Writer.ApplyRedundancySupport(ConfigRedundancyMode.Hot);
env.ServerObject.ServerRedundancy.RedundancySupport.Value.ShouldBe(RedundancySupport.Hot);
env.Writer.ApplyRedundancySupport(ConfigRedundancyMode.None);
env.ServerObject.ServerRedundancy.RedundancySupport.Value.ShouldBe(RedundancySupport.None);
}
[Fact]
public void ApplyServerUriArray_writes_when_non_transparent_state_present()
{
var env = BuildEnv(nonTransparent: true);
env.Writer.ApplyServerUriArray(["urn:self", "urn:peer"]);
var ntr = (NonTransparentRedundancyState)env.ServerObject.ServerRedundancy;
ntr.ServerUriArray.Value.ShouldBe(new[] { "urn:self", "urn:peer" });
var ts = ntr.ServerUriArray.Timestamp;
Thread.Sleep(5);
env.Writer.ApplyServerUriArray(["urn:self", "urn:peer"]); // dedupe
ntr.ServerUriArray.Timestamp.ShouldBe(ts);
env.Writer.ApplyServerUriArray(["urn:self", "urn:peer", "urn:peer2"]);
ntr.ServerUriArray.Value.Length.ShouldBe(3);
}
[Fact]
public void ApplyServerUriArray_skips_silently_on_base_redundancy_type()
{
var env = BuildEnv(nonTransparent: false);
Should.NotThrow(() => env.Writer.ApplyServerUriArray(["urn:self"]));
env.ServerObject.ServerRedundancy.ShouldBeOfType();
}
private static Env BuildEnv(bool nonTransparent = false)
{
var serverObject = new ServerObjectState(parent: null)
{
ServiceLevel = new PropertyState(null),
};
serverObject.ServerRedundancy = nonTransparent
? new NonTransparentRedundancyState(serverObject)
{
RedundancySupport = new PropertyState(null),
ServerUriArray = new PropertyState(null),
}
: new ServerRedundancyState(serverObject)
{
RedundancySupport = new PropertyState(null),
};
var proxy = DispatchProxy.Create();
var fake = (FakeServerInternalProxy)(object)proxy;
fake.ServerObjectValue = serverObject;
fake.DefaultSystemContextValue = new ServerSystemContext(proxy);
var writer = new ServerRedundancyNodeWriter(proxy, NullLogger.Instance);
return new Env(proxy, serverObject, writer);
}
private sealed record Env(
IServerInternal Server,
ServerObjectState ServerObject,
ServerRedundancyNodeWriter Writer);
public class FakeServerInternalProxy : DispatchProxy
{
public ServerObjectState? ServerObjectValue;
public ISystemContext? DefaultSystemContextValue;
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) =>
targetMethod?.Name switch
{
"get_ServerObject" => ServerObjectValue,
"get_DefaultSystemContext" => DefaultSystemContextValue,
_ => null,
};
}
}