using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Host.Health;

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

/// <summary>
/// M2.14 (#28): unit tests for <see cref="RequiredSingletonsHealthCheck"/>.
///
/// The check probes each required central singleton through its local
/// <c>ClusterSingletonProxy</c> by Asking an <see cref="Identify"/> with a short
/// bounded timeout and treating a non-null <see cref="ActorIdentity.Subject"/> as
/// "reachable". These tests exercise that probe logic directly against a TestKit
/// <see cref="ActorSystem"/>:
/// <list type="bullet">
/// <item>present + reachable proxy paths (live echo actors) → Healthy;</item>
/// <item>a missing proxy path (ActorSelection resolves a null Subject) → Unhealthy
/// naming the unreachable singleton.</item>
/// </list>
/// No WebApplicationFactory / DB / formed cluster is needed — the probe is just an
/// in-process Identify round-trip, so the tests are deterministic and fast.
/// </summary>
public class RequiredSingletonsHealthCheckTests : TestKit
{
    /// <summary>A minimal live actor that does nothing — its mere existence makes
    /// an <see cref="Identify"/> resolve a non-null Subject (i.e. "reachable").</summary>
    /// <remarks>No <c>Receive&lt;Identify&gt;</c> handler is needed: Akka's
    /// <see cref="ActorBase"/> answers every <see cref="Identify"/> message with
    /// an <see cref="ActorIdentity"/> automatically, so an empty actor at the proxy
    /// path is sufficient to simulate a reachable singleton.</remarks>
    private sealed class EchoActor : ReceiveActor
    {
    }

    private IServiceProvider ProviderReturning(ActorSystem system)
    {
        var services = new ServiceCollection();
        services.AddSingleton(system);
        return services.BuildServiceProvider();
    }

    private static async Task<HealthCheckResult> RunAsync(RequiredSingletonsHealthCheck check)
    {
        var context = new HealthCheckContext
        {
            Registration = new HealthCheckRegistration(
                "required-singletons", check, failureStatus: null, tags: null),
        };
        return await check.CheckHealthAsync(context, CancellationToken.None);
    }

    [Fact]
    public async Task AllRequiredSingletonProxiesReachable_ReportsHealthy()
    {
        // Create a live actor at every required proxy path so each Identify resolves
        // a non-null Subject.
        foreach (var name in RequiredSingletonsHealthCheck.RequiredSingletonProxyNames)
        {
            Sys.ActorOf(Props.Create(() => new EchoActor()), name);
        }

        var check = new RequiredSingletonsHealthCheck(
            ProviderReturning(Sys),
            NullLogger<RequiredSingletonsHealthCheck>.Instance);

        var result = await RunAsync(check);

        Assert.Equal(HealthStatus.Healthy, result.Status);
    }

    [Fact]
    public async Task OneRequiredSingletonUnreachable_ReportsUnhealthyNamingIt()
    {
        // Create all but one proxy. The missing one's ActorSelection resolves an
        // ActorIdentity with a null Subject within the bounded timeout → unreachable.
        var missing = RequiredSingletonsHealthCheck.RequiredSingletonProxyNames[0];
        foreach (var name in RequiredSingletonsHealthCheck.RequiredSingletonProxyNames)
        {
            if (name == missing)
                continue;
            Sys.ActorOf(Props.Create(() => new EchoActor()), name);
        }

        var check = new RequiredSingletonsHealthCheck(
            ProviderReturning(Sys),
            NullLogger<RequiredSingletonsHealthCheck>.Instance);

        var result = await RunAsync(check);

        Assert.Equal(HealthStatus.Unhealthy, result.Status);
        Assert.NotNull(result.Description);
        Assert.Contains(missing, result.Description!);
    }

    [Fact]
    public async Task ActorSystemNotYetAvailable_ReportsUnhealthy_DoesNotThrow()
    {
        // Startup race: ActorSystem not yet bridged into DI. The check must map this
        // to Unhealthy (the node is not ready to serve) rather than throwing.
        var emptyProvider = new ServiceCollection().BuildServiceProvider();

        var check = new RequiredSingletonsHealthCheck(
            emptyProvider,
            NullLogger<RequiredSingletonsHealthCheck>.Instance);

        var result = await RunAsync(check);

        Assert.Equal(HealthStatus.Unhealthy, result.Status);
    }

    [Fact]
    public async Task PreCancelledToken_ReportsUnhealthy_DoesNotThrow()
    {
        // Shutdown-race path: CheckHealthAsync is called with an already-cancelled
        // token (e.g. host is tearing down). The check must never throw — any
        // OperationCanceledException from Ask must be caught and mapped to Unhealthy.
        foreach (var name in RequiredSingletonsHealthCheck.RequiredSingletonProxyNames)
        {
            Sys.ActorOf(Props.Create(() => new EchoActor()), name);
        }

        var check = new RequiredSingletonsHealthCheck(
            ProviderReturning(Sys),
            NullLogger<RequiredSingletonsHealthCheck>.Instance);

        using var cts = new CancellationTokenSource();
        cts.Cancel(); // already cancelled before the check runs

        var context = new HealthCheckContext
        {
            Registration = new HealthCheckRegistration(
                "required-singletons", check, failureStatus: null, tags: null),
        };

        // Must not throw; an already-cancelled token → all probes fail → Unhealthy.
        var result = await check.CheckHealthAsync(context, cts.Token);

        Assert.Equal(HealthStatus.Unhealthy, result.Status);
    }
}
