using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport;

namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;

/// <summary>
/// Task 3 — verifies the net-new <see cref="LdapOptionsValidator"/> (built on the shared
/// <c>ZB.MOM.WW.Configuration</c> <c>OptionsValidatorBase</c>/<c>ValidationBuilder</c>) gates on
/// <see cref="LdapOptions.Enabled"/>, and that when enabled it requires <c>Server</c>,
/// <c>SearchBase</c>, and a valid <c>Port</c>. Failure messages carry the real <c>"Ldap:"</c>
/// section prefix so they read correctly when surfaced at host startup. Also verifies the
/// insecure-transport startup guard: a real-LDAP config selecting plaintext transport without
/// <see cref="LdapOptions.AllowInsecure"/> fails fast at boot.
/// </summary>
public sealed class LdapOptionsValidatorTests
{
    private static readonly LdapOptionsValidator Sut = new();

    private const string InsecureTransportFailure =
        "LDAP transport is None (plaintext) but AllowInsecure is false — set Transport to Ldaps/StartTls or set AllowInsecure for dev.";

    /// <summary>Valid enabled options (a TLS transport) pass validation.</summary>
    [Fact]
    public void Valid_enabled_options_succeed()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.Ldaps,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// Insecure-transport guard: an enabled real-LDAP config that selects plaintext
    /// <see cref="LdapTransport.None"/> without <see cref="LdapOptions.AllowInsecure"/> fails
    /// startup validation with the guard message.
    /// </summary>
    [Fact]
    public void Enabled_with_plaintext_transport_and_not_allow_insecure_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.None,
            AllowInsecure = false,
        };

        var result = Sut.Validate(null, options);

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(InsecureTransportFailure);
    }

    /// <summary>A TLS transport (<see cref="LdapTransport.Ldaps"/>) satisfies the guard.</summary>
    [Fact]
    public void Enabled_with_ldaps_transport_passes_guard()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 636,
            Transport = LdapTransport.Ldaps,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// Explicit opt-in: plaintext transport with <see cref="LdapOptions.AllowInsecure"/> set is
    /// permitted (dev/test escape hatch), so the guard does not trip.
    /// </summary>
    [Fact]
    public void Enabled_plaintext_with_allow_insecure_passes_guard()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.None,
            AllowInsecure = true,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// DevStubMode is exempt from the insecure-transport guard: the dev stub bypasses the real
    /// bind, so plaintext transport is irrelevant and must not block boot.
    /// </summary>
    [Fact]
    public void DevStubMode_with_plaintext_transport_passes_guard()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            DevStubMode = true,
            Transport = LdapTransport.None,
            AllowInsecure = false,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// A disabled config is exempt from the insecure-transport guard even with plaintext
    /// transport — LDAP login never runs, so the guard must not trip.
    /// </summary>
    [Fact]
    public void Disabled_with_plaintext_transport_passes_guard()
    {
        var options = new LdapOptions
        {
            Enabled = false,
            Transport = LdapTransport.None,
            AllowInsecure = false,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>When LDAP is disabled all checks are skipped, so a blank config still passes.</summary>
    [Fact]
    public void Disabled_options_succeed_even_when_blank()
    {
        var options = new LdapOptions
        {
            Enabled = false,
            Server = string.Empty,
            SearchBase = string.Empty,
            Port = 0,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// When the dev stub is active the real LDAP fields are irrelevant (the bind is bypassed), so
    /// the gate skips the Server/SearchBase/Port checks even though LDAP is nominally enabled.
    /// </summary>
    [Fact]
    public void DevStubMode_options_succeed_even_when_server_blank()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            DevStubMode = true,
            Server = string.Empty,
            SearchBase = string.Empty,
            Port = 0,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>Enabled with a blank server reports the required-server failure.</summary>
    [Fact]
    public void Enabled_with_blank_server_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = string.Empty,
            SearchBase = "dc=x",
            Port = 389,
        };

        var result = Sut.Validate(null, options);

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain("Ldap:Server is required when LDAP login is enabled.");
    }

    /// <summary>Enabled with a blank search base reports the required-search-base failure.</summary>
    [Fact]
    public void Enabled_with_blank_search_base_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = string.Empty,
            Port = 389,
        };

        var result = Sut.Validate(null, options);

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain("Ldap:SearchBase is required when LDAP login is enabled.");
    }

    /// <summary>Enabled with a non-positive <c>ConnectionTimeoutMs</c> fails validation (03/S2).</summary>
    [Fact]
    public void Enabled_with_nonpositive_ConnectionTimeoutMs_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.Ldaps,
            ConnectionTimeoutMs = 0,
        };

        Sut.Validate(null, options).Failed.ShouldBeTrue();
    }

    /// <summary>Enabled with a non-positive <c>AuthTimeoutMs</c> fails validation (03/S2).</summary>
    [Fact]
    public void Enabled_with_nonpositive_AuthTimeoutMs_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.Ldaps,
            AuthTimeoutMs = 0,
        };

        Sut.Validate(null, options).Failed.ShouldBeTrue();
    }

    /// <summary>Enabled with a non-positive <c>MaxConcurrentAuthentications</c> fails validation (03/S2).</summary>
    [Fact]
    public void Enabled_with_nonpositive_MaxConcurrentAuthentications_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.Ldaps,
            MaxConcurrentAuthentications = 0,
        };

        Sut.Validate(null, options).Failed.ShouldBeTrue();
    }

    /// <summary>A positive <c>OutageFailureThreshold</c> with a zero <c>OutageCooldownSeconds</c> fails —
    /// an open circuit with no cooldown would probe on every call (03/S2).</summary>
    [Fact]
    public void OutageFailureThreshold_positive_with_zero_cooldown_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.Ldaps,
            OutageFailureThreshold = 3,
            OutageCooldownSeconds = 0,
        };

        Sut.Validate(null, options).Failed.ShouldBeTrue();
    }

    /// <summary>A zero <c>OutageFailureThreshold</c> (circuit disabled) passes regardless of the
    /// cooldown value (03/S2).</summary>
    [Fact]
    public void OutageFailureThreshold_zero_disables_circuit_and_passes()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 389,
            Transport = LdapTransport.Ldaps,
            OutageFailureThreshold = 0,
            OutageCooldownSeconds = 0,
        };

        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    /// <summary>Enabled with port 0 reports the port-range failure using the shared primitive wording.</summary>
    [Fact]
    public void Enabled_with_zero_port_fails()
    {
        var options = new LdapOptions
        {
            Enabled = true,
            Server = "ldap",
            SearchBase = "dc=x",
            Port = 0,
        };

        var result = Sut.Validate(null, options);

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain("Ldap:Port must be between 1 and 65535 (was 0)");
    }
}
