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

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

/// <summary>
///     A <c>FetchAndCache</c> node with no endpoint or no key does not error — it simply never
///     applies a deployment, and the deploy times out naming a node that is otherwise healthy. These
///     tests make that misconfiguration a loud host-start failure instead.
/// </summary>
public class ConfigSourceOptionsValidatorTests
{
    private static ValidateOptionsResult Validate(ConfigSourceOptions o, params string[] roles)
    {
        var pairs = new Dictionary<string, string?>();
        for (var i = 0; i < roles.Length; i++)
        {
            pairs[$"Cluster:Roles:{i}"] = roles[i];
        }

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

        return new ConfigSourceOptionsValidator(configuration).Validate(ConfigSourceOptions.SectionName, o);
    }

    private static ConfigSourceOptions ValidFetch() => new()
    {
        Mode = ConfigSourceOptions.ModeFetchAndCache,
        CentralFetchEndpoints = ["http://central-1:4055", "http://central-2:4055"],
        ApiKey = "shared-key",
        FetchTimeoutSeconds = 30,
    };

    [Fact]
    public void Default_options_are_valid()
    {
        // The default is Direct with everything empty — the pre-Phase-3 behaviour. A node that never
        // sets this section must validate, which is the premise of the dark switch.
        Validate(new ConfigSourceOptions()).Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Unknown_mode_fails()
    {
        var result = Validate(new ConfigSourceOptions { Mode = "cache" });

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("cache");
    }

    [Fact]
    public void Mode_matching_is_case_insensitive()
    {
        Validate(new ConfigSourceOptions
        {
            Mode = "fetchandcache",
            CentralFetchEndpoints = ["http://central-1:4055"],
            ApiKey = "k",
        }).Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void FetchAndCache_with_no_endpoints_fails()
    {
        var o = ValidFetch();
        o.CentralFetchEndpoints = [];

        var result = Validate(o);

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.CentralFetchEndpoints));
    }

    [Fact]
    public void FetchAndCache_with_a_non_http_endpoint_fails()
    {
        var o = ValidFetch();
        o.CentralFetchEndpoints = ["central-1:4055"];

        var result = Validate(o);

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain("central-1:4055");
    }

    [Fact]
    public void FetchAndCache_with_an_empty_key_fails()
    {
        var o = ValidFetch();
        o.ApiKey = "";

        var result = Validate(o);

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.ApiKey));
    }

    [Fact]
    public void FetchAndCache_with_a_non_positive_timeout_fails()
    {
        var o = ValidFetch();
        o.FetchTimeoutSeconds = 0;

        var result = Validate(o);

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.FetchTimeoutSeconds));
    }

    [Fact]
    public void A_fully_populated_FetchAndCache_config_is_valid()
    {
        Validate(ValidFetch()).Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Direct_ignores_the_empty_fetch_surface()
    {
        // Under Direct the endpoints/key are never read, so their emptiness must not fail validation —
        // otherwise every admin-only and every Direct node would be forced to carry fetch config.
        Validate(new ConfigSourceOptions
        {
            Mode = ConfigSourceOptions.ModeDirect,
            CentralFetchEndpoints = [],
            ApiKey = "",
            FetchTimeoutSeconds = 0,
        }).Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Driver_only_node_on_Direct_fails()
    {
        // A driver-only node has no central ConfigDb connection (Phase 4) — Direct silently produces a
        // node that can never read its configuration.
        var result = Validate(
            new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
            "driver");

        result.Failed.ShouldBeTrue();
        result.FailureMessage.ShouldContain(ConfigSourceOptions.ModeFetchAndCache);
    }

    [Fact]
    public void Fused_admin_and_driver_node_on_Direct_is_valid()
    {
        // A fused node holds the ConfigDb connection via its admin role, so Direct is still legitimate.
        Validate(
            new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
            "admin", "driver").Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Driver_only_node_on_FetchAndCache_is_valid()
    {
        Validate(ValidFetch(), "driver").Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Admin_only_node_on_Direct_is_valid()
    {
        // An admin-only node has no driver role at all, so the driver-only rule does not apply to it.
        Validate(
            new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
            "admin").Succeeded.ShouldBeTrue();
    }
}
