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

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

/// <summary>
/// Task 16: Tests for the <see cref="NotificationOutboxActor"/> daily purge job — the
/// periodic sweep that bulk-deletes terminal notification rows older than
/// <see cref="NotificationOutboxOptions.TerminalRetention"/> via
/// <see cref="INotificationOutboxRepository.DeleteTerminalOlderThanAsync"/>.
/// </summary>
public class NotificationOutboxActorPurgeTests : TestKit
{
    private readonly INotificationOutboxRepository _outboxRepository =
        OutboxRepositorySubstitute.Healthy();

    private readonly INotificationRepository _notificationRepository =
        Substitute.For<INotificationRepository>();

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

    /// <summary>
    /// Creates the actor with both the dispatch and purge timers set to a long interval so
    /// neither periodic timer fires during a test — purge ticks are sent manually instead.
    /// </summary>
    private IActorRef CreateActor(NotificationOutboxOptions? options = null)
    {
        return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
            BuildServiceProvider(),
            options ?? new NotificationOutboxOptions
            {
                DispatchInterval = TimeSpan.FromHours(1),
                PurgeInterval = TimeSpan.FromHours(1),
            },
            new NoOpCentralAuditWriter(),
            NullLogger<NotificationOutboxActor>.Instance)));
    }

    [Fact]
    public void PurgeTick_DeletesTerminalRows_WithCutoffAtUtcNowMinusTerminalRetention()
    {
        var retention = TimeSpan.FromDays(30);
        var options = new NotificationOutboxOptions
        {
            DispatchInterval = TimeSpan.FromHours(1),
            PurgeInterval = TimeSpan.FromHours(1),
            TerminalRetention = retention,
        };
        _outboxRepository
            .DeleteTerminalOlderThanAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .Returns(3);
        var actor = CreateActor(options);

        var expectedCutoff = DateTimeOffset.UtcNow - retention;
        actor.Tell(InternalMessages.PurgeTick.Instance);

        AwaitAssert(() =>
            _outboxRepository.Received(1).DeleteTerminalOlderThanAsync(
                Arg.Is<DateTimeOffset>(cutoff =>
                    (cutoff - expectedCutoff).Duration() < TimeSpan.FromMinutes(1)),
                Arg.Any<CancellationToken>()));
    }

    [Fact]
    public void FaultedPurge_DoesNotCrashActor_AndSubsequentPurgeStillRuns()
    {
        var calls = 0;
        _outboxRepository
            .DeleteTerminalOlderThanAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .Returns(_ =>
            {
                calls++;
                // First purge faults; the second returns normally to prove the actor lives on.
                if (calls == 1)
                {
                    throw new InvalidOperationException("db down");
                }

                return Task.FromResult(0);
            });
        var actor = CreateActor();

        // First tick: the purge faults internally but must be handled and not kill the actor.
        actor.Tell(InternalMessages.PurgeTick.Instance);
        AwaitAssert(() => _outboxRepository.Received(1).DeleteTerminalOlderThanAsync(
            Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>()));

        // Second tick: if the faulted purge had crashed the actor, this would never run.
        actor.Tell(InternalMessages.PurgeTick.Instance);
        AwaitAssert(() => _outboxRepository.Received(2).DeleteTerminalOlderThanAsync(
            Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>()));
    }
}
