using System.Security.Claims;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;

namespace ZB.MOM.WW.MxGateway.IntegrationTests;

[Collection(LiveResourcesCollection.Name)]
[Trait("Category", "LiveLdap")]
public sealed class DashboardLdapLiveTests
{
    [LiveLdapFact]
    public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds()
    {
        DashboardAuthenticator authenticator = CreateAuthenticator();

        DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
            "admin",
            "admin123",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.NotNull(result.Principal);
        Assert.Equal("admin", result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value);
        Assert.Contains(result.Principal.Claims, claim =>
            claim.Type == DashboardAuthenticationDefaults.LdapGroupClaimType
            && claim.Value.Contains("GwAdmin", StringComparison.OrdinalIgnoreCase));

        // IntegrationTests-023: DashboardAuthenticator.CreatePrincipal emits a
        // ClaimTypes.Role claim derived from MapGroupsToRoles. The seeded
        // GroupToRole map (GwAdmin -> Admin) means the admin principal must
        // carry Role=Admin alongside the raw LDAP-group claim. A regression in
        // MapGroupsToRoles (returning an empty list, missing the RDN fallback)
        // would silently pass without this assertion.
        Assert.Contains(result.Principal.Claims, claim =>
            claim.Type == ClaimTypes.Role
            && claim.Value == DashboardRoles.Admin);
    }

    [LiveLdapFact]
    public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails()
    {
        DashboardAuthenticator authenticator = CreateAuthenticator();

        DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
            "readonly",
            "readonly123",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Null(result.Principal);
        Assert.DoesNotContain("readonly123", result.FailureMessage, StringComparison.Ordinal);
    }

    [LiveLdapFact]
    public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword()
    {
        // Exercises the LdapException branch: the user exists and the service
        // account search succeeds, but the candidate bind is rejected.
        const string wrongPassword = "definitely-not-the-admin-password";
        DashboardAuthenticator authenticator = CreateAuthenticator();

        DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
            "admin",
            wrongPassword,
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Null(result.Principal);
        Assert.DoesNotContain(wrongPassword, result.FailureMessage, StringComparison.Ordinal);
    }

    [LiveLdapFact]
    public async Task AuthenticateAsync_UnknownUsername_Fails()
    {
        // Exercises the `candidate is null` branch: the service-account search
        // returns no entry, so no candidate bind is attempted.
        DashboardAuthenticator authenticator = CreateAuthenticator();

        DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
            "no-such-user-9f3c1",
            "irrelevant-password",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Null(result.Principal);
    }

    [LiveLdapFact]
    public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing()
    {
        // Exercises the connect-failure path: a closed loopback port produces a
        // connection error that DashboardAuthenticator must absorb into a Fail
        // result rather than propagating an exception to the dashboard.
        DashboardAuthenticator authenticator = new(
            Options.Create(new GatewayOptions
            {
                Ldap = new LdapOptions
                {
                    // 1 is a reserved port number that no LDAP server listens on.
                    Port = 1,
                },
            }),
            NullLogger<DashboardAuthenticator>.Instance);

        DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
            "admin",
            "admin123",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Null(result.Principal);
    }

    private static DashboardAuthenticator CreateAuthenticator()
    {
        return new DashboardAuthenticator(
            Options.Create(new GatewayOptions
            {
                Dashboard = new DashboardOptions
                {
                    GroupToRole = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        ["GwAdmin"] = DashboardRoles.Admin,
                    },
                },
            }),
            NullLogger<DashboardAuthenticator>.Instance);
    }
}
