using Akka.Configuration;
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
using ZB.MOM.WW.ScadaBridge.Host.Actors;

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

/// <summary>
/// Host-006: the Akka HOCON document must be assembled with every interpolated value
/// quoted/escaped, so a hostname, seed-node URI or split-brain strategy containing a
/// quote, backslash or whitespace cannot corrupt the document or be silently
/// misparsed.
/// </summary>
public class HoconBuilderTests
{
    private static ClusterOptions DefaultCluster() => new()
    {
        SeedNodes = new List<string>
        {
            "akka.tcp://scadabridge@localhost:8081",
            "akka.tcp://scadabridge@localhost:8082",
        },
        SplitBrainResolverStrategy = "keep-oldest",
        MinNrOfMembers = 1,
        StableAfter = TimeSpan.FromSeconds(15),
        HeartbeatInterval = TimeSpan.FromSeconds(2),
        FailureDetectionThreshold = TimeSpan.FromSeconds(10),
    };

    [Fact]
    public void BuildHocon_ClusterLeavePhaseTimeout_ExceedsDrainBudget()
    {
        // Review 01 [Medium]: the five singleton drain tasks GracefulStop(10s)
        // inside cluster-leave, whose default phase timeout is 5s — Akka abandons
        // the task at 5s and the drain guarantee is silently halved.
        var node = new NodeOptions { Role = "Central", NodeHostname = "node1", RemotingPort = 8081 };
        var hocon = AkkaHostedService.BuildHocon(
            node, DefaultCluster(), new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        var phaseTimeout = config.GetTimeSpan("akka.coordinated-shutdown.phases.cluster-leave.timeout");
        Assert.True(phaseTimeout >= TimeSpan.FromSeconds(15),
            $"cluster-leave phase timeout ({phaseTimeout}) must exceed the 10s singleton GracefulStop budget");
    }

    [Fact]
    public void BuildHocon_PlainValues_ParsesAndPreservesValues()
    {
        var node = new NodeOptions
        {
            Role = "Central",
            NodeHostname = "central-node1",
            RemotingPort = 8081,
        };

        var hocon = AkkaHostedService.BuildHocon(
            node, DefaultCluster(), new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.Equal("central-node1", config.GetString("akka.remote.dot-netty.tcp.hostname"));
        Assert.Equal("keep-oldest", config.GetString("akka.cluster.split-brain-resolver.active-strategy"));
    }

    [Fact]
    public void BuildHocon_HostnameWithQuote_DoesNotCorruptDocument()
    {
        // A hostname containing a double quote would, with raw interpolation, close
        // the HOCON string literal early and corrupt every following key.
        var node = new NodeOptions
        {
            Role = "Central",
            NodeHostname = "evil\"host",
            RemotingPort = 8081,
        };

        var hocon = AkkaHostedService.BuildHocon(
            node, DefaultCluster(), new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        // Must still parse, and the keys after the hostname must remain intact.
        var config = ConfigurationFactory.ParseString(hocon);
        Assert.Equal("evil\"host", config.GetString("akka.remote.dot-netty.tcp.hostname"));
        Assert.Equal(8081, config.GetInt("akka.remote.dot-netty.tcp.port"));
        Assert.Equal(1, config.GetInt("akka.cluster.min-nr-of-members"));
    }

    [Fact]
    public void BuildHocon_StrategyWithWhitespace_RemainsQuoted()
    {
        var cluster = DefaultCluster();
        cluster.SplitBrainResolverStrategy = "keep oldest";
        var node = new NodeOptions
        {
            Role = "Central",
            NodeHostname = "node1",
            RemotingPort = 8081,
        };

        var hocon = AkkaHostedService.BuildHocon(
            node, cluster, new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.Equal("keep oldest", config.GetString("akka.cluster.split-brain-resolver.active-strategy"));
    }

    private static NodeOptions DefaultNode() => new()
    {
        Role = "Central",
        NodeHostname = "node1",
        RemotingPort = 8081,
    };

    [Fact]
    public void BuildHocon_DownIfAloneTrue_EmitsOn()
    {
        // Host-012: BuildHocon must consume ClusterOptions.DownIfAlone, not
        // hard-code the down-if-alone token.
        var cluster = DefaultCluster();
        cluster.DownIfAlone = true;

        var hocon = AkkaHostedService.BuildHocon(
            DefaultNode(), cluster, new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.True(config.GetBoolean(
            "akka.cluster.split-brain-resolver.keep-oldest.down-if-alone"));
    }

    [Fact]
    public void BuildHocon_DownIfAloneFalse_EmitsOff()
    {
        // Host-012: setting DownIfAlone=false must actually flip the emitted token.
        var cluster = DefaultCluster();
        cluster.DownIfAlone = false;

        var hocon = AkkaHostedService.BuildHocon(
            DefaultNode(), cluster, new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.False(config.GetBoolean(
            "akka.cluster.split-brain-resolver.keep-oldest.down-if-alone"));
    }

    [Fact]
    public void BuildHocon_SubSecondTimings_PreservedWithoutRounding()
    {
        // Host-013: cluster timing values below one second must not be rounded to
        // whole seconds. A 750ms heartbeat must survive as 750ms, not collapse to
        // 1s — and a 500ms value must not collapse to a degenerate 0s.
        var cluster = DefaultCluster();
        cluster.HeartbeatInterval = TimeSpan.FromMilliseconds(750);
        cluster.FailureDetectionThreshold = TimeSpan.FromMilliseconds(2600);
        cluster.StableAfter = TimeSpan.FromMilliseconds(500);

        var hocon = AkkaHostedService.BuildHocon(
            DefaultNode(), cluster, new[] { "Central" },
            TimeSpan.FromMilliseconds(2500), TimeSpan.FromMilliseconds(7500));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.Equal(
            TimeSpan.FromMilliseconds(750),
            config.GetTimeSpan("akka.cluster.failure-detector.heartbeat-interval"));
        Assert.Equal(
            TimeSpan.FromMilliseconds(2600),
            config.GetTimeSpan("akka.cluster.failure-detector.acceptable-heartbeat-pause"));
        Assert.Equal(
            TimeSpan.FromMilliseconds(500),
            config.GetTimeSpan("akka.cluster.split-brain-resolver.stable-after"));
        Assert.Equal(
            TimeSpan.FromMilliseconds(2500),
            config.GetTimeSpan("akka.remote.transport-failure-detector.heartbeat-interval"));
        Assert.Equal(
            TimeSpan.FromMilliseconds(7500),
            config.GetTimeSpan("akka.remote.transport-failure-detector.acceptable-heartbeat-pause"));
    }

    [Fact]
    public void BuildHocon_EnablesSplitBrainResolverDowningProvider()
    {
        // Review 01 [Critical]: without downing-provider-class the entire
        // split-brain-resolver section is inert (Akka default = NoDowning) and
        // singletons never migrate on a hard crash.
        var node = new NodeOptions { Role = "Central", NodeHostname = "node1", RemotingPort = 8081 };

        var hocon = AkkaHostedService.BuildHocon(
            node, DefaultCluster(), new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.Equal(
            "Akka.Cluster.SBR.SplitBrainResolverProvider, Akka.Cluster",
            config.GetString("akka.cluster.downing-provider-class"));
    }

    [Fact]
    public void BuildHocon_AutoDownStrategy_EmitsAutoDowningProvider()
    {
        // Decision 2026-07-21 (availability over partition-safety): 'auto-down' must
        // swap the downing provider to Akka's AutoDowning so the survivor downs a
        // crashed peer — including a crashed OLDEST, which two-node keep-oldest
        // cannot survive — after StableAfter.
        var cluster = DefaultCluster();
        cluster.SplitBrainResolverStrategy = "auto-down";
        cluster.StableAfter = TimeSpan.FromSeconds(15);

        var hocon = AkkaHostedService.BuildHocon(
            DefaultNode(), cluster, new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.Equal(
            "Akka.Cluster.AutoDowning, Akka.Cluster",
            config.GetString("akka.cluster.downing-provider-class"));
        Assert.Equal(
            TimeSpan.FromSeconds(15),
            config.GetTimeSpan("akka.cluster.auto-down-unreachable-after"));
        // The SBR section must NOT be active alongside AutoDowning.
        Assert.False(config.HasPath("akka.cluster.split-brain-resolver.active-strategy"));
    }

    [Fact]
    public void BuildHocon_AutoDownStrategy_IsCaseInsensitive_AndDocumentStaysIntact()
    {
        var cluster = DefaultCluster();
        cluster.SplitBrainResolverStrategy = "Auto-Down";

        var hocon = AkkaHostedService.BuildHocon(
            DefaultNode(), cluster, new[] { "Central" },
            TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);
        Assert.Equal(
            "Akka.Cluster.AutoDowning, Akka.Cluster",
            config.GetString("akka.cluster.downing-provider-class"));
        // Keys after the downing block must remain intact (document not corrupted).
        Assert.Equal(1, config.GetInt("akka.cluster.min-nr-of-members"));
        Assert.True(config.GetBoolean("akka.cluster.run-coordinated-shutdown-when-down"));
    }
}
