using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;

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

/// <summary>
///     A split-topology node (one carrying a <c>cluster-{ClusterId}</c> role) left on a DPS transport
///     mode does not error — the transport simply delivers nothing across the mesh boundary, and the
///     symptom is a deployment that never applies or telemetry that never arrives. These tests make
///     that misconfiguration a loud host-start failure instead. A node with no cluster-role is exempt
///     (legacy / single-mesh / test), and the validator is a no-op for it.
/// </summary>
public class SplitTopologyTransportValidatorTests
{
    private static ValidateOptionsResult Validate(
        string? meshMode,
        string? telemetryMode,
        string? telemetryDialMode,
        params string[] roles)
    {
        var pairs = new Dictionary<string, string?>();
        for (var i = 0; i < roles.Length; i++)
        {
            pairs[$"Cluster:Roles:{i}"] = roles[i];
        }

        if (meshMode is not null) pairs["MeshTransport:Mode"] = meshMode;
        if (telemetryMode is not null) pairs["Telemetry:Mode"] = telemetryMode;
        if (telemetryDialMode is not null) pairs["TelemetryDial:Mode"] = telemetryDialMode;

        var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();

        // The validated options instance is never read (all four values come from IConfiguration), so a
        // default MeshTransportOptions is a fine stand-in.
        return new SplitTopologyTransportValidator(configuration)
            .Validate(MeshTransportOptions.SectionName, new MeshTransportOptions());
    }

    [Fact]
    public void Cluster_role_driver_node_on_Dps_mesh_transport_fails()
    {
        // Red-before-green anchor: a split-topology node on DistributedPubSub cannot deliver commands.
        var result = Validate(
            meshMode: MeshTransportOptions.ModeDps,
            telemetryMode: TelemetryOptions.ModeGrpc,
            telemetryDialMode: null,
            "driver", "cluster-SITE-A");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("MeshTransport:Mode");
        result.FailureMessage.ShouldContain(MeshTransportOptions.ModeClusterClient);
        result.FailureMessage.ShouldContain("SITE-A");
    }

    [Fact]
    public void Cluster_role_driver_node_fully_configured_passes()
    {
        Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: TelemetryOptions.ModeGrpc,
            telemetryDialMode: null,
            "driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Cluster_role_admin_node_fully_configured_passes()
    {
        Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: null,
            telemetryDialMode: TelemetryDialOptions.ModeGrpc,
            "admin", "cluster-SITE-A").Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Cluster_role_driver_node_on_Dps_telemetry_fails()
    {
        var result = Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: TelemetryOptions.ModeDps,
            telemetryDialMode: null,
            "driver", "cluster-SITE-A");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("Telemetry:Mode");
        result.FailureMessage.ShouldContain(TelemetryOptions.ModeGrpc);
    }

