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

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

/// <summary>
///     End-to-end smoke against a live <c>opc-plc</c> (task #215). Drives the real
///     OPC UA Secure Channel + Session + MonitoredItem exchange — no mocks. Every
///     test here proves a capability surface that loopback against our own server
///     couldn't exercise cleanly: real cert negotiation, real endpoint descriptions,
///     real simulated nodes that change without a write.
/// </summary>
[Collection(OpcPlcCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Simulator", "opc-plc")]
public sealed class OpcUaClientSmokeTests(OpcPlcFixture sim)
{
    /// <summary>Verifies that the client can connect and read a node through the real OPC UA stack.</summary>
    [Fact]
    public async Task Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack()
    {
        if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);

        var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
        await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-read");
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var snapshots = await drv.ReadAsync(
            [OpcPlcProfile.StepUpRef], TestContext.Current.CancellationToken);

        snapshots.Count.ShouldBe(1);
        snapshots[0].StatusCode.ShouldBe(0u, "opc-plc StepUp read must succeed end-to-end");
        snapshots[0].Value.ShouldNotBeNull("StepUp always has a current value");
    }

    /// <summary>Verifies that the client can read a batch of varied types from the simulator.</summary>
    [Fact]
    public async Task Client_reads_batch_of_varied_types_from_live_simulator()
    {
        if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);

        var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
        await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-batch");
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var snapshots = await drv.ReadAsync(
            [OpcPlcProfile.StepUpRef, OpcPlcProfile.RandomSignedInt32Ref, OpcPlcProfile.AlternatingBooleanRef],
            TestContext.Current.CancellationToken);

        snapshots.Count.ShouldBe(3);
        foreach (var s in snapshots)
        {
            s.StatusCode.ShouldBe(0u);
            s.Value.ShouldNotBeNull();
        }
        // AlternatingBoolean should decode as a bool specifically — catches a common
        // attribute-mapping regression where the driver stringifies variant values.
        snapshots[2].Value.ShouldBeOfType<bool>();
    }

    /// <summary>Verifies that the client can subscribe to data changes from the live server.</summary>
    [Fact]
    public async Task Client_subscribe_receives_StepUp_data_changes_from_live_server()
    {
        if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);

        var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
        await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-sub");
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var observed = new List<DataChangeEventArgs>();
        var gate = new SemaphoreSlim(0);
        drv.OnDataChange += (_, e) =>
        {
            lock (observed) observed.Add(e);
            gate.Release();
        };

        var handle = await drv.SubscribeAsync(
            [OpcPlcProfile.FastUInt1Ref], TimeSpan.FromMilliseconds(250),
            TestContext.Current.CancellationToken);

        // FastUInt1 ticks every 100 ms — one publishing interval (250 ms) should deliver.
        // Wait up to 3 s to tolerate container warm-up + first-publish delay.
        var got = await gate.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
        got.ShouldBeTrue("opc-plc FastUInt1 must publish at least one data change within 3s");

        int observedCount;
        lock (observed) observedCount = observed.Count;
        observedCount.ShouldBeGreaterThan(0);

        await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
    }

    /// <summary>
    ///     Verifies HistoryReadEvents passthrough issues a well-formed request and returns a
    ///     result without throwing. opc-plc exposes live alarm conditions (--alm) but is NOT a
    ///     historian, so the upstream may return zero historical events or reject the service —
    ///     either way the driver must produce a HistoricalEventsResult, never throw. This proves
    ///     the wire request + unwrap path; a non-empty event list is infra-gated on an upstream
    ///     that historizes events.
    /// </summary>
    [Fact]
    public async Task Client_reads_events_returns_result_without_throwing()
    {
        if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);

        var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
        await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-events");
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var result = await drv.ReadEventsAsync(
            sourceName: null,                       // null → upstream Server object (i=2253)
            startUtc: DateTime.UtcNow.AddHours(-1),
            endUtc: DateTime.UtcNow,
            maxEvents: 100,
            cancellationToken: TestContext.Current.CancellationToken);

        result.ShouldNotBeNull();
        result.Events.ShouldNotBeNull();
    }
}
