using Microsoft.Extensions.Configuration;
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>
/// Regression guard for the LDAP config-section fix. The real config (admin/driver/Development
/// overlays) lives under <c>Security:Ldap</c>, and <see cref="LdapOptions.SectionName"/> must point
/// there so the configured <c>DevStubMode</c> actually binds. Previously the binders used the
/// nonexistent <c>"Ldap"</c>/<c>"Authentication:Ldap"</c> sections, so the dev stub never activated.
/// </summary>
public sealed class LdapOptionsBindingTests
{
    /// <summary><see cref="LdapOptions.SectionName"/> resolves to the real overlay section.</summary>
    [Fact]
    public void SectionName_is_Security_Ldap()
    {
        LdapOptions.SectionName.ShouldBe("Security:Ldap");
    }

    /// <summary>
    /// Binding from <see cref="LdapOptions.SectionName"/> reads the configured <c>DevStubMode</c>
    /// from the real <c>Security:Ldap</c> section — proving the dev stub now takes effect.
    /// </summary>
    [Fact]
    public void Binding_from_SectionName_reads_Security_Ldap_DevStubMode()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Security:Ldap:DevStubMode"] = "true",
            })
            .Build();

        var options = configuration.GetSection(LdapOptions.SectionName).Get<LdapOptions>();

        options.ShouldNotBeNull();
        options.DevStubMode.ShouldBeTrue();
    }

    /// <summary>
    /// Negative control: binding from the old (nonexistent) <c>"Ldap"</c> section against the same
    /// <c>Security:Ldap</c> config does NOT pick up <c>DevStubMode</c> — it falls back to the C#
    /// default (false). This is the pre-fix behaviour the change corrects.
    /// </summary>
    [Fact]
    public void Binding_from_old_Ldap_section_does_not_read_DevStubMode()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Security:Ldap:DevStubMode"] = "true",
            })
            .Build();

        var options = configuration.GetSection("Ldap").Get<LdapOptions>() ?? new LdapOptions();

        options.DevStubMode.ShouldBeFalse();
    }

    /// <summary>
    /// The five S2 resilience keys bind from the real <c>Security:Ldap</c> section (03/S2).
    /// </summary>
    [Fact]
    public void Binding_reads_resilience_keys()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Security:Ldap:ConnectionTimeoutMs"] = "7000",
                ["Security:Ldap:AuthTimeoutMs"] = "12000",
                ["Security:Ldap:MaxConcurrentAuthentications"] = "5",
                ["Security:Ldap:OutageFailureThreshold"] = "4",
                ["Security:Ldap:OutageCooldownSeconds"] = "20",
            })
            .Build();

        var options = configuration.GetSection(LdapOptions.SectionName).Get<LdapOptions>();

        options.ShouldNotBeNull();
        options.ConnectionTimeoutMs.ShouldBe(7000);
        options.AuthTimeoutMs.ShouldBe(12000);
        options.MaxConcurrentAuthentications.ShouldBe(5);
        options.OutageFailureThreshold.ShouldBe(4);
        options.OutageCooldownSeconds.ShouldBe(20);
    }

    /// <summary>The resilience keys carry the documented defaults when absent from config.</summary>
    [Fact]
    public void Resilience_key_defaults_hold_when_absent()
    {
        var options = new LdapOptions();

        options.ConnectionTimeoutMs.ShouldBe(10000);
        options.AuthTimeoutMs.ShouldBe(15000);
        options.MaxConcurrentAuthentications.ShouldBe(8);
        options.OutageFailureThreshold.ShouldBe(3);
        options.OutageCooldownSeconds.ShouldBe(15);
    }

    /// <summary><c>ConnectionTimeoutMs</c> is projected onto the shared-library options so the
    /// socket-connect + per-search timeout is bounded inside the directory client.</summary>
    [Fact]
    public void ToLibraryOptions_projects_ConnectionTimeoutMs()
    {
        var options = new LdapOptions { ConnectionTimeoutMs = 4321 };

        options.ToLibraryOptions().ConnectionTimeoutMs.ShouldBe(4321);
    }
}

/// <summary>
/// End-to-end guard for the shipped production overlays: binds each of the three prod overlay
/// files' real <c>Security:Ldap</c> section (the same files the host loads at boot, copied into the
/// test output via the Host project reference) and runs the <see cref="LdapOptionsValidator"/> the
/// host wires via <c>AddValidatedOptions</c>. Proves each prod overlay declares a TLS transport and
/// therefore PASSES startup validation — i.e. the host actually boots with these overlays after the
/// insecure-transport guard was added. The <c>Development</c> overlay (DevStubMode) is verified to
/// pass via the guard exemption.
/// </summary>
public sealed class ProdOverlayValidationTests
{
    private static readonly LdapOptionsValidator Sut = new();

    private static LdapOptions BindOverlay(string fileName)
    {
        var path = Path.Combine(AppContext.BaseDirectory, fileName);
        File.Exists(path).ShouldBeTrue($"overlay '{fileName}' should be copied to the test output");

        var configuration = new ConfigurationBuilder()
            .AddJsonFile(path, optional: false, reloadOnChange: false)
            .Build();

        return configuration.GetSection(LdapOptions.SectionName).Get<LdapOptions>() ?? new LdapOptions();
    }

    [Theory]
    [InlineData("appsettings.admin.json")]
    [InlineData("appsettings.driver.json")]
    [InlineData("appsettings.admin-driver.json")]
    public void Prod_overlay_declares_ldaps_transport(string fileName)
    {
        var options = BindOverlay(fileName);

        options.DevStubMode.ShouldBeFalse();
        options.Transport.ShouldBe(LdapTransport.Ldaps);
    }

    [Theory]
    [InlineData("appsettings.admin.json")]
    [InlineData("appsettings.driver.json")]
    [InlineData("appsettings.admin-driver.json")]
    public void Prod_overlay_passes_startup_validation(string fileName)
    {
        var options = BindOverlay(fileName);

        // Match the host: these overlays only set Security:Ldap fields, so backfill the required
        // Server/SearchBase/Port the way the base C# defaults do (LdapOptions defaults are valid),
        // then validate exactly as AddValidatedOptions would at boot.
        Sut.Validate(null, options).Succeeded.ShouldBeTrue();
    }

    [Fact]
    public void Development_overlay_passes_startup_validation_via_devstub_exemption()
    {
        var options = BindOverlay("appsettings.Development.json");

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