using Akka.Actor;
using Akka.TestKit.Xunit2;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;

/// <summary>
/// WP2.1b — the alarm subscriber first-segment bucket index must make EXACTLY the routing
/// decisions the previous linear scan made. The expectation in each case is computed with
/// the original rule (plain <c>StartsWith</c> against every subscribed source), so the
/// test pins equivalence rather than restating the new implementation.
/// </summary>
public class DataConnectionActorAlarmIndexTests : TestKit
{
    private readonly ISiteHealthCollector _health = Substitute.For<ISiteHealthCollector>();
    private readonly IDataConnectionFactory _factory = Substitute.For<IDataConnectionFactory>();
    private readonly DataConnectionOptions _options = new()
    {
        ReconnectInterval = TimeSpan.FromMilliseconds(100),
        TagResolutionRetryInterval = TimeSpan.FromMilliseconds(200),
        WriteTimeout = TimeSpan.FromSeconds(5)
    };

    // Sources chosen to exercise every index path:
    //  - "Area1"            → NO separator: the residue list (can match other buckets)
    //  - "Area1.Tank"       → bucket "Area1", shared prefix with the next one
    //  - "Area1.Tank1.Sub"  → bucket "Area1", deeper
    //  - "Area2.Tank"       → a different bucket
    private static readonly string[] Sources = ["Area1", "Area1.Tank", "Area1.Tank1.Sub", "Area2.Tank"];

    private static readonly (string SourceRef, string SourceObjectRef)[] Transitions =
    [
        ("Area1.Tank1.Hi", "Area1.Tank1"),   // Area1 (residue) + Area1.Tank
        ("Area1.Tank1.Sub.Hi", "Area1.Tank1.Sub"), // + Area1.Tank1.Sub
        ("Area1X.Pump.Hi", "Area1X.Pump"),   // sub-segment prefix: Area1 ONLY
        ("Area2.Tank9.Hi", "Area2.Tank9"),   // Area2.Tank only
        ("Zone.A.Hi", "Zone.A"),             // nobody
    ];

    private static NativeAlarmTransition Transition(string sourceRef, string sourceObj) =>
        new(sourceRef, sourceObj, "AnalogLimit.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 500),
            "Process", "hi", "hi", "", "", null, DateTimeOffset.UtcNow, "92", "90");

    /// <summary>The ORIGINAL linear-scan rule, kept here as the oracle.</summary>
    private static bool LinearScanMatches(string sourceRef, string transitionSourceRef, string transitionSourceObjectRef) =>
        transitionSourceObjectRef.StartsWith(sourceRef, StringComparison.Ordinal)
        || transitionSourceRef.StartsWith(sourceRef, StringComparison.Ordinal);

    [Fact]
    public void BucketIndex_RoutesExactlyLikeTheLinearScan()
    {
        AlarmTransitionCallback? cb = null;
        var adapter = Substitute.For<IDataConnection, IAlarmSubscribableConnection>();
        adapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        ((IAlarmSubscribableConnection)adapter)
            .SubscribeAlarmsAsync(Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Do<AlarmTransitionCallback>(c => cb = c), Arg.Any<CancellationToken>())
            .Returns(ci => Task.FromResult("alarm-" + ci.ArgAt<string>(0)));

        var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor(
            "conn", adapter, _options, _health, _factory, "OpcUa")), "alarm-index");

        // One probe per source so the routing decision per source is observable.
        var probes = Sources.ToDictionary(s => s, _ => CreateTestProbe());
        foreach (var source in Sources)
        {
            actor.Tell(new SubscribeAlarmsRequest("c", "inst-" + source, "conn", source, null, DateTimeOffset.UtcNow),
                probes[source].Ref);
            probes[source].ExpectMsg<SubscribeAlarmsResponse>(m => m.Success, TimeSpan.FromSeconds(5));
        }

        Assert.NotNull(cb);

        foreach (var (sourceRef, sourceObjectRef) in Transitions)
        {
            cb!(Transition(sourceRef, sourceObjectRef));

            foreach (var source in Sources)
            {
                if (LinearScanMatches(source, sourceRef, sourceObjectRef))
                {
                    probes[source].ExpectMsg<NativeAlarmTransitionUpdate>(
                        u => u.Transition.SourceReference == sourceRef, TimeSpan.FromSeconds(5));
                }
            }
        }

        // Nothing extra was delivered to anybody.
        foreach (var source in Sources)
            probes[source].ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    [Fact]
    public void SnapshotComplete_StillBroadcastsToEverySubscriber_BypassingTheIndex()
    {
        // The framing sentinel carries an EMPTY source reference, so it matches no bucket.
        // It must still reach every alarm subscriber, or statically-active conditions
        // delivered only in the snapshot would buffer forever.
        AlarmTransitionCallback? cb = null;
        var adapter = Substitute.For<IDataConnection, IAlarmSubscribableConnection>();
        adapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        ((IAlarmSubscribableConnection)adapter)
            .SubscribeAlarmsAsync(Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Do<AlarmTransitionCallback>(c => cb = c), Arg.Any<CancellationToken>())
            .Returns(ci => Task.FromResult("alarm-" + ci.ArgAt<string>(0)));

        var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor(
            "conn", adapter, _options, _health, _factory, "OpcUa")), "alarm-index-snapshot");

        var probes = Sources.ToDictionary(s => s, _ => CreateTestProbe());
        foreach (var source in Sources)
        {
            actor.Tell(new SubscribeAlarmsRequest("c", "inst-" + source, "conn", source, null, DateTimeOffset.UtcNow),
                probes[source].Ref);
            probes[source].ExpectMsg<SubscribeAlarmsResponse>(m => m.Success, TimeSpan.FromSeconds(5));
        }

        Assert.NotNull(cb);
        cb!(new NativeAlarmTransition(
            "", "", "", AlarmTransitionKind.SnapshotComplete,
            new AlarmConditionState(false, true, null, AlarmShelveState.Unshelved, false, 0),
            "", "", "", "", "", null, DateTimeOffset.UtcNow, "", ""));

        foreach (var source in Sources)
        {
            probes[source].ExpectMsg<NativeAlarmTransitionUpdate>(
                u => u.Transition.Kind == AlarmTransitionKind.SnapshotComplete, TimeSpan.FromSeconds(5));
        }
    }

    [Fact]
    public void UnsubscribedSource_IsDroppedFromTheIndex()
    {
        AlarmTransitionCallback? cb = null;
        var adapter = Substitute.For<IDataConnection, IAlarmSubscribableConnection>();
        adapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        ((IAlarmSubscribableConnection)adapter)
            .SubscribeAlarmsAsync(Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Do<AlarmTransitionCallback>(c => cb = c), Arg.Any<CancellationToken>())
            .Returns(ci => Task.FromResult("alarm-" + ci.ArgAt<string>(0)));

        var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor(
            "conn", adapter, _options, _health, _factory, "OpcUa")), "alarm-index-unsub");

        var probe = CreateTestProbe();
        actor.Tell(new SubscribeAlarmsRequest("c", "inst", "conn", "Area1.Tank", null, DateTimeOffset.UtcNow), probe.Ref);
        probe.ExpectMsg<SubscribeAlarmsResponse>(m => m.Success, TimeSpan.FromSeconds(5));

        actor.Tell(new UnsubscribeAlarmsRequest("c2", "inst", "conn", "Area1.Tank", DateTimeOffset.UtcNow), probe.Ref);
        Thread.Sleep(200);

        Assert.NotNull(cb);
        cb!(Transition("Area1.Tank1.Hi", "Area1.Tank1"));
        probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }
}
