using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;

[Trait("Category", "Unit")]
public sealed class OpcUaClientHistoryTests
{
    /// <summary>Verifies MapAggregateToNodeId returns standard Part 13 aggregate for every enum.</summary>
    /// <param name="agg">The history aggregate type to test.</param>
    [Theory]
    [InlineData(HistoryAggregateType.Average)]
    [InlineData(HistoryAggregateType.Minimum)]
    [InlineData(HistoryAggregateType.Maximum)]
    [InlineData(HistoryAggregateType.Total)]
    [InlineData(HistoryAggregateType.Count)]
    public void MapAggregateToNodeId_returns_standard_Part13_aggregate_for_every_enum(HistoryAggregateType agg)
    {
        var nodeId = OpcUaClientDriver.MapAggregateToNodeId(agg);
        NodeId.IsNull(nodeId).ShouldBeFalse();
        // Every mapping should resolve to an AggregateFunction_* NodeId (namespace 0, numeric id).
        nodeId.NamespaceIndex.ShouldBe((ushort)0);
    }

    /// <summary>Verifies MapAggregateToNodeId rejects invalid enum values.</summary>
    [Fact]
    public void MapAggregateToNodeId_rejects_invalid_enum_value()
    {
        // Defense-in-depth: a future HistoryAggregateType addition mustn't silently fall through.
        Should.Throw<ArgumentOutOfRangeException>(() =>
            OpcUaClientDriver.MapAggregateToNodeId((HistoryAggregateType)99));
    }

