using System.Net;
using System.Net.Sockets;
using Akka.Actor;
using Akka.Cluster;
using Akka.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;

/// <summary>
/// Real-ActorSystem tests for the simultaneous-cold-start split-brain guard
/// (<see cref="ClusterBootstrapCoordinator"/> + <see cref="ClusterBootstrapGuard"/>), started through
/// the SAME wiring as production (<see cref="ServiceCollectionExtensions.WithOtOpcUaClusterBootstrap"/>
/// with <c>Cluster:BootstrapGuard:Enabled</c> + the coordinator hosted service). This subsystem has a
/// history of bugs invisible to pure unit tests, so the load-bearing correctness properties are
/// asserted against running nodes, mirroring <see cref="SelfFirstSeedBootstrapTests"/>.
/// </summary>
/// <remarks>
/// Ephemeral loopback ports are 5-digit, so numeric order equals the guard's ordinal "host:port"
/// string order — <c>Min(port)</c> is the founder, <c>Max(port)</c> is the higher (probing) node.
/// </remarks>
public sealed class ClusterBootstrapCoordinatorTests
{
    private static int FreePort()
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    }

    /// <summary>
    /// Starts a node with the bootstrap guard ENABLED, wired exactly as Program.cs does: empty Akka
    /// seed list (via <c>BuildClusterOptions</c>) plus the coordinator hosted service that issues the
    /// reachability-gated <c>JoinSeedNodes</c>. Seeds are listed self-first (as every shipped config
    /// is); the guard, not the order, decides founder vs. joiner.
    /// </summary>
    private static async Task<IHost> StartGuardedNodeAsync(
        string systemName, int selfPort, int peerPort, int probeSeconds = 4)
    {
        var self = $"akka.tcp://{systemName}@127.0.0.1:{selfPort}";
        var peer = $"akka.tcp://{systemName}@127.0.0.1:{peerPort}";
        var options = new AkkaClusterOptions
        {
            SystemName = systemName,
            Hostname = "127.0.0.1",
            PublicHostname = "127.0.0.1",
            Port = selfPort,
            Roles = new[] { "driver" },
            SeedNodes = new[] { self, peer },
            BootstrapGuard = new ClusterBootstrapGuardOptions
            {
                Enabled = true,
                PartnerProbeSeconds = probeSeconds,
                PartnerProbeIntervalMs = 250,
                ProbeConnectTimeoutMs = 500,
            },
        };

        var builder = Host.CreateDefaultBuilder();
        builder.ConfigureServices(services =>
        {
            services.AddSingleton<IOptions<AkkaClusterOptions>>(Options.Create(options));
            services.AddAkka(systemName, (ab, sp) => ab.WithOtOpcUaClusterBootstrap(sp));
            // Mirror Program.cs — the coordinator is what drives the guarded join.
            services.AddHostedService(sp => new ClusterBootstrapCoordinator(
                () => sp.GetRequiredService<ActorSystem>(),
                sp.GetRequiredService<IOptions<AkkaClusterOptions>>(),
                sp.GetRequiredService<ILogger<ClusterBootstrapCoordinator>>()));
        });

        var host = builder.Build();
        await host.StartAsync();
        return host;
    }

    private static async Task<bool> WaitForUpMembersAsync(IHost host, int expected, TimeSpan timeout)
    {
        var cluster = Akka.Cluster.Cluster.Get(host.Services.GetRequiredService<ActorSystem>());
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (cluster.State.Members.Count(m => m.Status == MemberStatus.Up) >= expected) return true;
            await Task.Delay(200);
        }
        return cluster.State.Members.Count(m => m.Status == MemberStatus.Up) >= expected;
    }

    private static async Task StopAsync(IHost host)
    {
        try { await host.StopAsync(); }
        catch (Exception) { /* teardown only */ }
        host.Dispose();
    }

    /// <summary>
    /// The FOUNDER (lower address) with the guard on forms a cluster alone when its partner is dead —
    /// exactly as un-guarded self-first does. The guard must not regress the preferred founder.
    /// </summary>
    [Fact]
    public async Task Founder_forms_alone_when_partner_is_dead()
    {
        var low = FreePort();
        var high = FreePort();
        var founderPort = Math.Min(low, high);
        var deadPeerPort = Math.Max(low, high); // nothing listening

        var host = await StartGuardedNodeAsync("otopcua-guard-1", founderPort, deadPeerPort);
        try
        {
            (await WaitForUpMembersAsync(host, 1, TimeSpan.FromSeconds(30)))
                .ShouldBeTrue("the founder (lower address) must self-form immediately when its partner is down");
        }
        finally { await StopAsync(host); }
    }

    /// <summary>
    /// THE LOAD-BEARING CASE. The HIGHER node with the guard on must still cold-start ALONE when its
    /// partner is genuinely dead: it probes the (dead) founder, times out, and falls back to self-first.
    /// If the guard's peer-first logic were wrong, this node would hang in JoinSeedNodeProcess forever —
    /// the very regression a naive "always let the lower node found" rule would introduce.
    /// </summary>
    [Fact]
    public async Task Higher_node_forms_alone_when_partner_is_dead_after_probing()
    {
        var low = FreePort();
        var high = FreePort();
        var higherPort = Math.Max(low, high);
        var deadFounderPort = Math.Min(low, high); // the founder is dead

        var host = await StartGuardedNodeAsync("otopcua-guard-2", higherPort, deadFounderPort, probeSeconds: 3);
        try
        {
            // Must come Up AFTER the probe window (~3 s) expires and it falls back to self-first.
            (await WaitForUpMembersAsync(host, 1, TimeSpan.FromSeconds(30)))
                .ShouldBeTrue("the higher node must fall back to self-first and form alone once the dead partner never answers the probe");
        }
        finally { await StopAsync(host); }
    }

    /// <summary>
    /// FALSIFIABILITY CONTROL for the test above: the higher node does NOT form alone during the probe
    /// window — it is genuinely waiting/probing, not self-forming immediately (which would mean the
    /// guard is inert). If this ever starts seeing a member before the window, the probe is not gating.
    /// </summary>
    [Fact]
    public async Task Higher_node_does_not_form_before_the_probe_window_expires()
    {
        var low = FreePort();
        var high = FreePort();
        var higherPort = Math.Max(low, high);
        var deadFounderPort = Math.Min(low, high);

        var host = await StartGuardedNodeAsync("otopcua-guard-3", higherPort, deadFounderPort, probeSeconds: 10);
        try
        {
            // Well within the 10 s probe window: still no cluster, because it is probing the dead founder.
            (await WaitForUpMembersAsync(host, 1, TimeSpan.FromSeconds(3)))
                .ShouldBeFalse("the higher node must probe for the founder, not self-form immediately");
        }
        finally { await StopAsync(host); }
    }

    /// <summary>
    /// The higher node JOINS a live founder rather than forming a second cluster: start the founder,
    /// then the higher node — its probe finds the founder reachable, it commits peer-first, and the
    /// pair converges to ONE cluster of two.
    /// </summary>
    [Fact]
    public async Task Higher_node_joins_a_live_founder()
    {
        const string systemName = "otopcua-guard-4";
        var low = FreePort();
        var high = FreePort();
        var founderPort = Math.Min(low, high);
        var higherPort = Math.Max(low, high);

        var founder = await StartGuardedNodeAsync(systemName, founderPort, higherPort);
        IHost? higher = null;
        try
        {
            (await WaitForUpMembersAsync(founder, 1, TimeSpan.FromSeconds(30)))
                .ShouldBeTrue("the founder must be Up before the higher node probes it");

            higher = await StartGuardedNodeAsync(systemName, higherPort, founderPort);

            (await WaitForUpMembersAsync(higher, 2, TimeSpan.FromSeconds(60)))
                .ShouldBeTrue("the higher node must join the founder, forming one cluster of two");
            (await WaitForUpMembersAsync(founder, 2, TimeSpan.FromSeconds(60)))
                .ShouldBeTrue("the founder must see the higher node as a member of ITS cluster");
        }
        finally
        {
            if (higher is not null) await StopAsync(higher);
            await StopAsync(founder);
        }
    }
}
