using System; using System.Linq; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests { [Trait("Category", "Unit")] public sealed class HistorianClusterEndpointPickerTests { private static HistorianConfiguration Config(params string[] nodes) => new() { ServerName = "ignored", ServerNames = nodes.ToList(), FailureCooldownSeconds = 60, }; [Fact] public void Single_node_config_falls_back_to_ServerName_when_ServerNames_empty() { var cfg = new HistorianConfiguration { ServerName = "only-node", ServerNames = new() }; var p = new HistorianClusterEndpointPicker(cfg); p.NodeCount.ShouldBe(1); p.GetHealthyNodes().ShouldBe(new[] { "only-node" }); } [Fact] public void Failed_node_enters_cooldown_and_is_skipped() { var now = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc); var p = new HistorianClusterEndpointPicker(Config("a", "b"), () => now); p.MarkFailed("a", "boom"); p.GetHealthyNodes().ShouldBe(new[] { "b" }); } [Fact] public void Cooldown_expires_after_configured_window() { var clock = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc); var p = new HistorianClusterEndpointPicker(Config("a", "b"), () => clock); p.MarkFailed("a", "boom"); p.GetHealthyNodes().ShouldBe(new[] { "b" }); clock = clock.AddSeconds(61); p.GetHealthyNodes().ShouldBe(new[] { "a", "b" }); } [Fact] public void MarkHealthy_immediately_clears_cooldown() { var now = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc); var p = new HistorianClusterEndpointPicker(Config("a"), () => now); p.MarkFailed("a", "boom"); p.GetHealthyNodes().ShouldBeEmpty(); p.MarkHealthy("a"); p.GetHealthyNodes().ShouldBe(new[] { "a" }); } [Fact] public void All_nodes_in_cooldown_returns_empty_healthy_list() { var now = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc); var p = new HistorianClusterEndpointPicker(Config("a", "b"), () => now); p.MarkFailed("a", "x"); p.MarkFailed("b", "y"); p.GetHealthyNodes().ShouldBeEmpty(); p.NodeCount.ShouldBe(2); } [Fact] public void Snapshot_reports_failure_count_and_last_error() { var now = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc); var p = new HistorianClusterEndpointPicker(Config("a"), () => now); p.MarkFailed("a", "first"); p.MarkFailed("a", "second"); var snap = p.SnapshotNodeStates().Single(); snap.FailureCount.ShouldBe(2); snap.LastError.ShouldBe("second"); snap.IsHealthy.ShouldBeFalse(); snap.CooldownUntil.ShouldNotBeNull(); } [Fact] public void Duplicate_hostnames_are_deduplicated_case_insensitively() { var p = new HistorianClusterEndpointPicker(Config("NodeA", "nodea", "NodeB")); p.NodeCount.ShouldBe(2); } } }