using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Metrics;

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

public sealed class GatewayApplicationTests
{
    /// <summary>Verifies that Build maps the live health check endpoint.</summary>
    [Fact]
    public async Task Build_MapsLiveHealthEndpoint()
    {
        await using WebApplication app = GatewayApplication.Build([]);

        RouteEndpoint endpoint = Assert.Single(
            ((IEndpointRouteBuilder)app).DataSources
                .SelectMany(dataSource => dataSource.Endpoints)
                .OfType<RouteEndpoint>(),
            candidate => candidate.RoutePattern.RawText == "/health/live");

        Assert.Equal("LiveHealth", endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName);
    }

    /// <summary>Verifies that Build registers the gateway metrics service.</summary>
    [Fact]
    public async Task Build_RegistersGatewayMetrics()
    {
        await using WebApplication app = GatewayApplication.Build([]);

        GatewayMetrics metrics = app.Services.GetRequiredService<GatewayMetrics>();

        Assert.NotNull(metrics);
    }

    /// <summary>Verifies that Build maps dashboard and authentication endpoints when the dashboard is enabled.</summary>
    [Fact]
    public async Task Build_WhenDashboardEnabled_MapsBlazorDashboardAndAuthEndpoints()
    {
        await using WebApplication app = GatewayApplication.Build([]);
        IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);

        Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/");
        Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/sessions");
        Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/workers");
        Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/events");
        Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/settings");
        Assert.Contains(endpoints, endpoint =>
            endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogin");
        Assert.Contains(endpoints, endpoint =>
            endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogout");
    }

    /// <summary>Verifies that the dashboard login, logout, and denied endpoints allow anonymous access.</summary>
    [Fact]
    public async Task Build_WhenDashboardEnabled_AuthEndpointsAllowAnonymousAccess()
    {
        await using WebApplication app = GatewayApplication.Build([]);
        IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);

        string[] anonymousEndpointNames =
            ["DashboardLogin", "DashboardLoginPost", "DashboardLogout", "DashboardAccessDenied"];
        foreach (string endpointName in anonymousEndpointNames)
        {
            RouteEndpoint endpoint = Assert.Single(
                endpoints,
                candidate => candidate.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == endpointName);

            Assert.NotNull(endpoint.Metadata.GetMetadata<IAllowAnonymous>());
        }
    }

    /// <summary>Verifies that dashboard Razor component routes require the dashboard viewer policy.</summary>
    [Fact]
    public async Task Build_WhenDashboardEnabled_ComponentRoutesRequireAuthorization()
    {
        await using WebApplication app = GatewayApplication.Build([]);
        IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);

        // Razor-component endpoints are distinguished from minimal-API
        // endpoints registered at the same path by the presence of
        // ComponentTypeMetadata. Filter to those before checking auth.
        string[] componentRoutes = ["/", "/sessions", "/workers", "/events", "/settings"];
        foreach (string route in componentRoutes)
        {
            RouteEndpoint[] matches = endpoints
                .Where(endpoint => endpoint.RoutePattern.RawText == route
                    && endpoint.Metadata.GetMetadata<Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata>() is not null)
                .ToArray();

            Assert.NotEmpty(matches);
            Assert.All(matches, endpoint =>
            {
                IAuthorizeData? authorize = endpoint.Metadata.GetMetadata<IAuthorizeData>();
                Assert.NotNull(authorize);
                Assert.Equal(DashboardAuthenticationDefaults.ViewerPolicy, authorize.Policy);
            });
        }
    }

    [Fact]
    public async Task Build_WhenDashboardEnabled_RegistersDashboardRoutesAtRoot()
    {
        await using WebApplication app = GatewayApplication.Build([]);
        IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);

        string[] canonicalRoutes =
        [
            "/",
            "/sessions",
            "/workers",
            "/events",
            "/settings",
            "/galaxy",
            "/apikeys",
            "/sessions/{SessionId}",
        ];
        foreach (string canonical in canonicalRoutes)
        {
            Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == canonical
                && endpoint.Metadata.GetMetadata<Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata>() is not null);
        }
    }

    [Fact]
    public async Task Build_WhenDashboardDisabled_DoesNotMapDashboardRoutes()
    {
        await using WebApplication app = GatewayApplication.Build(["--MxGateway:Dashboard:Enabled=false"]);
        IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);

        Assert.DoesNotContain(endpoints, endpoint =>
            endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName?.StartsWith(
                "Dashboard",
                StringComparison.Ordinal) == true);
    }

    /// <summary>Verifies that StartAsync fails when gateway configuration is invalid.</summary>
    /// <param name="key">Configuration key to override.</param>
    /// <param name="value">Invalid configuration value.</param>
    /// <param name="expectedFailure">Expected validation error message.</param>
    [Theory]
    [InlineData(
        "MxGateway:Worker:ExecutablePath",
        "worker.dll",
        "MxGateway:Worker:ExecutablePath must point to a .exe file.")]
    [InlineData(
        "MxGateway:Events:QueueCapacity",
        "0",
        "MxGateway:Events:QueueCapacity must be greater than zero.")]
    [InlineData(
        "MxGateway:Authentication:PepperSecretName",
        "",
        "MxGateway:Authentication:PepperSecretName is required")]
    [InlineData(
        "MxGateway:Dashboard:GroupToRole:GwAdmin",
        "BogusRole",
        "MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Admin' or 'Viewer'.")]
    [InlineData(
        "MxGateway:Ldap:AllowInsecureLdap",
        "false",
        "MxGateway:Ldap:AllowInsecureLdap must be true when UseTls is false.")]
    public async Task StartAsync_InvalidGatewayConfiguration_FailsStartup(
        string key,
        string value,
        string expectedFailure)
    {
        // Bind an ephemeral port (:0) — xUnit runs test collections in parallel, so any
        // WebApplication-building test must avoid a fixed port to prevent a bind collision.
        await using WebApplication app = GatewayApplication.Build(
            [$"--{key}={value}", "--urls=http://127.0.0.1:0"]);

        OptionsValidationException exception = await Assert.ThrowsAsync<OptionsValidationException>(
            () => app.StartAsync());

        Assert.Contains(
            exception.Failures,
            failure => failure.Contains(expectedFailure, StringComparison.Ordinal));
    }

    private static IReadOnlyList<RouteEndpoint> GetRouteEndpoints(WebApplication app)
    {
        return ((IEndpointRouteBuilder)app).DataSources
            .SelectMany(dataSource => dataSource.Endpoints)
            .OfType<RouteEndpoint>()
            .ToArray();
    }
}
