using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications;
using ZB.MOM.WW.ScadaBridge.NotificationOutbox.Delivery;
using ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests.TestSupport;

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

/// <summary>
/// Task 15: Tests for the <see cref="NotificationOutboxActor"/> query surface — the
/// paginated outbox query, single-notification status query, manual retry and discard
/// of parked notifications, and the KPI snapshot.
/// </summary>
public class NotificationOutboxActorQueryTests : TestKit
{
    private readonly INotificationOutboxRepository _repository =
        OutboxRepositorySubstitute.Healthy();

    private IServiceProvider BuildServiceProvider()
    {
        var services = new ServiceCollection();
        services.AddScoped(_ => _repository);
        return services.BuildServiceProvider();
    }

    private IActorRef CreateActor(NotificationOutboxOptions? options = null)
    {
        return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
            BuildServiceProvider(),
            // A long dispatch interval keeps the dispatch loop from interfering with these tests.
            options ?? new NotificationOutboxOptions { DispatchInterval = TimeSpan.FromHours(1) },
            new NoOpCentralAuditWriter(),
            NullLogger<NotificationOutboxActor>.Instance)));
    }

    private static Notification MakeNotification(
        NotificationStatus status = NotificationStatus.Pending,
        DateTimeOffset? createdAt = null,
        int retryCount = 0,
        string? lastError = null,
        DateTimeOffset? deliveredAt = null)
    {
        return new Notification(
            Guid.NewGuid().ToString(), NotificationType.Email, "ops-team", "Subject", "Body", "site-1")
        {
            Status = status,
            CreatedAt = createdAt ?? DateTimeOffset.UtcNow,
            RetryCount = retryCount,
            LastError = lastError,
            DeliveredAt = deliveredAt,
            SourceInstanceId = "instance-42",
        };
    }

    [Fact]
    public void Query_PassesFilterFromRequest_AndMapsRowsToSummaries()
    {
        var now = DateTimeOffset.UtcNow;
        var staleRow = MakeNotification(
            status: NotificationStatus.Pending, createdAt: now - TimeSpan.FromHours(1));
        var freshRow = MakeNotification(
            status: NotificationStatus.Pending, createdAt: now);
        _repository.QueryAsync(
                Arg.Any<NotificationOutboxFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(((IReadOnlyList<Notification>)new[] { staleRow, freshRow }, 2));
        var actor = CreateActor();

        actor.Tell(
            new NotificationOutboxQueryRequest(
                CorrelationId: "corr-1",
                StatusFilter: null,
                TypeFilter: null,
                SourceSiteFilter: "site-1",
                ListNameFilter: "ops-team",
                StuckOnly: false,
                SubjectKeyword: "tank",
                From: null,
                To: null,
                PageNumber: 2,
                PageSize: 25),
            TestActor);

        var response = ExpectMsg<NotificationOutboxQueryResponse>();
        Assert.Equal("corr-1", response.CorrelationId);
        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);
        Assert.Equal(2, response.TotalCount);
        Assert.Equal(2, response.Notifications.Count);

        _repository.Received(1).QueryAsync(
            Arg.Is<NotificationOutboxFilter>(f =>
                f.SourceSiteId == "site-1" &&
                f.ListName == "ops-team" &&
                f.SubjectKeyword == "tank" &&
                f.Status == null &&
                f.Type == null &&
                f.StuckOnly == false),
            2, 25, Arg.Any<CancellationToken>());

        // IsStuck: the hour-old Pending row is stuck, the just-created one is not.
        var staleSummary = response.Notifications.Single(s => s.NotificationId == staleRow.NotificationId);
        var freshSummary = response.Notifications.Single(s => s.NotificationId == freshRow.NotificationId);
        Assert.True(staleSummary.IsStuck);
        Assert.False(freshSummary.IsStuck);
        Assert.Equal("Pending", staleSummary.Status);
        Assert.Equal("Email", staleSummary.Type);
        Assert.Equal("site-1", staleSummary.SourceSiteId);
    }

    [Fact]
    public void Query_WithStatusFilterString_ParsesToEnumOnFilter()
    {
        _repository.QueryAsync(
                Arg.Any<NotificationOutboxFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(((IReadOnlyList<Notification>)Array.Empty<Notification>(), 0));
        var actor = CreateActor();

        actor.Tell(
            new NotificationOutboxQueryRequest(
                CorrelationId: "corr-2",
                StatusFilter: "Parked",
                TypeFilter: "Email",
                SourceSiteFilter: null,
                ListNameFilter: null,
                StuckOnly: true,
                SubjectKeyword: null,
                From: null,
                To: null,
                PageNumber: 1,
                PageSize: 50),
            TestActor);

        ExpectMsg<NotificationOutboxQueryResponse>();

        _repository.Received(1).QueryAsync(
            Arg.Is<NotificationOutboxFilter>(f =>
                f.Status == NotificationStatus.Parked &&
                f.Type == NotificationType.Email &&
                f.StuckOnly == true &&
                f.StuckCutoff != null),
            1, 50, Arg.Any<CancellationToken>());
    }

    [Fact]
    public void Query_PassesSourceNodeFilter_AndProjectsSourceNodeOntoSummary()
    {
        // Task 16: the Notifications page's new Node filter input pushes a
        // value into NotificationOutboxQueryRequest.SourceNodeFilter; the actor
        // must thread it onto NotificationOutboxFilter.SourceNode AND mirror
        // the row's SourceNode column onto the response summaries.
        var row = MakeNotification(status: NotificationStatus.Pending);
        row.SourceNode = "central-a";
        _repository.QueryAsync(
                Arg.Any<NotificationOutboxFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(((IReadOnlyList<Notification>)new[] { row }, 1));
        var actor = CreateActor();

        actor.Tell(
            new NotificationOutboxQueryRequest(
                CorrelationId: "corr-node",
                StatusFilter: null,
                TypeFilter: null,
                SourceSiteFilter: null,
                ListNameFilter: null,
                StuckOnly: false,
                SubjectKeyword: null,
                From: null,
                To: null,
                PageNumber: 1,
                PageSize: 50,
                SourceNodeFilter: "central-a"),
            TestActor);

        var response = ExpectMsg<NotificationOutboxQueryResponse>();
        Assert.True(response.Success);
        Assert.Equal("central-a", response.Notifications.Single().SourceNode);

        _repository.Received(1).QueryAsync(
            Arg.Is<NotificationOutboxFilter>(f => f.SourceNode == "central-a"),
            1, 50, Arg.Any<CancellationToken>());
    }

    [Fact]
    public void Query_RepositoryThrows_RepliesFailureWithEmptyList()
    {
        _repository.QueryAsync(
                Arg.Any<NotificationOutboxFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("db down"));
        var actor = CreateActor();

        actor.Tell(
            new NotificationOutboxQueryRequest(
                "corr-err", null, null, null, null, false, null, null, null, 1, 50),
            TestActor);

        var response = ExpectMsg<NotificationOutboxQueryResponse>();
        Assert.Equal("corr-err", response.CorrelationId);
        Assert.False(response.Success);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("db down", response.ErrorMessage);
        Assert.Empty(response.Notifications);
    }

    [Fact]
    public void StatusQuery_Found_RepliesWithRowDetail()
    {
        var row = MakeNotification(
            status: NotificationStatus.Retrying, retryCount: 3, lastError: "smtp timeout");
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        var actor = CreateActor();

        actor.Tell(new NotificationStatusQuery("corr-3", row.NotificationId), TestActor);

        var response = ExpectMsg<NotificationStatusResponse>();
        Assert.Equal("corr-3", response.CorrelationId);
        Assert.True(response.Found);
        Assert.Equal("Retrying", response.Status);
        Assert.Equal(3, response.RetryCount);
        Assert.Equal("smtp timeout", response.LastError);
    }

    [Fact]
    public void StatusQuery_NotFound_RepliesFoundFalse()
    {
        _repository.GetByIdAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns((Notification?)null);
        var actor = CreateActor();

        actor.Tell(new NotificationStatusQuery("corr-4", "missing-id"), TestActor);

        var response = ExpectMsg<NotificationStatusResponse>();
        Assert.Equal("corr-4", response.CorrelationId);
        Assert.False(response.Found);
        Assert.Equal(string.Empty, response.Status);
        Assert.Equal(0, response.RetryCount);
    }

    [Fact]
    public void Retry_ParkedNotification_ResetsToPending_AndSucceeds()
    {
        var row = MakeNotification(status: NotificationStatus.Parked, retryCount: 10, lastError: "gave up");
        row.NextAttemptAt = DateTimeOffset.UtcNow;
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        var actor = CreateActor();

        actor.Tell(new RetryNotificationRequest("corr-5", row.NotificationId), TestActor);

        var response = ExpectMsg<RetryNotificationResponse>();
        Assert.Equal("corr-5", response.CorrelationId);
        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);

        _repository.Received(1).UpdateAsync(
            Arg.Is<Notification>(n =>
                n.NotificationId == row.NotificationId &&
                n.Status == NotificationStatus.Pending &&
                n.RetryCount == 0 &&
                n.NextAttemptAt == null &&
                n.LastError == null),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public void Retry_NonParkedNotification_Fails()
    {
        var row = MakeNotification(status: NotificationStatus.Delivered);
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        var actor = CreateActor();

        actor.Tell(new RetryNotificationRequest("corr-6", row.NotificationId), TestActor);

        var response = ExpectMsg<RetryNotificationResponse>();
        Assert.False(response.Success);
        Assert.NotNull(response.ErrorMessage);
        _repository.DidNotReceive().UpdateAsync(Arg.Any<Notification>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public void Retry_MissingNotification_Fails()
    {
        _repository.GetByIdAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns((Notification?)null);
        var actor = CreateActor();

        actor.Tell(new RetryNotificationRequest("corr-7", "missing-id"), TestActor);

        var response = ExpectMsg<RetryNotificationResponse>();
        Assert.False(response.Success);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("not found", response.ErrorMessage);
    }

    /// <summary>
    /// The row is read successfully but the retention purge deletes it before the
    /// write lands, so the targeted UPDATE matches nothing. The operator must be
    /// told the notification is gone — reporting a re-queue that never happened
    /// is what the ExecuteUpdate rewrite silently introduced (the predecessor
    /// threw DbUpdateConcurrencyException here).
    /// </summary>
    [Fact]
    public void Retry_RowPurgedBeforeWrite_RepliesNotFound()
    {
        var row = MakeNotification(status: NotificationStatus.Parked, retryCount: 10, lastError: "gave up");
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        _repository.UpdateAsync(Arg.Any<Notification>(), Arg.Any<CancellationToken>()).Returns(false);
        var actor = CreateActor();

        actor.Tell(new RetryNotificationRequest("corr-vanished-retry", row.NotificationId), TestActor);

        var response = ExpectMsg<RetryNotificationResponse>();
        Assert.Equal("corr-vanished-retry", response.CorrelationId);
        Assert.False(response.Success);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("not found", response.ErrorMessage);
    }

    /// <summary>Discard half of <see cref="Retry_RowPurgedBeforeWrite_RepliesNotFound"/>.</summary>
    [Fact]
    public void Discard_RowPurgedBeforeWrite_RepliesNotFound()
    {
        var row = MakeNotification(status: NotificationStatus.Parked);
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        _repository.UpdateAsync(Arg.Any<Notification>(), Arg.Any<CancellationToken>()).Returns(false);
        var actor = CreateActor();

        actor.Tell(new DiscardNotificationRequest("corr-vanished-discard", row.NotificationId), TestActor);

        var response = ExpectMsg<DiscardNotificationResponse>();
        Assert.Equal("corr-vanished-discard", response.CorrelationId);
        Assert.False(response.Success);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("not found", response.ErrorMessage);
    }

    [Fact]
    public void Discard_ParkedNotification_MarksDiscarded_AndSucceeds()
    {
        var row = MakeNotification(status: NotificationStatus.Parked);
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        var actor = CreateActor();

        actor.Tell(new DiscardNotificationRequest("corr-8", row.NotificationId), TestActor);

        var response = ExpectMsg<DiscardNotificationResponse>();
        Assert.Equal("corr-8", response.CorrelationId);
        Assert.True(response.Success);

        _repository.Received(1).UpdateAsync(
            Arg.Is<Notification>(n => n.Status == NotificationStatus.Discarded),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public void Discard_NonParkedNotification_Fails()
    {
        var row = MakeNotification(status: NotificationStatus.Pending);
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        var actor = CreateActor();

        actor.Tell(new DiscardNotificationRequest("corr-9", row.NotificationId), TestActor);

        var response = ExpectMsg<DiscardNotificationResponse>();
        Assert.False(response.Success);
        Assert.NotNull(response.ErrorMessage);
        _repository.DidNotReceive().UpdateAsync(Arg.Any<Notification>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public void Discard_MissingNotification_Fails()
    {
        _repository.GetByIdAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns((Notification?)null);
        var actor = CreateActor();

        actor.Tell(new DiscardNotificationRequest("corr-10", "missing-id"), TestActor);

        var response = ExpectMsg<DiscardNotificationResponse>();
        Assert.False(response.Success);
        Assert.Contains("not found", response.ErrorMessage);
    }

    [Fact]
    public void DetailRequest_KnownId_ReturnsFullDetail_WithBodyAndResolvedTargets()
    {
        var row = MakeNotification(
            status: NotificationStatus.Delivered, retryCount: 2, lastError: "transient blip");
        row.Body = "Tank-7 has exceeded its high-level setpoint.";
        row.ResolvedTargets = "[\"ops@example.com\",\"oncall@example.com\"]";
        row.TypeData = "{\"priority\":\"high\"}";
        row.SourceScript = "HighLevelAlarm.csx";
        row.SourceNode = "node-a";
        row.SiteEnqueuedAt = DateTimeOffset.UtcNow.AddMinutes(-5);
        row.DeliveredAt = DateTimeOffset.UtcNow;
        _repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
        var actor = CreateActor();

        actor.Tell(new NotificationDetailRequest("corr-d1", row.NotificationId), TestActor);

        var response = ExpectMsg<NotificationDetailResponse>();
        Assert.Equal("corr-d1", response.CorrelationId);
        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);
        Assert.NotNull(response.Detail);
        var detail = response.Detail!;
        Assert.Equal(row.NotificationId, detail.NotificationId);
        Assert.Equal("Email", detail.Type);
        Assert.Equal("Delivered", detail.Status);
        Assert.Equal("Tank-7 has exceeded its high-level setpoint.", detail.Body);
        Assert.Equal("[\"ops@example.com\",\"oncall@example.com\"]", detail.ResolvedTargets);
        Assert.Equal("{\"priority\":\"high\"}", detail.TypeData);
        Assert.Equal("HighLevelAlarm.csx", detail.SourceScript);
        Assert.Equal("instance-42", detail.SourceInstanceId);
        Assert.Equal(2, detail.RetryCount);
        Assert.Equal("transient blip", detail.LastError);
        // SourceNode flows through the detail projection so the report detail
        // modal binds uniformly to the detail record (was previously read off
        // the summary).
        Assert.Equal("node-a", detail.SourceNode);
    }

    [Fact]
    public void DetailRequest_UnknownId_ReturnsNotFound()
    {
        _repository.GetByIdAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns((Notification?)null);
        var actor = CreateActor();

        actor.Tell(new NotificationDetailRequest("corr-d2", "missing-id"), TestActor);

        var response = ExpectMsg<NotificationDetailResponse>();
        Assert.Equal("corr-d2", response.CorrelationId);
        Assert.False(response.Success);
        Assert.Null(response.Detail);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("not found", response.ErrorMessage);
    }

    [Fact]
    public void KpiRequest_ComputesKpis_AndMapsSnapshot()
    {
        var snapshot = new NotificationKpiSnapshot(
            QueueDepth: 7,
            StuckCount: 2,
            ParkedCount: 3,
            DeliveredLastInterval: 12,
            OldestPendingAge: TimeSpan.FromMinutes(4));
        _repository.ComputeKpisAsync(
                Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .Returns(snapshot);
        var actor = CreateActor();

        actor.Tell(new NotificationKpiRequest("corr-11"), TestActor);

        var response = ExpectMsg<NotificationKpiResponse>();
        Assert.Equal("corr-11", response.CorrelationId);
        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);
        Assert.Equal(7, response.QueueDepth);
        Assert.Equal(2, response.StuckCount);
        Assert.Equal(3, response.ParkedCount);
        Assert.Equal(12, response.DeliveredLastInterval);
        Assert.Equal(TimeSpan.FromMinutes(4), response.OldestPendingAge);

        _repository.Received(1).ComputeKpisAsync(
            Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public void KpiRequest_RepositoryThrows_RepliesFailureResponse()
    {
        _repository.ComputeKpisAsync(
                Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("kpi db down"));
        var actor = CreateActor();

        actor.Tell(new NotificationKpiRequest("corr-12"), TestActor);

        // A repository fault yields a failure NotificationKpiResponse, not a Status.Failure.
        var response = ExpectMsg<NotificationKpiResponse>();
        Assert.Equal("corr-12", response.CorrelationId);
        Assert.False(response.Success);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("kpi db down", response.ErrorMessage);
        Assert.Equal(0, response.QueueDepth);
        Assert.Equal(0, response.StuckCount);
        Assert.Equal(0, response.ParkedCount);
        Assert.Equal(0, response.DeliveredLastInterval);
        Assert.Null(response.OldestPendingAge);
    }

    [Fact]
    public void PerSiteKpiRequest_RepliesWithPerSiteSnapshots()
    {
        _repository.ComputePerSiteKpisAsync(
                Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .Returns(new List<SiteNotificationKpiSnapshot>
            {
                new("plant-a", 4, 1, 0, 9, TimeSpan.FromMinutes(7)),
            });
        var actor = CreateActor();

        actor.Tell(new PerSiteNotificationKpiRequest("corr-ps"), TestActor);

        var response = ExpectMsg<PerSiteNotificationKpiResponse>();
        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);
        Assert.Equal("corr-ps", response.CorrelationId);
        Assert.Single(response.Sites);
        Assert.Equal("plant-a", response.Sites[0].SourceSiteId);

        _repository.Received(1).ComputePerSiteKpisAsync(
            Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public void PerSiteKpiRequest_RepositoryFault_RepliesUnsuccessful()
    {
        _repository.ComputePerSiteKpisAsync(
                Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("db down"));
        var actor = CreateActor();

        actor.Tell(new PerSiteNotificationKpiRequest("corr-ps"), TestActor);

        var response = ExpectMsg<PerSiteNotificationKpiResponse>();
        Assert.False(response.Success);
        Assert.Equal("corr-ps", response.CorrelationId);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("db down", response.ErrorMessage);
        Assert.Empty(response.Sites);
    }

    // ── Per-node KPI (T6: M5.2 per-node stuck-count KPIs) ──────────────────

    [Fact]
    public void PerNodeKpiRequest_RepliesWithPerNodeSnapshots()
    {
        _repository.ComputePerNodeKpisAsync(
                Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .Returns(new List<NodeNotificationKpiSnapshot>
            {
                new("node-a", QueueDepth: 3, StuckCount: 1, ParkedCount: 0,
                    DeliveredLastInterval: 5, OldestPendingAge: TimeSpan.FromMinutes(12)),
            });
        var actor = CreateActor();

        actor.Tell(new PerNodeNotificationKpiRequest("corr-pn"), TestActor);

        var response = ExpectMsg<PerNodeNotificationKpiResponse>();
        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);
        Assert.Equal("corr-pn", response.CorrelationId);
        Assert.Single(response.Nodes);
        Assert.Equal("node-a", response.Nodes[0].SourceNode);
        Assert.Equal(1, response.Nodes[0].StuckCount);

        _repository.Received(1).ComputePerNodeKpisAsync(
            Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public void PerNodeKpiRequest_RepositoryFault_RepliesUnsuccessful()
    {
        _repository.ComputePerNodeKpisAsync(
                Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("node-kpi db down"));
        var actor = CreateActor();

        actor.Tell(new PerNodeNotificationKpiRequest("corr-pn"), TestActor);

        var response = ExpectMsg<PerNodeNotificationKpiResponse>();
        Assert.False(response.Success);
        Assert.Equal("corr-pn", response.CorrelationId);
        Assert.NotNull(response.ErrorMessage);
        Assert.Contains("node-kpi db down", response.ErrorMessage);
        Assert.Empty(response.Nodes);
    }
}
