using System.Net.Security;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.Auth.Ldap;

namespace ZB.MOM.WW.Auth.Ldap.Tests;

public class LdapAuthServiceTests
{
    // Sensible test defaults: insecure plaintext transport (dev/test), a service
    // account set, and DisplayNameAttribute aligned with the fake's "displayName"
    // key so display-name extraction is genuinely exercised.
    private static LdapOptions Opts() => new()
    {
        Enabled = true,
        Server = "x",
        Port = 3893,
        Transport = LdapTransport.None,
        AllowInsecure = true,
        SearchBase = "dc=x",
        ServiceAccountDn = "cn=svc,dc=x",
        ServiceAccountPassword = "svcpw",
        UserNameAttribute = "cn",
        DisplayNameAttribute = "displayName",
        GroupAttribute = "memberOf",
    };

    [Fact]
    public async Task Succeeds_AndReturnsStrippedGroups_OnValidCredentials()
    {
        var fake = new FakeLdapConnection().WithUserEntry(
            "cn=alice,dc=x",
            memberOf: new[] { "cn=Engineers,ou=g,dc=x", "cn=Viewers,ou=g,dc=x" },
            displayName: "Alice");
        var svc = new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake));

        var r = await svc.AuthenticateAsync("  alice ", "pw", default);

        Assert.True(r.Succeeded);
        Assert.Equal("alice", r.Username);                       // trimmed
        Assert.Equal("Alice", r.DisplayName);                    // from DisplayNameAttribute
        Assert.Equal(new[] { "Engineers", "Viewers" }, r.Groups); // CN= stripped
    }

    [Fact]
    public async Task BindsServiceAccountThenUser_OnValidCredentials()
    {
        // Non-empty memberOf: fail-closed requires at least one group for success, and this
        // test asserts bind ORDER, so the user must successfully resolve and bind.
        var fake = new FakeLdapConnection().WithUserEntry(
            "cn=alice,dc=x", memberOf: new[] { "cn=Engineers,ou=g,dc=x" }, displayName: "Alice");
        var svc = new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake));

        await svc.AuthenticateAsync("alice", "pw", default);

        // Service account first, user DN second (bind-then-search-then-bind).
        Assert.Equal(new[] { "cn=svc,dc=x", "cn=alice,dc=x" }, fake.BoundDns);
    }

    [Fact]
    public async Task FallsBackToUsername_WhenNoDisplayName()
    {
        // Non-empty memberOf so fail-closed lets success through; this test only asserts the
        // display-name fallback (no displayName attribute -> username).
        var fake = new FakeLdapConnection().WithUserEntry(
            "cn=bob,dc=x", memberOf: new[] { "cn=Viewers,ou=g,dc=x" });
        var svc = new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake));

        var r = await svc.AuthenticateAsync("bob", "pw", default);

        Assert.True(r.Succeeded);
        Assert.Equal("bob", r.DisplayName);
    }

    [Fact]
    public async Task Fails_Disabled_WhenNotEnabled()
    {
        var svc = new LdapAuthService(
            Opts() with { Enabled = false },
            new FakeLdapConnectionFactory(new FakeLdapConnection()));

        Assert.Equal(LdapAuthFailure.Disabled, (await svc.AuthenticateAsync("a", "b", default)).Failure);
    }

    // --- Auth-006: TLS validation seam — allowInsecure is honoured and a cert-validation
    //     callback is threaded into the connection rather than being silently ignored. ---

    [Fact]
    public async Task Connect_ReceivesAllowInsecureFlag_FromOptions()
    {
        // The allowInsecure flag must reach the connection (it used to be an unused parameter).
        var fake = new FakeLdapConnection().WithUserEntry(
            "cn=alice,dc=x", memberOf: new[] { "cn=Engineers,ou=g,dc=x" });
        var svc = new LdapAuthService(
            Opts() with { AllowInsecure = true }, new FakeLdapConnectionFactory(fake));

        await svc.AuthenticateAsync("alice", "pw", default);

        Assert.NotNull(fake.ConnectArgs);
        Assert.True(fake.ConnectArgs!.Value.AllowInsecure);
    }

    [Fact]
    public async Task Connect_ReceivesConfiguredCertValidationCallback()
    {
        // A consumer-supplied RemoteCertificateValidationCallback must be passed through to the
        // connection so production callers can pin a CA / validate the SAN — the seam no longer
        // discards it.
        RemoteCertificateValidationCallback callback = (_, _, _, _) => true;
        var fake = new FakeLdapConnection().WithUserEntry(
            "cn=alice,dc=x", memberOf: new[] { "cn=Engineers,ou=g,dc=x" });
        var svc = new LdapAuthService(
            Opts() with { ServerCertificateValidationCallback = callback },
            new FakeLdapConnectionFactory(fake));

        await svc.AuthenticateAsync("alice", "pw", default);

        Assert.Same(callback, fake.ConnectCertCallback);
    }

    [Fact]
    public async Task Connect_NoCertCallbackConfigured_PassesNull()
    {
        // Default: no callback configured -> null reaches the connection, which means the
        // production adapter falls back to OS-trust-store validation (documented behaviour).
        var fake = new FakeLdapConnection().WithUserEntry(
            "cn=alice,dc=x", memberOf: new[] { "cn=Engineers,ou=g,dc=x" });
        var svc = new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake));

        await svc.AuthenticateAsync("alice", "pw", default);

        Assert.Null(fake.ConnectCertCallback);
    }

    [Fact]
    public async Task PreservesEscapedCommaInGroupName_OnRfc4514Dn()
    {
        // C1: a group CN that legitimately contains a comma (escaped per RFC 4514)
        // must be returned intact, not truncated at the escaped comma.
        var fake = new FakeLdapConnection().WithUserEntry(
            "cn=alice,dc=x",
            memberOf: new[] { @"cn=Eng\,ineers,ou=g,dc=x", @"cn=A\2cB,dc=x" },
            displayName: "Alice");
        var svc = new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake));

        var r = await svc.AuthenticateAsync("alice", "pw", default);

        Assert.True(r.Succeeded);
        Assert.Equal(new[] { "Eng,ineers", "A,B" }, r.Groups);
    }
}
