using Opc.Ua;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
///
/// #821 / #809 facts, through the BOOTED server's runtime AggregateManager — the calculators a live
/// AggregateFilter monitored item gets, under the server's own default configuration. Both are decided (owner,
/// 2026-11-11): live PercentGood / DurationGood count Good time only (#809), and live Total / TimeAverage integrate only
/// the covered part of an interval the data starts inside, even with an Uncertain first sample (#821) — equal to
/// HistoryRead. The HistoryRead (Part 13) answers, the untouched baseline's and SDK 2.0.1's stock answers for the same
/// input are pinned beside it in SdkAggregateParityTests.
///
public sealed class LiveAggregateLateStartFactsTests
{
private static readonly DateTime T0 = new(2026, 11, 11, 8, 0, 0, DateTimeKind.Utc);
/// The #810 QualityMix tag QP: Uncertain 10 at +5, Good 11 at +12, Uncertain 12 at +25, nothing after.
private static readonly (double Seconds, double Value, uint Status)[] Qp =
[
(5, 10, 0x40000000u), (12, 11, 0x00000000u), (25, 12, 0x40000000u), // Uncertain, Good, Uncertain
];
[Fact]
public async Task Live_AggregateFilter_calculators_on_an_Uncertain_first_late_start_interval()
{
await using var rig = await ServerEventRig.StartAsync("AggregateLateStart", encrypted: false,
TestContext.Current.CancellationToken);
var manager = rig.Server.CurrentInstance.AggregateManager;
var config = manager.GetDefaultConfiguration(NodeId.Null);
config.TreatUncertainAsBad.ShouldBeFalse("the server pins TreatUncertainAsBad = false (#782)");
double One(NodeId aggregate)
{
var calc = manager.CreateCalculator(aggregate, T0, T0.AddSeconds(20), 20_000, false, config);
calc.ShouldNotBeNull();
foreach (var (s, v, st) in Qp)
calc.QueueRawValue(new DataValue(Variant.From(v), st, T0.AddSeconds(s), T0.AddSeconds(s)));
calc.TryGetProcessedValue(true, out var dv).ShouldBeTrue();
dv.WrappedValue.TryGetValue(out double d).ShouldBeTrue();
return d;
}
// #821: Part 13 (HistoryRead on SqlServer/QuestDB): Total = 73.5 + 88 + 32/13 = 163.96…, TimeAverage = Total ÷ 15 s
// — live as in HistoryRead (before #821 the live calculator integrated the uncovered [0,5) as a ramp from 0 (+25)
// and divided by the whole 20 s: 188.96… / 9.448…).
const double part13Total = 73.5 + 88 + 32d / 13;
One(ObjectIds.AggregateFunction_Total).ShouldBe(part13Total, 1e-9); // 163.96…
One(ObjectIds.AggregateFunction_TimeAverage).ShouldBe(part13Total / 15, 1e-9); // 10.93…
// #809: the Uncertain [5,12) is not Good — only [12,20) is, live as in HistoryRead (8 000 ms / 40 %; before #809
// the live calculator counted the Uncertain time too: 15 000 ms / 75 %).
One(ObjectIds.AggregateFunction_DurationGood).ShouldBe(8_000d, 1e-9);
One(ObjectIds.AggregateFunction_PercentGood).ShouldBe(40d, 1e-9);
// DurationBad / PercentBad agree with HistoryRead: the 5 s before the data is Bad.
One(ObjectIds.AggregateFunction_DurationBad).ShouldBe(5_000d, 1e-9);
One(ObjectIds.AggregateFunction_PercentBad).ShouldBe(25d, 1e-9);
}
}