using System.Net;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;

public sealed class DashboardAuthorizationHandlerTests
{
    /// <summary>Verifies that unauthenticated remote requests fail authorization.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_UnauthenticatedRemoteRequest_DoesNotSucceed()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            IPAddress.Parse("10.0.0.5"),
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AnyDashboardRole);

        Assert.False(context.HasSucceeded);
    }

    /// <summary>Verifies that anonymous localhost access succeeds when allowed.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_AnonymousLocalhostAllowed_Succeeds()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            IPAddress.Loopback,
            allowAnonymousLocalhost: true,
            DashboardAuthorizationRequirement.AnyDashboardRole);

        Assert.True(context.HasSucceeded);
    }

    /// <summary>
    /// Verifies that the anonymous-localhost bypass is denied when <c>AllowAnonymousLocalhost</c>
    /// is off, even on a loopback connection — the misconfiguration must not expose the dashboard.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_AnonymousLocalhostDisallowed_DoesNotSucceed()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            IPAddress.Loopback,
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AnyDashboardRole);

        Assert.False(context.HasSucceeded);
    }

    /// <summary>
    /// Verifies that the anonymous-localhost bypass stays scoped to loopback: an anonymous
    /// request from a non-loopback address is denied even when <c>AllowAnonymousLocalhost</c> is on.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_AnonymousLocalhostAllowedFromRemoteAddress_DoesNotSucceed()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            IPAddress.Parse("10.0.0.5"),
            allowAnonymousLocalhost: true,
            DashboardAuthorizationRequirement.AnyDashboardRole);

        Assert.False(context.HasSucceeded);
    }

    /// <summary>Verifies that an authenticated user without any dashboard role fails the viewer requirement.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_AuthenticatedWithoutDashboardRole_DoesNotSucceed()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            CreatePrincipal("SomeOtherRole"),
            IPAddress.Loopback,
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AnyDashboardRole);

        Assert.False(context.HasSucceeded);
    }

    /// <summary>Verifies that a Viewer satisfies the viewer-or-admin requirement.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_ViewerRole_SatisfiesViewerPolicy()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            CreatePrincipal(DashboardRoles.Viewer),
            IPAddress.Parse("10.0.0.5"),
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AnyDashboardRole);

        Assert.True(context.HasSucceeded);
    }

    /// <summary>Verifies that an Admin satisfies the admin-only requirement.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_AdminRole_SatisfiesAdminPolicy()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            CreatePrincipal(DashboardRoles.Admin),
            IPAddress.Parse("10.0.0.5"),
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AdminOnly);

        Assert.True(context.HasSucceeded);
    }

    /// <summary>Verifies that a Viewer does NOT satisfy the admin-only requirement.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_ViewerRole_DoesNotSatisfyAdminPolicy()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            CreatePrincipal(DashboardRoles.Viewer),
            IPAddress.Parse("10.0.0.5"),
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AdminOnly);

        Assert.False(context.HasSucceeded);
    }

    /// <summary>
    /// Verifies that the anonymous-localhost bypass is read-only: a loopback request with
    /// <c>AllowAnonymousLocalhost</c> on is denied the <see cref="DashboardAuthorizationRequirement.AdminOnly"/>
    /// requirement, so destructive/admin surfaces are never reachable without a real Admin role.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_AnonymousLocalhostAllowed_DoesNotSatisfyAdminPolicy()
    {
        AuthorizationHandlerContext context = await AuthorizeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            IPAddress.Loopback,
            allowAnonymousLocalhost: true,
            DashboardAuthorizationRequirement.AdminOnly);

        Assert.False(context.HasSucceeded);
    }

    /// <summary>
    /// Verifies that with authentication fully disabled the environment bypass stays read-only:
    /// a remote request satisfies <see cref="DashboardAuthorizationRequirement.AnyDashboardRole"/>
    /// (Viewer) but is denied <see cref="DashboardAuthorizationRequirement.AdminOnly"/>.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task HandleAsync_AuthenticationDisabled_GrantsViewerButNotAdmin()
    {
        AuthorizationHandlerContext viewerContext = await AuthorizeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            IPAddress.Parse("10.0.0.5"),
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AnyDashboardRole,
            authenticationMode: AuthenticationMode.Disabled);

        Assert.True(viewerContext.HasSucceeded);

        AuthorizationHandlerContext adminContext = await AuthorizeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            IPAddress.Parse("10.0.0.5"),
            allowAnonymousLocalhost: false,
            DashboardAuthorizationRequirement.AdminOnly,
            authenticationMode: AuthenticationMode.Disabled);

        Assert.False(adminContext.HasSucceeded);
    }

    private static async Task<AuthorizationHandlerContext> AuthorizeAsync(
        ClaimsPrincipal principal,
        IPAddress remoteAddress,
        bool allowAnonymousLocalhost,
        DashboardAuthorizationRequirement requirement,
        AuthenticationMode authenticationMode = AuthenticationMode.ApiKey)
    {
        DefaultHttpContext httpContext = new();
        httpContext.Connection.RemoteIpAddress = remoteAddress;
        DashboardAuthorizationHandler handler = new(
            new HttpContextAccessor { HttpContext = httpContext },
            Options.Create(new GatewayOptions
            {
                Authentication = new AuthenticationOptions
                {
                    Mode = authenticationMode,
                },
                Dashboard = new DashboardOptions
                {
                    AllowAnonymousLocalhost = allowAnonymousLocalhost,
                }
            }));
        AuthorizationHandlerContext context = new([requirement], principal, httpContext);

        await handler.HandleAsync(context);

        return context;
    }

    private static ClaimsPrincipal CreatePrincipal(string role)
    {
        ClaimsIdentity identity = new(
            [new Claim(ClaimTypes.Role, role)],
            DashboardAuthenticationDefaults.AuthenticationScheme,
            ClaimTypes.Name,
            ClaimTypes.Role);

        return new ClaimsPrincipal(identity);
    }
}
