using Akka.Actor;
using Akka.TestKit.Xunit2;
using NSubstitute;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Communication.Actors;

namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Actors;

/// <summary>
/// T1A.3: the site communication actor delegates each of the seven site→central sends to the
/// injected <see cref="ICentralTransport"/>, preserving the current <c>Sender</c> as the reply
/// target — and a transport failure surfaces to that sender exactly as the S&amp;F / audit / health
/// layers already expect, while a heartbeat transport fault never faults the actor.
/// </summary>
public class SiteCommunicationActorTransportTests : TestKit
{
    private readonly CommunicationOptions _options = new();

    private (IActorRef actor, ICentralTransport transport) NewActor()
    {
        var transport = Substitute.For<ICentralTransport>();
        var dmProbe = CreateTestProbe();
        var actor = Sys.ActorOf(Props.Create(() =>
            new SiteCommunicationActor("site1", _options, dmProbe.Ref, () => false, null, transport)));
        return (actor, transport);
    }

    [Fact]
    public void NotificationSubmit_DelegatesToTransport_WithSenderAsReplyTo()
    {
        var (actor, transport) = NewActor();
        var submit = new NotificationSubmit(
            "notif-1", "Operators", "Subj", "Body", "site1", "inst1", "alarmScript", DateTimeOffset.UtcNow);

        actor.Tell(submit, TestActor);

        AwaitAssert(() => transport.Received(1).SubmitNotification(
            Arg.Is<NotificationSubmit>(m => m.NotificationId == "notif-1"), Arg.Is(TestActor)));
    }

    [Fact]
    public void NotificationStatusQuery_DelegatesToTransport_WithSenderAsReplyTo()
    {
        var (actor, transport) = NewActor();

        actor.Tell(new NotificationStatusQuery("corr-1", "notif-1"), TestActor);

        AwaitAssert(() => transport.Received(1).QueryNotificationStatus(
            Arg.Is<NotificationStatusQuery>(m => m.NotificationId == "notif-1"), Arg.Is(TestActor)));
    }

    [Fact]
    public void IngestAuditEvents_DelegatesToTransport_WithSenderAsReplyTo()
    {
        var (actor, transport) = NewActor();

        actor.Tell(new IngestAuditEventsCommand(new List<AuditEvent>()), TestActor);

        AwaitAssert(() => transport.Received(1).IngestAuditEvents(Arg.Any<IngestAuditEventsCommand>(), Arg.Is(TestActor)));
    }

    [Fact]
    public void IngestCachedTelemetry_DelegatesToTransport_WithSenderAsReplyTo()
    {
        var (actor, transport) = NewActor();

        actor.Tell(new IngestCachedTelemetryCommand(new List<CachedTelemetryEntry>()), TestActor);

        AwaitAssert(() => transport.Received(1).IngestCachedTelemetry(Arg.Any<IngestCachedTelemetryCommand>(), Arg.Is(TestActor)));
    }

    [Fact]
    public void ReconcileSite_DelegatesToTransport_WithSenderAsReplyTo()
    {
        var (actor, transport) = NewActor();

        actor.Tell(
            new ReconcileSiteRequest("site1", "node-a", new Dictionary<string, string>()), TestActor);

        AwaitAssert(() => transport.Received(1).ReconcileSite(
            Arg.Is<ReconcileSiteRequest>(m => m.NodeId == "node-a"), Arg.Is(TestActor)));
    }

    [Fact]
    public void ReportSiteHealth_DelegatesToTransport_WithSenderAsReplyTo()
    {
        var (actor, transport) = NewActor();
        var report = MinimalHealthReport(sequence: 7);

        actor.Tell(report, TestActor);

        AwaitAssert(() => transport.Received(1).ReportSiteHealth(
            Arg.Is<SiteHealthReport>(m => m.SequenceNumber == 7), Arg.Is(TestActor)));
    }

    [Fact]
    public void TransportFailureReply_RoutesBackToTheWaitingSender()
    {
        // The seam preserves the reply-routing the S&F layer depends on: when the transport
        // answers the captured replyTo with a Status.Failure (its transient-failure signal on a
        // non-OK status), that failure reaches the original sender — here the test actor — so the
        // waiting Ask faults, exactly as it did on the removed ClusterClient path.
        var transport = Substitute.For<ICentralTransport>();
        transport
            .When(t => t.SubmitNotification(Arg.Any<NotificationSubmit>(), Arg.Any<IActorRef>()))
            .Do(ci => ci.Arg<IActorRef>().Tell(
                new Status.Failure(new InvalidOperationException("central unavailable"))));

        var dmProbe = CreateTestProbe();
        var actor = Sys.ActorOf(Props.Create(() =>
            new SiteCommunicationActor("site1", _options, dmProbe.Ref, () => false, null, transport)));

        actor.Tell(new NotificationSubmit(
            "notif-x", "Operators", "S", "B", "site1", null, null, DateTimeOffset.UtcNow), TestActor);

        var failure = ExpectMsg<Status.Failure>();
        Assert.IsType<InvalidOperationException>(failure.Cause);
    }

    [Fact]
    public void HeartbeatTransportThrow_DoesNotFaultTheActor()
    {
        // A transport whose SendHeartbeat throws must not fault the actor — heartbeats are
        // fire-and-forget and their failure is swallowed. Prove the actor still serves messages
        // after a heartbeat that threw.
        var transport = Substitute.For<ICentralTransport>();
        transport
            .When(t => t.SendHeartbeat(Arg.Any<HeartbeatMessage>(), Arg.Any<IActorRef>()))
            .Do(_ => throw new InvalidOperationException("boom"));

        var dmProbe = CreateTestProbe();
        var actor = Sys.ActorOf(Props.Create(() =>
            new SiteCommunicationActor(
                "site1",
                new CommunicationOptions { ApplicationHeartbeatInterval = TimeSpan.FromMilliseconds(50) },
                dmProbe.Ref, () => true, null, transport)));

        // Let the heartbeat timer fire a few times (each throws inside the transport).
        Thread.Sleep(250);

        // The actor is still alive and delegating: a subsequent send is handled normally.
        actor.Tell(new NotificationSubmit(
            "after-heartbeat", "Operators", "S", "B", "site1", null, null, DateTimeOffset.UtcNow), TestActor);
        AwaitAssert(() => transport.Received(1).SubmitNotification(
            Arg.Is<NotificationSubmit>(m => m.NotificationId == "after-heartbeat"), Arg.Is(TestActor)));
    }

    private static SiteHealthReport MinimalHealthReport(long sequence) => new(
        SiteId: "site1",
        SequenceNumber: sequence,
        ReportTimestamp: DateTimeOffset.UtcNow,
        DataConnectionStatuses: new Dictionary<string, ConnectionHealth>(),
        TagResolutionCounts: new Dictionary<string, TagResolutionStatus>(),
        ScriptErrorCount: 0,
        AlarmEvaluationErrorCount: 0,
        StoreAndForwardBufferDepths: new Dictionary<string, int>(),
        DeadLetterCount: 0,
        DeployedInstanceCount: 0,
        EnabledInstanceCount: 0,
        DisabledInstanceCount: 0);
}
