using Google.Protobuf.WellKnownTypes; using Microsoft.Extensions.Logging.Abstractions; using Xunit; using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests; public sealed class GatewayHistorianDataSourceTests { [Fact] public async Task ReadRaw_maps_samples_and_passes_args() { var fake = new FakeHistorianGatewayClient { RawSamples = new[] { new HistorianSample { Tag = "T", NumericValue = 1.0, OpcQuality = 192, Timestamp = Ts(2026, 1, 1, 0, 0, 0) }, new HistorianSample { Tag = "T", NumericValue = 2.0, OpcQuality = 0, Timestamp = Ts(2026, 1, 1, 0, 0, 1) }, }, }; var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); var r = await ds.ReadRawAsync("T", DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow, 100, TestContext.Current.CancellationToken); Assert.Equal(2, r.Samples.Count); Assert.Equal(0x80000000u, r.Samples[1].StatusCode); // Bad from quality 0 Assert.Equal("T", fake.LastReadRawTag); Assert.Equal(100, fake.LastReadRawMaxValues); } [Fact] public async Task ReadProcessed_uses_aggregate_mode_mapping() { var fake = new FakeHistorianGatewayClient(); var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); await ds.ReadProcessedAsync("T", default, default, TimeSpan.FromSeconds(60), HistoryAggregateType.Minimum, TestContext.Current.CancellationToken); Assert.Equal(RetrievalMode.MinimumWithTime, fake.LastAggregateMode); Assert.Equal(TimeSpan.FromSeconds(60), fake.LastAggregateInterval); } [Fact] public async Task ReadAtTime_aligns_one_snapshot_per_timestamp_with_gaps_Bad() { var fake = new FakeHistorianGatewayClient(); var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); var t1 = t0.AddSeconds(1); fake.AtTimeSamples = new[] { new HistorianSample { NumericValue = 5.0, OpcQuality = 192, Timestamp = Timestamp.FromDateTime(t0) } }; var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); var r = await ds.ReadAtTimeAsync("T", new[] { t0, t1 }, TestContext.Current.CancellationToken); Assert.Equal(2, r.Samples.Count); // exactly one per requested ts, in order Assert.Equal(5.0, r.Samples[0].Value); Assert.Equal(0x80000000u, r.Samples[1].StatusCode); // gap → Bad at requested ts } [Fact] public async Task Empty_window_is_not_a_fault() { var fake = new FakeHistorianGatewayClient { RawSamples = Array.Empty() }; var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); var r = await ds.ReadRawAsync("T", default, default, 10, TestContext.Current.CancellationToken); Assert.Empty(r.Samples); // GoodNoData-empty, no throw } [Fact] public async Task Disposing_data_source_disposes_client() { var fake = new FakeHistorianGatewayClient(); var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); await ds.DisposeAsync(); Assert.Equal(1, fake.DisposeCallCount); } // Ts(...) builds a Google.Protobuf.WellKnownTypes.Timestamp from UTC parts. private static Timestamp Ts(int y, int mo, int d, int h, int mi, int s) => Timestamp.FromDateTime(new DateTime(y, mo, d, h, mi, s, DateTimeKind.Utc)); }