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

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

[Trait("Category", "Unit")]
public sealed class GenericDriverNodeManagerTests
{
    /// <summary>
    ///     BuildAddressSpaceAsync walks the driver's discovery through the caller's builder. Every
    ///     variable marked with MarkAsAlarmCondition captures its sink in the node manager; later,
    ///     IAlarmSource.OnAlarmEvent payloads are routed by SourceNodeId to the matching sink.
    ///     This is the plumbing that PR 16's concrete OPC UA builder will use to update the actual
    ///     AlarmConditionState nodes.
    /// </summary>
    [Fact]
    public async Task Alarm_events_are_routed_to_the_sink_registered_for_the_matching_source_node_id()
    {
        var driver = new FakeDriver();
        var builder = new RecordingBuilder();
        using var nm = new GenericDriverNodeManager(driver);

        await nm.BuildAddressSpaceAsync(builder, CancellationToken.None);

        builder.Alarms.Count.ShouldBe(2);
        nm.TrackedAlarmSources.Count.ShouldBe(2);

        // Simulate the driver raising a transition for one of the alarms.
        var args = new AlarmEventArgs(
            SubscriptionHandle: new FakeHandle("s1"),
            SourceNodeId: "Tank.HiHi",
            ConditionId: "cond-1",
            AlarmType: "Tank.HiHi",
            Message: "Level exceeded",
            Severity: AlarmSeverity.High,
            SourceTimestampUtc: DateTime.UtcNow);
        driver.RaiseAlarm(args);

        builder.Alarms["Tank.HiHi"].Received.Count.ShouldBe(1);
        builder.Alarms["Tank.HiHi"].Received[0].Message.ShouldBe("Level exceeded");
        // The other alarm sink never received a payload — fan-out is tag-scoped.
        builder.Alarms["Heater.OverTemp"].Received.Count.ShouldBe(0);
    }

    [Fact]
    public async Task Non_alarm_variables_do_not_register_sinks()
    {
        var driver = new FakeDriver();
        var builder = new RecordingBuilder();
        using var nm = new GenericDriverNodeManager(driver);

        await nm.BuildAddressSpaceAsync(builder, CancellationToken.None);

        // FakeDriver registers 2 alarm-bearing variables + 1 plain variable.
        nm.TrackedAlarmSources.ShouldNotContain("Tank.Level"); // the plain one
    }

    [Fact]
    public async Task Unknown_source_node_id_is_dropped_silently()
    {
        var driver = new FakeDriver();
        var builder = new RecordingBuilder();
        using var nm = new GenericDriverNodeManager(driver);
        await nm.BuildAddressSpaceAsync(builder, CancellationToken.None);

        driver.RaiseAlarm(new AlarmEventArgs(
            new FakeHandle("s1"), "Unknown.Source", "c", "t", "m", AlarmSeverity.Low, DateTime.UtcNow));

        builder.Alarms.Values.All(s => s.Received.Count == 0).ShouldBeTrue();
    }

    [Fact]
    public async Task Dispose_unsubscribes_from_OnAlarmEvent()
    {
        var driver = new FakeDriver();
        var builder = new RecordingBuilder();
        var nm = new GenericDriverNodeManager(driver);
        await nm.BuildAddressSpaceAsync(builder, CancellationToken.None);

        nm.Dispose();

        driver.RaiseAlarm(new AlarmEventArgs(
            new FakeHandle("s1"), "Tank.HiHi", "c", "t", "m", AlarmSeverity.Low, DateTime.UtcNow));

        // No sink should have received it — the forwarder was detached.
        builder.Alarms["Tank.HiHi"].Received.Count.ShouldBe(0);
    }

    // --- test doubles ---

    private sealed class FakeDriver : IDriver, ITagDiscovery, IAlarmSource
    {
        public string DriverInstanceId => "fake";
        public string DriverType => "Fake";
        public event EventHandler<AlarmEventArgs>? OnAlarmEvent;

        public Task InitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
        public Task ReinitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
        public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
        public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
        public long GetMemoryFootprint() => 0;
        public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;

        public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
        {
            var folder = builder.Folder("Tank", "Tank");
            var lvl = folder.Variable("Level", "Level", new DriverAttributeInfo(
                "Tank.Level", DriverDataType.Float64, false, null, SecurityClassification.FreeAccess, false, IsAlarm: false));
            var hiHi = folder.Variable("HiHi", "HiHi", new DriverAttributeInfo(
                "Tank.HiHi", DriverDataType.Boolean, false, null, SecurityClassification.FreeAccess, false, IsAlarm: true));
            hiHi.MarkAsAlarmCondition(new AlarmConditionInfo("Tank.HiHi", AlarmSeverity.High, "High-high alarm"));

            var heater = builder.Folder("Heater", "Heater");
            var ot = heater.Variable("OverTemp", "OverTemp", new DriverAttributeInfo(
                "Heater.OverTemp", DriverDataType.Boolean, false, null, SecurityClassification.FreeAccess, false, IsAlarm: true));
            ot.MarkAsAlarmCondition(new AlarmConditionInfo("Heater.OverTemp", AlarmSeverity.Critical, "Over-temperature"));
            return Task.CompletedTask;
        }

        public void RaiseAlarm(AlarmEventArgs args) => OnAlarmEvent?.Invoke(this, args);

        public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(IReadOnlyList<string> _, CancellationToken __)
            => Task.FromResult<IAlarmSubscriptionHandle>(new FakeHandle("sub"));
        public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle _, CancellationToken __) => Task.CompletedTask;
        public Task AcknowledgeAsync(IReadOnlyList<AlarmAcknowledgeRequest> _, CancellationToken __) => Task.CompletedTask;
    }

    private sealed class FakeHandle(string diagnosticId) : IAlarmSubscriptionHandle
    {
        public string DiagnosticId { get; } = diagnosticId;
    }

    private sealed class RecordingBuilder : IAddressSpaceBuilder
    {
        public Dictionary<string, RecordingSink> Alarms { get; } = new(StringComparer.OrdinalIgnoreCase);

        public IAddressSpaceBuilder Folder(string _, string __) => this;

        public IVariableHandle Variable(string _, string __, DriverAttributeInfo info)
            => new Handle(info.FullName, Alarms);

        public void AddProperty(string _, DriverDataType __, object? ___) { }

        public sealed class Handle(string fullRef, Dictionary<string, RecordingSink> alarms) : IVariableHandle
        {
            public string FullReference { get; } = fullRef;
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo _)
            {
                var sink = new RecordingSink();
                alarms[FullReference] = sink;
                return sink;
            }
        }

        public sealed class RecordingSink : IAlarmConditionSink
        {
            public List<AlarmEventArgs> Received { get; } = new();
            public void OnTransition(AlarmEventArgs args) => Received.Add(args);
        }
    }
}
