using MemberCenter.Infrastructure.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace MemberCenter.Security.Tests;

public sealed class SecurityConfigurationTests
{
    [Theory]
    [InlineData("https://example.com/app", true)]
    [InlineData("https://example.com/app/callback?code=1", true)]
    [InlineData("https://example.com/application", false)]
    [InlineData("https://example.com.attacker.test/app", false)]
    [InlineData("https://example.com:444/app", false)]
    [InlineData("https://user@example.com/app", false)]
    public void ReturnUrlRequiresMatchingOriginAndPathBoundary(string candidate, bool expected)
    {
        var result = ReturnUrlValidator.IsWithinAllowedPrefix(
            new Uri(candidate), "https://example.com/app", allowInsecureHttp: false);

        Assert.Equal(expected, result);
    }

    [Fact]
    public void ReturnUrlRequiresHttpsWhenInsecureHttpIsDisabled()
    {
        Assert.False(ReturnUrlValidator.IsWithinAllowedPrefix(
            new Uri("http://example.com/app"), "http://example.com/app", allowInsecureHttp: false));
    }

    [Fact]
    public void ForwardedHeadersFailClosedWithoutTrustedProxy()
    {
        var options = new ForwardedHeadersOptions();
        TrustedForwardedHeaders.Configure(options, Configuration());

        Assert.Equal(ForwardedHeaders.None, options.ForwardedHeaders);
        Assert.Empty(options.KnownNetworks);
        Assert.Empty(options.KnownProxies);
    }

    [Fact]
    public void TrustedProxyAllowlistEnablesExpectedHeaders()
    {
        var options = new ForwardedHeadersOptions();
        TrustedForwardedHeaders.Configure(options, Configuration(new()
        {
            ["ReverseProxy:KnownProxies"] = "10.0.0.10",
            ["ReverseProxy:KnownNetworks"] = "10.1.0.0/16"
        }));

        Assert.Equal(ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, options.ForwardedHeaders);
        Assert.Single(options.KnownProxies);
        Assert.Single(options.KnownNetworks);
    }

    [Theory]
    [InlineData("0.0.0.0/0")]
    [InlineData("::/0")]
    [InlineData("not-a-network")]
    [InlineData("10.0.0.0/33")]
    public void InvalidOrGlobalProxyNetworkIsRejected(string network)
    {
        var options = new ForwardedHeadersOptions();

        Assert.Throws<InvalidOperationException>(() => TrustedForwardedHeaders.Configure(
            options, Configuration(new() { ["ReverseProxy:KnownNetworks"] = network })));
    }

    [Fact]
    public void RequiredCertificateMustBeConfigured()
    {
        Assert.Throws<InvalidOperationException>(() => CertificateLoader.LoadFromConfiguration(
            Configuration(), "Auth:Certificates:Signing", required: true));
    }

    [Fact]
    public void MissingCertificateFileIsRejected()
    {
        Assert.Throws<InvalidOperationException>(() => CertificateLoader.LoadFromConfiguration(
            Configuration(new() { ["Auth:Certificates:Signing:Path"] = "/missing/signing.pfx" }),
            "Auth:Certificates:Signing", required: true));
    }

    [Fact]
    public void SecurityOptionDefaultsRemainStable()
    {
        var identity = new IdentitySecurityOptions();
        Assert.Equal(8, identity.Password.RequiredLength);
        Assert.True(identity.Password.RequireDigit);
        Assert.True(identity.Password.RequireLowercase);
        Assert.True(identity.Password.RequireUppercase);
        Assert.False(identity.Password.RequireNonAlphanumeric);
        Assert.Equal(5, identity.Lockout.MaxFailedAccessAttempts);
        Assert.Equal(15, identity.Lockout.DefaultLockoutMinutes);

        var newsletter = new NewsletterTokenOptions();
        Assert.Equal(7, newsletter.ConfirmTokenLifetimeDays);
        Assert.Equal(7, newsletter.UnsubscribeTokenLifetimeDays);
        Assert.Equal(7, newsletter.OneClickTokenLifetimeDays);
        Assert.Equal(1000, newsletter.OneClickBatchSizeLimit);

        var fileAccess = new FileAccessTokenOptions();
        Assert.Equal(30, fileAccess.MinimumLifetimeSeconds);
        Assert.Equal(300, fileAccess.DefaultLifetimeSeconds);
        Assert.Equal(900, fileAccess.MaximumLifetimeSeconds);
    }

    [Fact]
    public void InvalidSecurityOptionsAreRejected()
    {
        Assert.Throws<InvalidOperationException>(() => SecurityRuntimeOptionsValidator.Validate(new IdentitySecurityOptions
        {
            Password = new PasswordPolicyOptions { RequiredLength = 7 }
        }));
        Assert.Throws<InvalidOperationException>(() => SecurityRuntimeOptionsValidator.Validate(new FileAccessTokenOptions
        {
            MinimumLifetimeSeconds = 60,
            DefaultLifetimeSeconds = 30,
            MaximumLifetimeSeconds = 900
        }));
    }

    private static IConfiguration Configuration(Dictionary<string, string?>? values = null) =>
        new ConfigurationBuilder().AddInMemoryCollection(values).Build();
}
