using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Communication.Actors;

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

/// <summary>
/// Tests for <see cref="PendingDeploymentPurgeActor"/> — the notify-and-fetch central
/// singleton that sweeps expired PendingDeployment staging rows. The actor is thin glue
/// over <c>IDeploymentManagerRepository.PurgeExpiredPendingDeploymentsAsync</c> (whose
/// row-level semantics are covered by the repository tests); these tests pin the actor's
/// own policy: it opens a per-tick DI scope, calls the purge with a now-ish threshold,
/// and — being best-effort hygiene — survives a throwing purge and keeps ticking.
/// </summary>
/// <remarks>
/// The substitute's return is driven by a closure with an <see cref="System.Threading.Interlocked"/>
/// counter rather than NSubstitute's <c>ReceivedCalls()</c>, because the repeating
/// scheduler timer records calls on a background thread concurrently with the test
/// thread's assertions — querying NSubstitute's call list under that concurrency can
/// throw "collection was modified". A fenced counter is safe to read while the timer runs.
/// </remarks>
public class PendingDeploymentPurgeActorTests : TestKit
{
    private IActorRef CreateActor(IDeploymentManagerRepository repo, TimeSpan interval)
    {
        var services = new ServiceCollection();
        // Mirror AddConfigurationDatabase: IDeploymentManagerRepository is scoped, so the
        // actor opens a fresh scope per tick and resolves the repository there.
        services.AddScoped(_ => repo);
        var sp = services.BuildServiceProvider();

        var options = Options.Create(new CommunicationOptions { PendingDeploymentPurgeInterval = interval });
        return Sys.ActorOf(Props.Create(() => new PendingDeploymentPurgeActor(
            sp, options, NullLogger<PendingDeploymentPurgeActor>.Instance)));
    }

    [Fact]
    public void Tick_PurgesExpiredRows_WithNowThreshold()
    {
        var calls = 0;
        DateTimeOffset captured = default;
        var repo = Substitute.For<IDeploymentManagerRepository>();
        repo.PurgeExpiredPendingDeploymentsAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .Returns(ci =>
            {
                captured = ci.Arg<DateTimeOffset>();
                Interlocked.Increment(ref calls);
                return 3;
            });

        // Large interval so the PreStart timer never fires during the test — only the
        // single manual tick below drives a purge, making the count deterministic.
        var actor = CreateActor(repo, TimeSpan.FromHours(1));

        var before = DateTimeOffset.UtcNow;
        actor.Tell(PendingDeploymentPurgeActor.PurgeTick.Instance);

        AwaitAssert(
            () => Assert.Equal(1, Volatile.Read(ref calls)),
            duration: TimeSpan.FromSeconds(3),
            interval: TimeSpan.FromMilliseconds(50));

        // The purge threshold is "now" — bounded by the window around the tick. Reading
        // `captured` after the fenced counter read is safe (Interlocked establishes the
        // happens-before with the write that precedes the increment).
        var after = DateTimeOffset.UtcNow;
        Assert.InRange(captured, before, after);
    }

    [Fact]
    public void Tick_PurgeThrows_ActorSurvives_AndRetriesOnNextInterval()
    {
        var calls = 0;
        var repo = Substitute.For<IDeploymentManagerRepository>();
        repo.PurgeExpiredPendingDeploymentsAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
            .Returns<int>(_ =>
            {
                Interlocked.Increment(ref calls);
                throw new InvalidOperationException("simulated purge failure");
            });

        // Fast interval so the self-scheduling timer fires repeatedly; the actor must
        // swallow each per-tick exception and keep ticking (best-effort hygiene — a
        // crash here would drop the singleton until failover).
        CreateActor(repo, TimeSpan.FromMilliseconds(100));

        AwaitAssert(
            () => Assert.True(Volatile.Read(ref calls) >= 2,
                $"expected >= 2 ticks despite failures, got {Volatile.Read(ref calls)}"),
            duration: TimeSpan.FromSeconds(5),
            interval: TimeSpan.FromMilliseconds(50));
    }
}
