using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
///
/// AVEVA's native TimeAverage / Total answer Good-status wrong values until the gateway's interval fix
/// (historiangw#27) is live-verified, so they are refused BadAggregateNotSupported unless
/// ServerHistorian:Gateway:EnableNativeTimeWeighted opts back in.
///
public sealed class GatewayNativeTimeWeightedGateTests
{
private static readonly DateTime T0 = new(2026, 10, 22, 0, 0, 0, DateTimeKind.Utc);
private static CancellationToken Ct => TestContext.Current.CancellationToken;
[Theory]
[InlineData(HistoryAggregateType.TimeAverage)]
[InlineData(HistoryAggregateType.Total)]
public async Task By_default_the_native_time_weighted_pair_is_refused_unsent_and_unadvertised(HistoryAggregateType a)
{
var fake = new FakeHistorianGatewayClient();
var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance);
ds.SupportedAggregates.ShouldNotContain(a);
await Should.ThrowAsync(
() => ds.ReadProcessedAsync("T", T0, T0.AddHours(1), TimeSpan.FromMinutes(1), a, Ct));
fake.ReadAggregateCallCount.ShouldBe(0);
fake.TagExistsCallCount.ShouldBe(0);
ds.GetHealthSnapshot().TotalQueries.ShouldBe(0, "a refused aggregate is a client request, not a backend read");
// Positive control: a raw-evaluated per-sample aggregate on the same source is still served.
ds.SupportedAggregates.ShouldContain(HistoryAggregateType.Average);
var avg = await ds.ReadProcessedAsync("T", T0, T0.AddHours(1), TimeSpan.FromMinutes(30), HistoryAggregateType.Average, Ct);
avg.Samples.Count.ShouldBe(2);
fake.ReadRawCallCount.ShouldBeGreaterThan(0);
}
[Theory]
[InlineData(HistoryAggregateType.TimeAverage)]
[InlineData(HistoryAggregateType.Total)]
public async Task Opting_in_serves_and_advertises_the_native_pair(HistoryAggregateType a)
{
var fake = new FakeHistorianGatewayClient();
var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance, enableNativeTimeWeighted: true);
ds.SupportedAggregates.ShouldBe(GatewayHistorianDataSource.ServedAggregates);
await ds.ReadProcessedAsync("T", T0, T0.AddHours(1), TimeSpan.FromMinutes(1), a, Ct);
fake.ReadAggregateCallCount.ShouldBeGreaterThan(0);
}
[Fact]
public void The_default_set_is_the_ten_raw_evaluated_aggregates()
{
var ds = new GatewayHistorianDataSource(new FakeHistorianGatewayClient(), NullLogger.Instance);
ds.SupportedAggregates.Count.ShouldBe(10);
ds.SupportedAggregates.ShouldBe(GatewayHistorianDataSource.RawEvaluatedAggregates);
GatewayHistorianDataSource.ServedAggregates.Except(ds.SupportedAggregates).OrderBy(x => x)
.ShouldBe([HistoryAggregateType.Total, HistoryAggregateType.TimeAverage]);
}
[Theory]
[InlineData(null, false)]
[InlineData("false", false)]
[InlineData("true", true)]
public void The_flag_binds_from_configuration_and_reaches_the_data_source(string? configured, bool expected)
{
var values = new Dictionary
{
["ServerHistorian:Enabled"] = "true",
["ServerHistorian:Endpoint"] = "https://localhost:5222",
["ServerHistorian:ApiKey"] = "histgw_x_y",
};
if (configured is not null) values["ServerHistorian:Gateway:EnableNativeTimeWeighted"] = configured;
var opts = new ConfigurationBuilder().AddInMemoryCollection(values).Build()
.GetSection(ServerHistorianOptions.SectionName).Get()!;
using var services = new ServiceCollection().BuildServiceProvider();
var ds = Assert.IsType(GatewayHistorian.CreateDataSource(opts, services));
ds.SupportedAggregates.Contains(HistoryAggregateType.TimeAverage).ShouldBe(expected);
}
}