    /// <summary>Verifies ReadRawAsync throws without initialization.</summary>
    [Fact]
    public async Task ReadRawAsync_without_initialize_throws_InvalidOperationException()
    {
        using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-hist-uninit");
        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await drv.ReadRawAsync("ns=2;s=Counter",
                DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow, 1000,
                TestContext.Current.CancellationToken));
    }

    /// <summary>Verifies ReadRawAsync with malformed NodeId returns empty result.</summary>
    [Fact]
    public async Task ReadRawAsync_with_malformed_NodeId_returns_empty_result_not_throw()
    {
        // Same defensive pattern as ReadAsync / WriteAsync — malformed NodeId short-circuits
        // to an empty result rather than crashing a batch history call. Needs init via the
        // throw path first, then we pass "" to trigger the parse-fail branch inside
        // ExecuteHistoryReadAsync. The init itself fails against 127.0.0.1:1 so we stop there.
        // Not runnable without init — keep as placeholder for when the in-process fixture
        // PR lands.
        await Task.CompletedTask;
    }

    /// <summary>Verifies ReadProcessedAsync throws without initialization.</summary>
    [Fact]
    public async Task ReadProcessedAsync_without_initialize_throws_InvalidOperationException()
    {
        using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-hist-uninit");
        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await drv.ReadProcessedAsync("ns=2;s=Counter",
                DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow,
                TimeSpan.FromSeconds(10), HistoryAggregateType.Average,
                TestContext.Current.CancellationToken));
    }

    /// <summary>Verifies ReadAtTimeAsync throws without initialization.</summary>
    [Fact]
    public async Task ReadAtTimeAsync_without_initialize_throws_InvalidOperationException()
    {
        using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-hist-uninit");
        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await drv.ReadAtTimeAsync("ns=2;s=Counter",
                [DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow],
                TestContext.Current.CancellationToken));
    }

    /// <summary>ReadEventsAsync requires an initialized session like the sibling history reads.</summary>
    [Fact]
    public async Task ReadEventsAsync_without_initialize_throws_InvalidOperationException()
    {
        using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-events-uninit");
        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await drv.ReadEventsAsync(
                sourceName: null,
                startUtc: DateTime.UtcNow.AddMinutes(-5),
                endUtc: DateTime.UtcNow,
                maxEvents: 100,
                cancellationToken: TestContext.Current.CancellationToken));
    }

    /// <summary>BuildBaseEventFilter emits the six canonical BaseEventType select clauses in order.</summary>
    [Fact]
    public void BuildBaseEventFilter_has_six_canonical_BaseEventType_value_clauses()
    {
        var filter = OpcUaClientDriver.BuildBaseEventFilter();

        filter.SelectClauses.Count.ShouldBe(6);
        string[] expected = ["EventId", "SourceName", "Time", "ReceiveTime", "Message", "Severity"];
        for (var i = 0; i < expected.Length; i++)
        {
            var clause = filter.SelectClauses[i];
            clause.TypeDefinitionId.ShouldBe(ObjectTypeIds.BaseEventType);
            clause.AttributeId.ShouldBe(Attributes.Value);
            clause.BrowsePath.Count.ShouldBe(1);
            clause.BrowsePath[0].Name.ShouldBe(expected[i]);
            clause.BrowsePath[0].NamespaceIndex.ShouldBe((ushort)0); // BaseEventType fields live in ns 0
        }
    }

    /// <summary>MapHistoryEvents maps every field by its canonical select-clause index.</summary>
    [Fact]
    public void MapHistoryEvents_maps_all_six_fields_by_canonical_index()
    {
        var eventTime = new DateTime(2026, 6, 18, 10, 0, 0, DateTimeKind.Utc);
        var recvTime = eventTime.AddSeconds(1);
        var he = new HistoryEvent();
        var fields = new HistoryEventFieldList();
        fields.EventFields.Add(new Variant(new byte[] { 1, 2, 3 }));            // 0 EventId
        fields.EventFields.Add(new Variant("Pump17"));                          // 1 SourceName
        fields.EventFields.Add(new Variant(eventTime));                         // 2 Time
        fields.EventFields.Add(new Variant(recvTime));                          // 3 ReceiveTime
        fields.EventFields.Add(new Variant(new LocalizedText("High temp")));    // 4 Message
        fields.EventFields.Add(new Variant((ushort)700));                       // 5 Severity
        he.Events.Add(fields);

        var mapped = OpcUaClientDriver.MapHistoryEvents(he);

        mapped.Count.ShouldBe(1);
        var e = mapped[0];
        e.EventId.ShouldBe(Convert.ToBase64String(new byte[] { 1, 2, 3 }));
        e.SourceName.ShouldBe("Pump17");
        e.EventTimeUtc.ShouldBe(eventTime);
        e.ReceivedTimeUtc.ShouldBe(recvTime);
        e.Message.ShouldBe("High temp");
        e.Severity.ShouldBe((ushort)700);
    }

    /// <summary>MapHistoryEvents returns empty for a HistoryEvent with no rows.</summary>
    [Fact]
    public void MapHistoryEvents_with_no_events_returns_empty()
    {
        OpcUaClientDriver.MapHistoryEvents(new HistoryEvent()).ShouldBeEmpty();
    }

    /// <summary>MapHistoryEvents tolerates a field list shorter than six without throwing.</summary>
    [Fact]
    public void MapHistoryEvents_tolerates_short_field_list_without_throwing()
    {
        var he = new HistoryEvent();
        var fields = new HistoryEventFieldList();
        fields.EventFields.Add(new Variant(new byte[] { 9 })); // only EventId present
        he.Events.Add(fields);

        var mapped = OpcUaClientDriver.MapHistoryEvents(he);

        mapped.Count.ShouldBe(1);
        mapped[0].EventId.ShouldBe(Convert.ToBase64String(new byte[] { 9 }));
        mapped[0].SourceName.ShouldBeNull();
        mapped[0].EventTimeUtc.ShouldBe(DateTime.MinValue);
        mapped[0].EventTimeUtc.Kind.ShouldBe(DateTimeKind.Utc); // sentinel is UTC-kinded, not Unspecified
        mapped[0].Severity.ShouldBe((ushort)0);
    }
}
