using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using static ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests.Fixture; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests; /// Direct tests of the AtTime rules, with the literals of the SqlServer AtTimeReadTests. public sealed class AtTimeEvaluatorTests { private static readonly DateTime Now = At(3600); [Fact] public void AtTime_inside_an_outage_returns_the_bad_neighbours_code_with_no_value() { var t0 = new DateTime(2026, 10, 9, 8, 0, 0, DateTimeKind.Utc); var prev = new StoredSample(t0, HistorianValue.Empty, 0x80310000u); var next = new StoredSample(t0.AddSeconds(10), HistorianValue.FromDouble(5), 0); var r = AtTimeEvaluator.Evaluate(t0.AddSeconds(5), prev, next, t0, t0.AddHours(1)); r.StatusCode.ShouldBe(0x80310000u); r.Value.ShouldBeNull(); } [Fact] public void A_bad_valued_previous_neighbour_keeps_its_code_and_drops_its_value() { var r = AtTimeEvaluator.Evaluate(At(25), A[2], A[3], Now, Now); r.Value.ShouldBeNull(); // NOT 35 and NOT 30 r.StatusCode.ShouldBe(0x80000000u); r.SourceTimestampUtc.ShouldBe(At(25)); } [Fact] public void Exact_hit_returns_the_stored_value_and_status_unchanged() { var r = AtTimeEvaluator.Evaluate(At(20), A[2], A[3], At(21), Now); r.Value.ShouldBe(30d); r.StatusCode.ShouldBe(0x80000000u); r.ServerTimestampUtc.ShouldBe(At(21)); } [Fact] public void No_previous_sample_is_BadNoData_with_no_value() { var r = AtTimeEvaluator.Evaluate(At(0).AddTicks(-1), null, A[0], Now, Now); r.Value.ShouldBeNull(); r.StatusCode.ShouldBe(0x809B0000u); r.SourceTimestampUtc.ShouldBe(At(0).AddTicks(-1)); } [Fact] public void A_bad_next_neighbour_holds_the_previous_value_uncertain() { var r = AtTimeEvaluator.Evaluate(At(15), A[1], A[2], Now, Now); r.Value.ShouldBe(20d); // NOT 25 r.StatusCode.ShouldBe(UncertainInterpolated); } [Fact] public void Between_two_good_doubles_is_linearly_interpolated_good() { AtTimeEvaluator.Evaluate(At(5), A[0], A[1], Now, Now).Value.ShouldBe(15d); AtTimeEvaluator.Evaluate(At(2.5), A[0], A[1], Now, Now).Value.ShouldBe(12.5d); var r = AtTimeEvaluator.Evaluate(At(32.5), A[3], A[4], Now, Now); r.Value.ShouldBe(42.5d); r.StatusCode.ShouldBe(GoodInterpolated); r.SourceTimestampUtc.ShouldBe(At(32.5)); } [Fact] public void An_uncertain_neighbour_makes_the_interpolation_uncertain() { var r = AtTimeEvaluator.Evaluate(At(5), S(0, HistorianValue.FromDouble(10), Uncertain), S(10, HistorianValue.FromDouble(20), Good), Now, Now); r.Value.ShouldBe(15d); r.StatusCode.ShouldBe(UncertainInterpolated); } [Fact] public void Int64_is_interpolated_and_rounded_away_from_zero_keeping_its_type() { var p = S(0, HistorianValue.FromInt64(10), Good); var n = S(10, HistorianValue.FromInt64(15), Good); AtTimeEvaluator.Evaluate(At(5), p, n, Now, Now).Value.ShouldBe(13L); // 12.5 ⇒ 13 AtTimeEvaluator.Evaluate(At(2), p, n, Now, Now).Value.ShouldBe(11L); } [Fact] public void After_the_last_sample_the_value_is_stepped_good_until_now_then_uncertain() { var now = At(42); var held = AtTimeEvaluator.Evaluate(At(41), A[4], null, now, now); held.Value.ShouldBe(50d); held.StatusCode.ShouldBe(GoodInterpolated); var future = AtTimeEvaluator.Evaluate(At(45), A[4], null, now, now); future.Value.ShouldBe(50d); future.StatusCode.ShouldBe(UncertainInterpolated); } [Fact] public void A_non_numeric_kind_is_stepped_with_the_previous_status_plus_interpolated() { var p = S(0, HistorianValue.FromString("a"), Uncertain); var n = S(10, HistorianValue.FromString("b"), Good); var r = AtTimeEvaluator.Evaluate(At(5), p, n, Now, Now); r.Value.ShouldBe("a"); r.StatusCode.ShouldBe(Uncertain | 0x402u); } [Fact] public void An_uncertain_next_neighbour_makes_the_interpolation_uncertain() { var r = AtTimeEvaluator.Evaluate(At(5), S(0, HistorianValue.FromDouble(10), Good), S(10, HistorianValue.FromDouble(20), Uncertain), Now, Now); r.Value.ShouldBe(15d); r.StatusCode.ShouldBe(UncertainInterpolated); } }