using Akka.Actor;
using Akka.Cluster;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.Health.Akka;

namespace ZB.MOM.WW.Health.Akka.Tests;

/// <summary>
/// Covers the cluster-view <c>data</c> dictionary that <see cref="AkkaClusterHealthCheck"/> publishes
/// from 0.2.0 on (consumed by the family overview dashboard to show each cluster's current leader).
/// Uses a real single-node self-joined cluster via <see cref="TestKit"/> — the values come off
/// <see cref="Cluster"/>, which cannot be faked, so a formed cluster is the only honest fixture.
/// </summary>
public sealed class AkkaClusterDataTests : TestKit
{
    private const string ClusterConfig = """
        akka {
          actor.provider = cluster
          remote.dot-netty.tcp {
            hostname = "127.0.0.1"
            port = 0
          }
          cluster {
            roles = ["driver", "admin"]
            # Keep the single-node self-join deterministic and fast in-test.
            min-nr-of-members = 1
          }
        }
        """;

    private static readonly TimeSpan FormationTimeout = TimeSpan.FromSeconds(30);

    public AkkaClusterDataTests()
        : base(ClusterConfig, nameof(AkkaClusterDataTests))
    {
    }

    /// <summary>Self-joins the test node and waits until it is Up with a converged leader.</summary>
    private Cluster FormSingleNodeCluster()
    {
        var cluster = Cluster.Get(Sys);
        cluster.Join(cluster.SelfAddress);
        AwaitCondition(
            () => cluster.SelfMember.Status == MemberStatus.Up && cluster.State.Leader is not null,
            FormationTimeout);
        return cluster;
    }

    [Fact]
    public void BuildClusterData_SingleNodeCluster_ReportsSelfAsLeader()
    {
        var cluster = FormSingleNodeCluster();

        var data = AkkaClusterHealthCheck.BuildClusterData(cluster);

        Assert.Equal(cluster.SelfAddress.ToString(), Assert.IsType<string>(data["leader"]));
        Assert.Equal(cluster.SelfAddress.ToString(), Assert.IsType<string>(data["selfAddress"]));
        Assert.Equal(1, Assert.IsType<int>(data["memberCount"]));
        Assert.Equal(0, Assert.IsType<int>(data["unreachableCount"]));
    }

    [Fact]
    public void BuildClusterData_RolesRoundTripSorted()
    {
        var cluster = FormSingleNodeCluster();

        var roles = Assert.IsType<string[]>(AkkaClusterHealthCheck.BuildClusterData(cluster)["selfRoles"]);

        // Sorted, so the payload is stable across restarts; the configured roles survive verbatim.
        Assert.Equal(new[] { "admin", "driver" }, roles);
    }

    [Fact]
    public async Task CheckHealth_FormedCluster_HealthyResultCarriesClusterData()
    {
        var cluster = FormSingleNodeCluster();
        var provider = new ServiceCollection().AddSingleton(Sys).BuildServiceProvider();
        var check = new AkkaClusterHealthCheck(provider, AkkaClusterStatusPolicy.Default);

        var result = await check.CheckHealthAsync(NewContext(check));

        Assert.Equal(HealthStatus.Healthy, result.Status);
        Assert.Equal(cluster.SelfAddress.ToString(), result.Data["leader"]);
        Assert.Equal(1, result.Data["memberCount"]);
    }

    [Fact]
    public async Task CheckHealth_NoActorSystem_CarriesNoData()
    {
        // The startup-safety paths stay description-only: an empty data dictionary is what makes the
        // writer omit the "data" key entirely, so a not-yet-started node's payload is unchanged.
        var provider = new ServiceCollection().BuildServiceProvider();
        var check = new AkkaClusterHealthCheck(provider, AkkaClusterStatusPolicy.Default);

        var result = await check.CheckHealthAsync(NewContext(check));

        Assert.Equal(HealthStatus.Degraded, result.Status);
        Assert.Empty(result.Data);
    }

    private static HealthCheckContext NewContext(IHealthCheck check) => new()
    {
        Registration = new HealthCheckRegistration("akka-cluster", check, HealthStatus.Unhealthy, tags: null),
    };
}