    [Fact]
    public void Cluster_role_admin_node_on_Dps_telemetry_dial_fails()
    {
        var result = Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: null,
            telemetryDialMode: TelemetryDialOptions.ModeDps,
            "admin", "cluster-SITE-A");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("TelemetryDial:Mode");
        result.FailureMessage.ShouldContain(TelemetryDialOptions.ModeGrpc);
    }

    [Fact]
    public void Fused_cluster_role_node_needs_all_three_to_pass()
    {
        Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: TelemetryOptions.ModeGrpc,
            telemetryDialMode: TelemetryDialOptions.ModeGrpc,
            "admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Fused_cluster_role_node_missing_telemetry_grpc_fails()
    {
        var result = Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: TelemetryOptions.ModeDps,
            telemetryDialMode: TelemetryDialOptions.ModeGrpc,
            "admin", "driver", "cluster-MAIN");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("Telemetry:Mode");
    }

    [Fact]
    public void Fused_cluster_role_node_missing_telemetry_dial_grpc_fails()
    {
        var result = Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: TelemetryOptions.ModeGrpc,
            telemetryDialMode: TelemetryDialOptions.ModeDps,
            "admin", "driver", "cluster-MAIN");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("TelemetryDial:Mode");
    }

    [Fact]
    public void Non_cluster_role_node_on_all_Dps_passes()
    {
        // The exemption that keeps the validator a no-op for every legacy / single-mesh / test node:
        // no cluster-{ClusterId} role means the split-topology rule does not apply at all.
        Validate(
            meshMode: MeshTransportOptions.ModeDps,
            telemetryMode: TelemetryOptions.ModeDps,
            telemetryDialMode: TelemetryDialOptions.ModeDps,
            "admin", "driver").Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Node_with_no_roles_on_all_Dps_passes()
    {
        // Absolute default: nothing declared, nothing to enforce.
        Validate(
            meshMode: MeshTransportOptions.ModeDps,
            telemetryMode: TelemetryOptions.ModeDps,
            telemetryDialMode: TelemetryDialOptions.ModeDps).Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Unset_modes_on_a_cluster_role_node_fail_as_the_Dps_default()
    {
        // A split node that never sets the mode keys inherits the Dps defaults — which the split
        // topology cannot use. Leaving them unset must fail exactly as setting them to Dps would.
        var result = Validate(
            meshMode: null,
            telemetryMode: null,
            telemetryDialMode: null,
            "admin", "driver", "cluster-MAIN");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("MeshTransport:Mode");
    }

    [Fact]
    public void Mode_matching_is_case_insensitive()
    {
        Validate(
            meshMode: "clusterclient",
            telemetryMode: "grpc",
            telemetryDialMode: "grpc",
            "admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Cluster_role_from_OTOPCUA_ROLES_only_on_Dps_fails()
    {
        // Roles-source divergence: Cluster:Roles is empty, so the node's effective Akka roles come from
        // OTOPCUA_ROLES (via RoleParser.Parse) exactly as Program.cs resolves them. A validator that
        // read only Cluster:Roles would see no cluster role and pass silently on Dps — the documented
        // production-incident shape.
        using (new EnvVarScope("OTOPCUA_ROLES", "driver,cluster-SITE-A"))
        {
            var pairs = new Dictionary<string, string?> { ["MeshTransport:Mode"] = MeshTransportOptions.ModeDps };
            var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();

            var result = new SplitTopologyTransportValidator(configuration)
                .Validate(MeshTransportOptions.SectionName, new MeshTransportOptions());

            result.Failed.ShouldBeTrue();
            result.FailureMessage.ShouldContain("MeshTransport:Mode");
        }
    }

    [Fact]
    public void Cluster_role_from_OTOPCUA_ROLES_is_ignored_when_Cluster_Roles_is_set()
    {
        // When Cluster:Roles is populated it wins outright — the OTOPCUA_ROLES fallback is not consulted,
        // mirroring AkkaClusterOptions.Roles. A non-cluster Cluster:Roles therefore stays exempt even if
        // a stray OTOPCUA_ROLES carries a cluster role.
        using (new EnvVarScope("OTOPCUA_ROLES", "driver,cluster-SITE-A"))
        {
            Validate(
                meshMode: MeshTransportOptions.ModeDps,
                telemetryMode: MeshTransportOptions.ModeDps,
                telemetryDialMode: MeshTransportOptions.ModeDps,
                "admin", "driver").Succeeded.ShouldBeTrue();
        }
    }

    [Fact]
    public void Mixed_case_cluster_and_role_names_on_Dps_fail()
    {
        // Case-sensitivity: the Cluster:Roles bind path is verbatim, so mixed-case entries must be
        // normalized here or they slip past the ordinal IsClusterRole / driver checks and pass silently.
        var result = Validate(
            meshMode: MeshTransportOptions.ModeDps,
            telemetryMode: TelemetryOptions.ModeGrpc,
            telemetryDialMode: null,
            "Driver", "Cluster-SITE-A");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("MeshTransport:Mode");
    }

    [Fact]
    public void Cluster_role_node_with_neither_admin_nor_driver_only_needs_ClusterClient()
    {
        // A cluster-role node that is neither admin nor driver has no telemetry surface to constrain —
        // only the always-on MeshTransport=ClusterClient rule applies. Telemetry/TelemetryDial left on
        // their Dps defaults must not fail it.
        Validate(
            meshMode: MeshTransportOptions.ModeClusterClient,
            telemetryMode: null,
            telemetryDialMode: null,
            "cluster-SITE-A").Succeeded.ShouldBeTrue();
    }

    /// <summary>Sets an environment variable for the scope's lifetime and restores its prior value on dispose.</summary>
    private sealed class EnvVarScope : IDisposable
    {
        private readonly string _name;
        private readonly string? _previous;

        public EnvVarScope(string name, string? value)
        {
            _name = name;
            _previous = Environment.GetEnvironmentVariable(name);
            Environment.SetEnvironmentVariable(name, value);
        }

        public void Dispose() => Environment.SetEnvironmentVariable(_name, _previous);
    }
}
