using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Alarms;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests;

[Trait("Category", "Unit")]
public sealed class GalaxyAlarmTrackerTests
{
    private sealed class FakeSubscriber
    {
        public readonly ConcurrentDictionary<string, Action<string, Vtq>> Subs = new();
        public readonly ConcurrentQueue<string> Unsubs = new();
        public readonly ConcurrentQueue<(string Tag, object Value)> Writes = new();
        public bool WriteReturns { get; set; } = true;

        public Task Subscribe(string tag, Action<string, Vtq> cb)
        {
            Subs[tag] = cb;
            return Task.CompletedTask;
        }
        public Task Unsubscribe(string tag)
        {
            Unsubs.Enqueue(tag);
            Subs.TryRemove(tag, out _);
            return Task.CompletedTask;
        }
        public Task<bool> Write(string tag, object value)
        {
            Writes.Enqueue((tag, value));
            return Task.FromResult(WriteReturns);
        }
    }

    private static Vtq Bool(bool v) => new(v, DateTime.UtcNow, 192);
    private static Vtq Int(int v) => new(v, DateTime.UtcNow, 192);
    private static Vtq Str(string v) => new(v, DateTime.UtcNow, 192);

    [Fact]
    public async Task Track_subscribes_to_four_alarm_attributes()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);

        await t.TrackAsync("Tank.Level.HiHi");

        fake.Subs.ShouldContainKey("Tank.Level.HiHi.InAlarm");
        fake.Subs.ShouldContainKey("Tank.Level.HiHi.Priority");
        fake.Subs.ShouldContainKey("Tank.Level.HiHi.DescAttrName");
        fake.Subs.ShouldContainKey("Tank.Level.HiHi.Acked");
        t.TrackedAlarmCount.ShouldBe(1);
    }

    [Fact]
    public async Task Track_is_idempotent_on_repeat_call()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);

        await t.TrackAsync("Alarm.A");
        await t.TrackAsync("Alarm.A");

        t.TrackedAlarmCount.ShouldBe(1);
        fake.Subs.Count.ShouldBe(4); // 4 sub calls, not 8
    }

    [Fact]
    public async Task InAlarm_false_to_true_fires_Active_transition()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
        var transitions = new ConcurrentQueue<AlarmTransition>();
        t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);

        await t.TrackAsync("Alarm.A");
        fake.Subs["Alarm.A.Priority"]("Alarm.A.Priority", Int(500));
        fake.Subs["Alarm.A.DescAttrName"]("Alarm.A.DescAttrName", Str("TankLevelHiHi"));
        fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true));

        transitions.Count.ShouldBe(1);
        transitions.TryDequeue(out var tr).ShouldBeTrue();
        tr!.Transition.ShouldBe(AlarmStateTransition.Active);
        tr.Priority.ShouldBe(500);
        tr.DescAttrName.ShouldBe("TankLevelHiHi");
    }

    [Fact]
    public async Task InAlarm_true_to_false_fires_Inactive_transition()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
        var transitions = new ConcurrentQueue<AlarmTransition>();
        t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);

        await t.TrackAsync("Alarm.A");
        fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true));
        fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(false));

        transitions.Count.ShouldBe(2);
        transitions.TryDequeue(out _);
        transitions.TryDequeue(out var tr).ShouldBeTrue();
        tr!.Transition.ShouldBe(AlarmStateTransition.Inactive);
    }

    [Fact]
    public async Task Acked_false_to_true_fires_Acknowledged_while_InAlarm_is_true()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
        var transitions = new ConcurrentQueue<AlarmTransition>();
        t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);

        await t.TrackAsync("Alarm.A");
        fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true));  // Active, clears Acked flag
        fake.Subs["Alarm.A.Acked"]("Alarm.A.Acked", Bool(true));      // Acknowledged

        transitions.Count.ShouldBe(2);
        transitions.TryDequeue(out _);
        transitions.TryDequeue(out var tr).ShouldBeTrue();
        tr!.Transition.ShouldBe(AlarmStateTransition.Acknowledged);
    }

    [Fact]
    public async Task Acked_transition_while_InAlarm_is_false_does_not_fire()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
        var transitions = new ConcurrentQueue<AlarmTransition>();
        t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);

        await t.TrackAsync("Alarm.A");
        // Initial Acked=true on subscribe (alarm is at rest, pre-ack'd) — should not fire.
        fake.Subs["Alarm.A.Acked"]("Alarm.A.Acked", Bool(true));

        transitions.Count.ShouldBe(0);
    }

    [Fact]
    public async Task Acknowledge_writes_AckMsg_with_comment()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
        await t.TrackAsync("Alarm.A");

        var ok = await t.AcknowledgeAsync("Alarm.A", "acknowledged by operator");

        ok.ShouldBeTrue();
        fake.Writes.Count.ShouldBe(1);
        fake.Writes.TryDequeue(out var w).ShouldBeTrue();
        w.Tag.ShouldBe("Alarm.A.AckMsg");
        w.Value.ShouldBe("acknowledged by operator");
    }

    [Fact]
    public async Task Snapshot_reports_latest_fields()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
        await t.TrackAsync("Alarm.A");
        fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true));
        fake.Subs["Alarm.A.Priority"]("Alarm.A.Priority", Int(900));
        fake.Subs["Alarm.A.DescAttrName"]("Alarm.A.DescAttrName", Str("MyAlarm"));
        fake.Subs["Alarm.A.Acked"]("Alarm.A.Acked", Bool(true));

        var snap = t.SnapshotStates();
        snap.Count.ShouldBe(1);
        snap[0].InAlarm.ShouldBeTrue();
        snap[0].Acked.ShouldBeTrue();
        snap[0].Priority.ShouldBe(900);
        snap[0].DescAttrName.ShouldBe("MyAlarm");
    }

    [Fact]
    public async Task Foreign_probe_callback_is_dropped()
    {
        var fake = new FakeSubscriber();
        using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
        var transitions = new ConcurrentQueue<AlarmTransition>();
        t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);

        // No TrackAsync was called — this callback is foreign and should be silently ignored.
        t.OnProbeCallback("Unknown.InAlarm", Bool(true));

        transitions.Count.ShouldBe(0);
    }
}
