using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Nexus.Api.Controllers;
using Nexus.Api.Extensions;
using Nexus.Api.Middleware;
using Nexus.Api.Security;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class SecurityBoundaryTests
{
    [Fact]
    public void Authorization_UsesAuthenticatedFallbackPolicy()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Jwt:Key"] = new string('k', 48),
                ["Jwt:Issuer"] = "nexus-test",
                ["Jwt:Audience"] = "nexus-test"
            })
            .Build();
        var services = new ServiceCollection();

        services.AddNexusAuth(configuration);

        using var provider = services.BuildServiceProvider();
        var options = provider.GetRequiredService<IOptions<AuthorizationOptions>>().Value;
        Assert.NotNull(options.FallbackPolicy);
        Assert.Contains(
            options.FallbackPolicy!.Requirements,
            requirement => requirement is DenyAnonymousAuthorizationRequirement);
    }

    [Fact]
    public void DomainControllers_DoNotOptOutOfAuthentication()
    {
        Type[] domainControllers =
        [
            typeof(ActivityController),
            typeof(AgentsController),
            typeof(CalendarController),
            typeof(DocsController),
            typeof(GatewayBridgeController),
            typeof(IncidentsController),
            typeof(MemoryController),
            typeof(RoutingController),
            typeof(TeamController)
        ];

        foreach (var controller in domainControllers)
        {
            Assert.Empty(controller.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true));
            Assert.DoesNotContain(
                controller.GetMethods(),
                method => method.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true).Length > 0);
        }
    }

    [Fact]
    public void TaskAutomationEndpoints_DoNotOptOutOfAuthentication()
    {
        var board = typeof(TasksController).GetMethod(nameof(TasksController.GetBoard));
        var reset = typeof(TasksController).GetMethod(nameof(TasksController.ResetStale));

        Assert.NotNull(board);
        Assert.NotNull(reset);
        Assert.Empty(board!.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true));
        Assert.Empty(reset!.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true));
    }

    [Fact]
    public void AgentCommand_RequiresOwnerRole()
    {
        var command = typeof(AgentsController).GetMethod(nameof(AgentsController.SendCommand));
        Assert.NotNull(command);

        var authorize = Assert.Single(
            command!.GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true)
                .OfType<AuthorizeAttribute>());
        Assert.Equal("owner", authorize.Roles);
    }

    [Theory]
    [InlineData(nameof(AuthController.Login))]
    [InlineData(nameof(AuthController.Refresh))]
    [InlineData(nameof(AuthController.Logout))]
    [InlineData(nameof(AuthController.AdminResetPassword))]
    public void PublicAuthBootstrapEndpoints_AreExplicitlyAnonymous(string methodName)
    {
        var method = typeof(AuthController).GetMethod(methodName);
        Assert.NotNull(method);
        Assert.NotEmpty(method!.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true));
    }

    [Theory]
    [InlineData(nameof(HealthController.Live))]
    [InlineData(nameof(HealthController.Ready))]
    [InlineData(nameof(HealthController.Get))]
    public void PublicHealthEndpoints_AreExplicitlyAnonymous(string methodName)
    {
        var method = typeof(HealthController).GetMethod(methodName);
        Assert.NotNull(method);
        Assert.NotEmpty(method!.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true));
    }

    [Theory]
    [InlineData(nameof(AuthController.GetMe))]
    [InlineData(nameof(AuthController.UpdateProfile))]
    [InlineData(nameof(AuthController.ChangePassword))]
    public void AccountEndpoints_InheritAuthenticatedFallback(string methodName)
    {
        var method = typeof(AuthController).GetMethod(methodName);
        Assert.NotNull(method);
        Assert.Empty(method!.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true));
    }

    [Fact]
    public void CookieBackedAuth_AllowsSameOriginBrowserRequests()
    {
        var context = new DefaultHttpContext();
        context.Request.Scheme = "https";
        context.Request.Host = new HostString("nexus.noveria.net");
        context.Request.Headers.Origin = "https://nexus.noveria.net";
        context.Request.Headers["Sec-Fetch-Site"] = "same-origin";

        Assert.True(BrowserRequestOriginGuard.IsAllowed(context.Request));
    }

    [Theory]
    [InlineData("https://attacker.invalid", "cross-site")]
    [InlineData("https://attacker.invalid", "")]
    [InlineData("not-an-origin", "same-origin")]
    public void CookieBackedAuth_RejectsExplicitCrossSiteRequests(string origin, string fetchSite)
    {
        var context = new DefaultHttpContext();
        context.Request.Scheme = "https";
        context.Request.Host = new HostString("nexus.noveria.net");
        context.Request.Headers.Origin = origin;
        if (fetchSite.Length > 0)
            context.Request.Headers["Sec-Fetch-Site"] = fetchSite;

        Assert.False(BrowserRequestOriginGuard.IsAllowed(context.Request));
    }

    [Fact]
    public async Task AgentIdentityHeader_IsRejectedWithoutVerifiedAuthentication()
    {
        await using var fixture = await TaskWorkflowFixture.CreateAsync();
        var context = TaskWorkflowFixture.CreateHttpContext(agentId: "iris");

        var resolution = await RequestAuthorizationHelper.ResolveAgentHeaderAsync(
            context,
            fixture.AgentService,
            fixture.Configuration,
            CancellationToken.None);

        Assert.Null(resolution.AgentId);
        Assert.True(resolution.HeaderProvided);
        Assert.True(resolution.IsRecognized);
        Assert.False(resolution.CredentialVerified);
        Assert.False(resolution.IdentityHintAuthorized);
    }

    [Fact]
    public async Task AgentIdentityHeader_IsAcceptedAsHintAfterVerifiedServiceKey()
    {
        await using var fixture = await TaskWorkflowFixture.CreateAsync();
        var context = TaskWorkflowFixture.CreateHttpContext(headers: new Dictionary<string, string>
        {
            ["X-Agent-Id"] = "iris",
            ["X-Nexus-Api-Key"] = "test-service-key"
        });

        var resolution = await RequestAuthorizationHelper.ResolveAgentHeaderAsync(
            context,
            fixture.AgentService,
            fixture.Configuration,
            CancellationToken.None);

        Assert.Equal("iris", resolution.AgentId);
        Assert.True(resolution.CredentialVerified);
        Assert.True(resolution.IdentityHintAuthorized);
    }

    [Fact]
    public async Task AgentIdentityHeader_CannotEscalateAnOrdinaryJwtUser()
    {
        await using var fixture = await TaskWorkflowFixture.CreateAsync();
        var context = TaskWorkflowFixture.CreateHttpContext(
            agentId: "iris",
            user: TaskWorkflowFixture.CreateUser("ordinary-user", "user"));

        var resolution = await RequestAuthorizationHelper.ResolveAgentHeaderAsync(
            context,
            fixture.AgentService,
            fixture.Configuration,
            CancellationToken.None);

        Assert.Null(resolution.AgentId);
        Assert.True(resolution.CredentialVerified);
        Assert.False(resolution.IdentityHintAuthorized);
    }

    [Fact]
    public async Task ApiKeyMiddleware_AuthenticatesMcpRequests()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["NexusApiKey"] = "service-secret"
            })
            .Build();
        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(configuration);
        using var provider = services.BuildServiceProvider();
        var context = new DefaultHttpContext { RequestServices = provider };
        context.Request.Path = "/mcp";
        context.Request.Headers["X-Nexus-Api-Key"] = "service-secret";
        var nextWasCalled = false;
        var middleware = new ApiKeyMiddleware(nextContext =>
        {
            nextWasCalled = true;
            Assert.True(nextContext.User.Identity?.IsAuthenticated);
            Assert.True(nextContext.User.IsInRole("Service"));
            return Task.CompletedTask;
        });

        await middleware.InvokeAsync(context);

        Assert.True(nextWasCalled);
    }

    [Fact]
    public async Task ApiKeyMiddleware_DoesNotAuthenticateMcpFromAgentHeaderAlone()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["NexusApiKey"] = "service-secret"
            })
            .Build();
        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(configuration);
        using var provider = services.BuildServiceProvider();
        var context = new DefaultHttpContext { RequestServices = provider };
        context.Request.Path = "/mcp";
        context.Request.Headers["X-Agent-Id"] = "iris";
        var middleware = new ApiKeyMiddleware(nextContext =>
        {
            Assert.False(nextContext.User.Identity?.IsAuthenticated);
            return Task.CompletedTask;
        });

        await middleware.InvokeAsync(context);
    }

    [Fact]
    public void ForwardedHeaders_TrustOnlyDefaultsAndConfiguredProxyRanges()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ForwardedHeaders:ForwardLimit"] = "2",
                ["ForwardedHeaders:KnownProxies:0"] = "10.10.0.12",
                ["ForwardedHeaders:KnownNetworks:0"] = "10.20.0.0/24"
            })
            .Build();
        var services = new ServiceCollection();
        services.AddNexusForwardedHeaders(configuration);

        using var provider = services.BuildServiceProvider();
        var options = provider.GetRequiredService<IOptions<ForwardedHeadersOptions>>().Value;

        Assert.Equal(2, options.ForwardLimit);
        Assert.Contains(IPAddress.Parse("10.10.0.12"), options.KnownProxies);
        Assert.Contains(options.KnownIPNetworks, network => network.ToString() == "10.20.0.0/24");
        Assert.DoesNotContain(options.KnownIPNetworks, network => network.ToString() == "0.0.0.0/0");
    }
}
