using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Server.Security;

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

/// <summary>
///     Deterministic guards for Active Directory compatibility of the internal helpers
///     <see cref="LdapUserAuthenticator"/> relies on. We can't live-bind against AD in unit
///     tests — instead, we pin the behaviors AD depends on (DN-parsing of AD-style
///     <c>memberOf</c> values, filter escaping with case-preserving RDN extraction) so a
///     future refactor can't silently break the AD path while the GLAuth live-smoke stays
///     green.
/// </summary>
[Trait("Category", "Unit")]
public sealed class LdapUserAuthenticatorAdCompatTests
{
    [Fact]
    public void ExtractFirstRdnValue_parses_AD_memberOf_group_name_from_CN_dn()
    {
        // AD's memberOf values use uppercase CN=… and full domain paths. The extractor
        // returns the first RDN's value regardless of attribute-type case, so operators'
        // GroupToRole keys stay readable ("OPCUA-Operators" not "CN=OPCUA-Operators,...").
        var dn = "CN=OPCUA-Operators,OU=OPC UA Security Groups,OU=Groups,DC=corp,DC=example,DC=com";
        LdapUserAuthenticator.ExtractFirstRdnValue(dn).ShouldBe("OPCUA-Operators");
    }

    [Fact]
    public void ExtractFirstRdnValue_handles_mixed_case_and_spaces_in_group_name()
    {
        var dn = "CN=Domain Users,CN=Users,DC=corp,DC=example,DC=com";
        LdapUserAuthenticator.ExtractFirstRdnValue(dn).ShouldBe("Domain Users");
    }

    [Fact]
    public void ExtractFirstRdnValue_also_works_for_OpenLDAP_ou_style_memberOf()
    {
        // GLAuth + some OpenLDAP deployments expose memberOf as ou=<group>,ou=groups,...
        // The authenticator needs one extractor that tolerates both shapes since directories
        // in the field mix them depending on schema.
        var dn = "ou=WriteOperate,ou=groups,dc=lmxopcua,dc=local";
        LdapUserAuthenticator.ExtractFirstRdnValue(dn).ShouldBe("WriteOperate");
    }

    [Fact]
    public void EscapeLdapFilter_prevents_injection_via_samaccountname_lookup()
    {
        // AD login names can contain characters that are meaningful to LDAP filter syntax
        // (parens, backslashes). The authenticator builds filters as
        // ($"({UserNameAttribute}={EscapeLdapFilter(username)})") so injection attempts must
        // not break out of the filter. The RFC 4515 escape set is: \ → \5c, * → \2a, ( → \28,
        // ) → \29, \0 → \00.
        LdapUserAuthenticator.EscapeLdapFilter("admin)(cn=*")
            .ShouldBe("admin\\29\\28cn=\\2a");
        LdapUserAuthenticator.EscapeLdapFilter("domain\\user")
            .ShouldBe("domain\\5cuser");
    }

    [Fact]
    public void LdapOptions_default_UserNameAttribute_is_uid_for_rfc2307_compat()
    {
        // Regression guard: PR 31 introduced UserNameAttribute with a default of "uid" so
        // existing deployments (pre-AD config) keep working. Changing the default breaks
        // everyone's config silently; require an explicit review.
        new LdapOptions().UserNameAttribute.ShouldBe("uid");
    }
}
