using Akka.Actor;
using Akka.Cluster.Tools.Client;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Runtime.Communication;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Communication;

public class NodeCommunicationActorTests : RuntimeActorTestBase
{
    private static DispatchDeployment NewDispatch() => new(
        new DeploymentId(Guid.NewGuid()),
        new RevisionHash("rev-1"),
        new CorrelationId(Guid.NewGuid()));

    [Fact]
    public void DispatchDeployment_from_central_lands_on_the_node_local_event_stream()
    {
        var subscriber = CreateTestProbe();
        Sys.EventStream.Subscribe(subscriber.Ref, typeof(DispatchDeployment));
        var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null));

        var dispatch = NewDispatch();
        actor.Tell(dispatch);

        subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5)).ShouldBe(dispatch);
    }

    [Fact]
    public void A_command_is_published_exactly_once()
    {
        // The reason inbound commands do NOT go back onto their DPS topic: central SendToAll-s to
        // every node's comm actor, so a mesh-wide republish would deliver N copies to every
        // subscriber. One publish per inbound command is the whole point.
        var subscriber = CreateTestProbe();
        Sys.EventStream.Subscribe(subscriber.Ref, typeof(DispatchDeployment));
        var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null));

        actor.Tell(NewDispatch());

        subscriber.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(5));
        subscriber.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    [Fact]
    public void RestartDriver_from_central_lands_on_the_event_stream()
    {
        var subscriber = CreateTestProbe();
        Sys.EventStream.Subscribe(subscriber.Ref, typeof(RestartDriver));
        var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null));

        var msg = new RestartDriver("C1", "driver-1", "alice", Guid.NewGuid());
        actor.Tell(msg);

        subscriber.ExpectMsg<RestartDriver>(TimeSpan.FromSeconds(5)).ShouldBe(msg);
    }

    [Fact]
    public void ReconnectDriver_from_central_lands_on_the_event_stream()
    {
        var subscriber = CreateTestProbe();
        Sys.EventStream.Subscribe(subscriber.Ref, typeof(ReconnectDriver));
        var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null));

        var msg = new ReconnectDriver("C1", "driver-1", "alice", Guid.NewGuid());
        actor.Tell(msg);

        subscriber.ExpectMsg<ReconnectDriver>(TimeSpan.FromSeconds(5)).ShouldBe(msg);
    }

    [Fact]
    public void AlarmCommand_from_central_lands_on_the_event_stream()
    {
        // ScriptedAlarmHostActor is a CHILD of DriverHostActor with no registry key, so there is no
        // ref to hand this actor at wiring time — the EventStream is what makes the alarm-command
        // leg possible without a registration handshake.
        var subscriber = CreateTestProbe();
        Sys.EventStream.Subscribe(subscriber.Ref, typeof(AlarmCommand));
        var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null));

        var msg = new AlarmCommand("alarm-1", "Acknowledge", "alice", "comment", null);
        actor.Tell(msg);

        subscriber.ExpectMsg<AlarmCommand>(TimeSpan.FromSeconds(5)).ShouldBe(msg);
    }

    [Fact]
    public void ApplyAck_goes_to_central_as_a_Send_not_a_SendToAll()
    {
        // Send, not SendToAll: the deploy coordinator is one singleton behind the central comm
        // actor's proxy. SendToAll would deliver one ack per central node and the coordinator would
        // count this node twice.
        var client = CreateTestProbe();
        var actor = Sys.ActorOf(NodeCommunicationActor.Props(client.Ref));

        var ack = new ApplyAck(
            new DeploymentId(Guid.NewGuid()),
            new NodeId("host-a:4053"),
            ApplyAckOutcome.Applied,
            null,
            new CorrelationId(Guid.NewGuid()));
        actor.Tell(ack);

        var sent = client.ExpectMsg<ClusterClient.Send>(TimeSpan.FromSeconds(5));
        sent.Path.ShouldBe(MeshPaths.CentralCommunication);
        sent.Message.ShouldBe(ack);
    }

    [Fact]
    public void An_ack_with_no_central_client_is_dropped_with_a_warning()
    {
        var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null));

        var ack = new ApplyAck(
            new DeploymentId(Guid.NewGuid()),
            new NodeId("host-a:4053"),
            ApplyAckOutcome.Applied,
            null,
            new CorrelationId(Guid.NewGuid()));

        EventFilter.Warning(contains: "dropping ApplyAck").ExpectOne(
            TimeSpan.FromSeconds(5), () => actor.Tell(ack));
    }
}
