using Akka.Actor;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Host;

namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;

/// <summary>
///     Unit coverage for the #459 down-if-alone recovery watchdog: an unexpected
///     <see cref="ActorSystem"/> termination (SBR self-down) must stop the host so the supervisor
///     restarts it, while a graceful host shutdown must stay silent and not re-trigger stop.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ActorSystemTerminationWatchdogTests
{
    /// <summary>An unexpected ActorSystem termination stops the host so the supervisor restarts the node.</summary>
    [Fact]
    public async Task Unexpected_termination_stops_the_application()
    {
        var system = ActorSystem.Create("watchdog-unexpected");
        var lifetime = new FakeHostApplicationLifetime();
        var watchdog = new ActorSystemTerminationWatchdog(
            () => system, lifetime, NullLogger<ActorSystemTerminationWatchdog>.Instance);

        await watchdog.StartAsync(CancellationToken.None);

        // Simulate the SBR self-down: the ActorSystem terminates while the host is still running.
        await system.Terminate();

        await WaitForAsync(() => lifetime.StopApplicationCalls > 0, TimeSpan.FromSeconds(5));
        lifetime.StopApplicationCalls.ShouldBe(1, "an out-of-band ActorSystem termination must stop the host once");
    }

    /// <summary>A graceful host stop (StopAsync ran) leaves the termination continuation silent.</summary>
    [Fact]
    public async Task Graceful_stop_does_not_stop_the_application()
    {
        var system = ActorSystem.Create("watchdog-graceful");
        var lifetime = new FakeHostApplicationLifetime();
        var watchdog = new ActorSystemTerminationWatchdog(
            () => system, lifetime, NullLogger<ActorSystemTerminationWatchdog>.Instance);

        await watchdog.StartAsync(CancellationToken.None);

        // Host is shutting down normally: StopAsync runs, THEN the system terminates.
        await watchdog.StopAsync(CancellationToken.None);
        await system.Terminate();

        // Give any continuation a chance to run, then assert it stayed silent.
        await Task.Delay(500, TestContext.Current.CancellationToken);
        lifetime.StopApplicationCalls.ShouldBe(0, "a graceful shutdown must not re-trigger StopApplication");
    }

    /// <summary>Termination while the host is already stopping (ApplicationStopping fired) stays silent.</summary>
    [Fact]
    public async Task Termination_while_already_stopping_stays_silent()
    {
        var system = ActorSystem.Create("watchdog-already-stopping");
        var lifetime = new FakeHostApplicationLifetime();
        var watchdog = new ActorSystemTerminationWatchdog(
            () => system, lifetime, NullLogger<ActorSystemTerminationWatchdog>.Instance);

        await watchdog.StartAsync(CancellationToken.None);

        lifetime.TriggerStopping(); // the host is already tearing down (ApplicationStopping cancelled)
        await system.Terminate();

        await Task.Delay(500, TestContext.Current.CancellationToken);
        lifetime.StopApplicationCalls.ShouldBe(0, "termination during an in-progress host shutdown must stay silent");
    }

    private static async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (!condition() && DateTime.UtcNow < deadline)
            await Task.Delay(20);
    }

    /// <summary>Minimal <see cref="IHostApplicationLifetime"/> that records <see cref="StopApplication"/> calls.</summary>
    private sealed class FakeHostApplicationLifetime : IHostApplicationLifetime
    {
        private readonly CancellationTokenSource _stopping = new();
        public int StopApplicationCalls { get; private set; }
        public CancellationToken ApplicationStarted => CancellationToken.None;
        public CancellationToken ApplicationStopping => _stopping.Token;
        public CancellationToken ApplicationStopped => CancellationToken.None;
        public void StopApplication() => StopApplicationCalls++;
        public void TriggerStopping() => _stopping.Cancel();
    }
}
