using Microsoft.Extensions.Configuration;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
///
/// lmxopcua#777: DriverWrites:Timeout is the one value every write bound derives from, and it is checked
/// against the resilience tiers' Write timeouts at startup.
///
public sealed class DriverWriteBoundsBindingTests
{
private static IConfiguration Config(Dictionary values) =>
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
/// The shipped default is consistent with every tier — no tier's Write policy is capped below itself.
[Fact]
public void An_absent_key_yields_the_default_which_fits_every_tier()
{
var bounds = DriverWriteBoundsBinding.Bind(Config(new()), out var warnings);
warnings.ShouldBeEmpty();
bounds.ShouldBe(DriverWriteBounds.Default);
foreach (var tier in Enum.GetValues())
{
TimeSpan.FromSeconds(DriverResilienceOptions.GetTierDefaults(tier)[DriverCapability.Write].TimeoutSeconds)
.ShouldBeLessThanOrEqualTo(bounds.Timeout, $"tier {tier}");
}
}
[Fact]
public void The_configured_timeout_binds_and_every_bound_derives_from_it()
{
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "00:00:12" }), out var warnings);
warnings.ShouldBeEmpty();
bounds.Timeout.ShouldBe(TimeSpan.FromSeconds(12));
bounds.HostAsk.ShouldBe(TimeSpan.FromSeconds(12 + 2 + 12 + 2 + 1));
bounds.GatewayAsk.ShouldBe(bounds.HostAsk + DriverWriteBounds.ReplyMargin);
}
///
/// An inconsistent config is caught at startup: a write timeout below a tier's Write timeout means that tier's
/// policy never takes effect, and each such tier is named. Tier A (2 s) fits under 3 s and is not.
///
[Fact]
public void A_timeout_below_a_tiers_Write_timeout_is_reported_per_tier()
{
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "00:00:03" }), out var warnings);
bounds.Timeout.ShouldBe(TimeSpan.FromSeconds(3), "an inconsistency is reported, not overridden");
warnings.Count.ShouldBe(2);
warnings.ShouldContain(w => w.StartsWith("Tier B ", StringComparison.Ordinal) && w.Contains("(4s)"));
warnings.ShouldContain(w => w.StartsWith("Tier C ", StringComparison.Ordinal) && w.Contains("(10s)"));
warnings.ShouldNotContain(w => w.StartsWith("Tier A ", StringComparison.Ordinal));
}
[Theory]
[InlineData("not-a-timespan")]
[InlineData("00:00:00")]
[InlineData("-00:00:05")]
public void An_unusable_value_falls_back_to_the_default_with_a_warning(string raw)
{
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = raw }), out var warnings);
bounds.ShouldBe(DriverWriteBounds.Default);
warnings.Count.ShouldBe(1);
warnings[0].ShouldContain("DriverWrites:Timeout");
}
[Fact]
public void A_value_above_the_max_is_clamped_with_a_warning()
{
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "01:00:00" }), out var warnings);
bounds.Timeout.ShouldBe(DriverWriteBounds.MaxTimeout);
warnings.Count.ShouldBe(1);
warnings[0].ShouldContain("clamped");
}
}