using System.Collections.Concurrent;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;

public sealed class DeferredAddressSpaceSinkTests
{
    /// <summary>Verifies that the default inner is a null sink so calls before SetSink are safe.</summary>
    [Fact]
    public void Default_inner_is_null_sink_so_calls_before_SetSink_are_safe()
    {
        var deferred = new DeferredAddressSpaceSink();

        // No throw, no observable side effect.
        deferred.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow);
        deferred.WriteAlarmCondition("a", Snapshot(active: true), DateTime.UtcNow);
        deferred.RebuildAddressSpace();
    }

    /// <summary>Verifies that calls after SetSink are forwarded to the inner sink.</summary>
    [Fact]
    public void Calls_after_SetSink_are_forwarded_to_the_inner()
    {
        var deferred = new DeferredAddressSpaceSink();
        var inner = new RecordingSink();
        deferred.SetSink(inner);

        deferred.WriteValue("x", 42, OpcUaQuality.Good, DateTime.UtcNow);
        deferred.WriteAlarmCondition("a-1", Snapshot(active: true), DateTime.UtcNow);
        deferred.RebuildAddressSpace();

        inner.Calls.ShouldBe(new[] { "WV:x", "WA:a-1", "RB" });
    }

    /// <summary>Verifies that setting sink to null reverts to null sink.</summary>
    [Fact]
    public void SetSink_to_null_reverts_to_null_sink()
    {
        var deferred = new DeferredAddressSpaceSink();
        var inner = new RecordingSink();
        deferred.SetSink(inner);
        deferred.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow);
        inner.Calls.Count.ShouldBe(1);

        deferred.SetSink(null);
        deferred.WriteValue("y", 2, OpcUaQuality.Good, DateTime.UtcNow); // dropped
        inner.Calls.Count.ShouldBe(1);
    }

    /// <summary>Verifies that sink can be swapped between implementations.</summary>
    [Fact]
    public void SetSink_can_swap_between_implementations()
    {
        var deferred = new DeferredAddressSpaceSink();
        var first = new RecordingSink();
        var second = new RecordingSink();

        deferred.SetSink(first);
        deferred.WriteValue("a", 1, OpcUaQuality.Good, DateTime.UtcNow);

        deferred.SetSink(second);
        deferred.WriteValue("b", 2, OpcUaQuality.Good, DateTime.UtcNow);

        first.Calls.Single().ShouldBe("WV:a");
        second.Calls.Single().ShouldBe("WV:b");
    }

    /// <summary>Builds a minimal <see cref="AlarmConditionSnapshot"/> for the forwarding tests (the
    /// inner sink only records the node id, so the exact state values don't matter here).</summary>
    private static AlarmConditionSnapshot Snapshot(bool active = false) =>
        new(active, Acknowledged: true, Confirmed: true, Enabled: true,
            Shelving: AlarmShelvingKind.Unshelved, Severity: 500, Message: "test");

    private sealed class RecordingSink : IOpcUaAddressSpaceSink
    {
        /// <summary>Gets the queue of recorded calls.</summary>
        public ConcurrentQueue<string> CallQueue { get; } = new();
        /// <summary>Gets the list of recorded calls.</summary>
        public List<string> Calls => CallQueue.ToList();

        /// <inheritdoc />
        public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
            => CallQueue.Enqueue($"WV:{nodeId}");
        /// <inheritdoc />
        public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
            => CallQueue.Enqueue($"WA:{alarmNodeId}");
        /// <inheritdoc />
        public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity)
            => CallQueue.Enqueue($"MA:{alarmNodeId}");
        /// <inheritdoc />
        public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
            => CallQueue.Enqueue($"EF:{folderNodeId}");
        /// <inheritdoc />
        public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType)
            => CallQueue.Enqueue($"EV:{variableNodeId}");
        /// <inheritdoc />
        public void RebuildAddressSpace() => CallQueue.Enqueue("RB");
    }
}
