using System.Reflection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.Historian;
///
/// Structural contract tests for the historian data-source surface added in PR 1.1.
/// Asserts the type shape — implementations are tested in their own projects.
///
public sealed class IHistorianDataSourceContractTests
{
[Fact]
public void Interface_LivesInRootNamespace()
{
typeof(IHistorianDataSource).Namespace
.ShouldBe("ZB.MOM.WW.OtOpcUa.Core.Abstractions");
}
[Fact]
public void Interface_IsPublic()
{
typeof(IHistorianDataSource).IsPublic.ShouldBeTrue();
typeof(IHistorianDataSource).IsInterface.ShouldBeTrue();
}
[Fact]
public void Interface_ExtendsIDisposable()
{
typeof(IDisposable).IsAssignableFrom(typeof(IHistorianDataSource))
.ShouldBeTrue("data sources own backend connections; the server disposes them on shutdown");
}
[Theory]
[InlineData("ReadRawAsync", typeof(Task))]
[InlineData("ReadProcessedAsync", typeof(Task))]
[InlineData("ReadAtTimeAsync", typeof(Task))]
[InlineData("ReadEventsAsync", typeof(Task))]
public void ReadMethods_ReturnExpectedTaskShape(string methodName, Type expectedReturnType)
{
var method = typeof(IHistorianDataSource).GetMethod(methodName);
method.ShouldNotBeNull();
method!.ReturnType.ShouldBe(expectedReturnType);
}
[Fact]
public void GetHealthSnapshot_IsSynchronous()
{
var method = typeof(IHistorianDataSource).GetMethod("GetHealthSnapshot");
method.ShouldNotBeNull();
method!.ReturnType.ShouldBe(typeof(HistorianHealthSnapshot));
}
[Fact]
public void HealthSnapshot_AcceptsEmptyClusterNodeList()
{
var snapshot = new HistorianHealthSnapshot(
TotalQueries: 0,
TotalSuccesses: 0,
TotalFailures: 0,
ConsecutiveFailures: 0,
LastSuccessTime: null,
LastFailureTime: null,
LastError: null,
ProcessConnectionOpen: false,
EventConnectionOpen: false,
ActiveProcessNode: null,
ActiveEventNode: null,
Nodes: Array.Empty());
snapshot.Nodes.ShouldBeEmpty();
}
[Fact]
public void HealthSnapshot_PreservesClusterNodes()
{
var node = new HistorianClusterNodeState(
Name: "hist-01",
IsHealthy: true,
CooldownUntil: null,
FailureCount: 0,
LastError: null,
LastFailureTime: null);
var snapshot = new HistorianHealthSnapshot(
TotalQueries: 5,
TotalSuccesses: 5,
TotalFailures: 0,
ConsecutiveFailures: 0,
LastSuccessTime: new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Utc),
LastFailureTime: null,
LastError: null,
ProcessConnectionOpen: true,
EventConnectionOpen: true,
ActiveProcessNode: "hist-01",
ActiveEventNode: "hist-01",
Nodes: new[] { node });
snapshot.Nodes.Count.ShouldBe(1);
snapshot.Nodes[0].ShouldBe(node);
}
[Fact]
public void ClusterNodeState_RecordEqualityByValue()
{
var a = new HistorianClusterNodeState("hist-01", true, null, 0, null, null);
var b = new HistorianClusterNodeState("hist-01", true, null, 0, null, null);
a.ShouldBe(b);
}
[Fact]
public void ClusterNodeState_DistinctByAnyField()
{
var healthy = new HistorianClusterNodeState("hist-01", true, null, 0, null, null);
var unhealthy = new HistorianClusterNodeState("hist-01", false, null, 1, "boom", null);
healthy.ShouldNotBe(unhealthy);
}
}