using System.Net;
using System.Reflection;
using System.Text;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Configuration;
using Tiku.Api.Controllers;
using Tiku.Api.Middleware;
using Tiku.Api.Options;

namespace Tiku.IntegrationTests.Api;

public sealed class AuthRateLimitPolicyTests
{
    [Fact]
    public void Authentication_rate_limits_bind_from_the_named_configuration_section()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"{AuthRateLimitOptions.SectionName}:PasswordPermitLimit"] = "7",
                [$"{AuthRateLimitOptions.SectionName}:PasswordWindowSeconds"] = "600",
                [$"{AuthRateLimitOptions.SectionName}:SmsPermitLimit"] = "3",
                [$"{AuthRateLimitOptions.SectionName}:SmsWindowSeconds"] = "90"
            })
            .Build();

        var options = configuration
            .GetSection(AuthRateLimitOptions.SectionName)
            .Get<AuthRateLimitOptions>();

        Assert.NotNull(options);
        Assert.Equal(7, options.PasswordPermitLimit);
        Assert.Equal(600, options.PasswordWindowSeconds);
        Assert.Equal(3, options.SmsPermitLimit);
        Assert.Equal(90, options.SmsWindowSeconds);
    }

    [Fact]
    public void Authentication_rate_limit_values_must_be_positive()
    {
        var options = new AuthRateLimitOptions
        {
            PasswordPermitLimit = 0,
            SmsWindowSeconds = 0
        };
        var validationResults = new List<ValidationResult>();

        var valid = Validator.TryValidateObject(
            options,
            new ValidationContext(options),
            validationResults,
            validateAllProperties: true);

        Assert.False(valid);
        Assert.Equal(2, validationResults.Count);
    }

    [Fact]
    public void Password_login_uses_the_password_named_policy()
    {
        AssertPolicy(nameof(AuthController.LoginWithPassword), AuthRateLimitPolicies.Password);
    }

    [Fact]
    public void Sms_send_uses_the_sms_named_policy()
    {
        AssertPolicy(nameof(AuthController.SendSmsCode), AuthRateLimitPolicies.Sms);
    }

    [Fact]
    public void Required_password_change_uses_the_password_named_policy()
    {
        AssertPolicy(nameof(AuthController.ChangeRequiredPassword), AuthRateLimitPolicies.Password);
    }

    [Fact]
    public async Task Password_partition_combines_account_and_ip_without_exposing_the_account()
    {
        var first = await CapturePartitionAsync(
            AuthRateLimitPolicies.Password,
            """{"Phone":"13800000000","password":"secret"}""",
            "127.0.0.1");
        var same = await CapturePartitionAsync(
            AuthRateLimitPolicies.Password,
            """{"phone":"13800000000","password":"different"}""",
            "127.0.0.1");
        var differentAccount = await CapturePartitionAsync(
            AuthRateLimitPolicies.Password,
            """{"phone":"13900000000","password":"secret"}""",
            "127.0.0.1");
        var differentIp = await CapturePartitionAsync(
            AuthRateLimitPolicies.Password,
            """{"phone":"13800000000","password":"secret"}""",
            "127.0.0.2");

        Assert.Equal(first, same);
        Assert.NotEqual(first, differentAccount);
        Assert.NotEqual(first, differentIp);
        Assert.DoesNotContain("13800000000", first, StringComparison.Ordinal);
        Assert.DoesNotContain("secret", first, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Sms_partition_combines_phone_and_ip_without_exposing_the_phone()
    {
        var first = await CapturePartitionAsync(
            AuthRateLimitPolicies.Sms,
            """{"phone":"13800000000","deviceId":"device-one"}""",
            "127.0.0.1");
        var differentPhone = await CapturePartitionAsync(
            AuthRateLimitPolicies.Sms,
            """{"phone":"13900000000","deviceId":"device-one"}""",
            "127.0.0.1");
        var differentIp = await CapturePartitionAsync(
            AuthRateLimitPolicies.Sms,
            """{"phone":"13800000000","deviceId":"device-one"}""",
            "127.0.0.2");

        Assert.NotEqual(first, differentPhone);
        Assert.NotEqual(first, differentIp);
        Assert.DoesNotContain("13800000000", first, StringComparison.Ordinal);
    }

    private static void AssertPolicy(string methodName, string expectedPolicy)
    {
        var method = typeof(AuthController).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
        var attribute = method?.GetCustomAttribute<EnableRateLimitingAttribute>();

        Assert.NotNull(attribute);
        Assert.Equal(expectedPolicy, attribute.PolicyName);
    }

    private static async Task<string> CapturePartitionAsync(
        string policyName,
        string json,
        string ipAddress)
    {
        var context = new DefaultHttpContext();
        context.Connection.RemoteIpAddress = IPAddress.Parse(ipAddress);
        context.Request.Method = HttpMethods.Post;
        context.Request.ContentType = "application/json";
        context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
        context.SetEndpoint(new Endpoint(
            _ => Task.CompletedTask,
            new EndpointMetadataCollection(new EnableRateLimitingAttribute(policyName)),
            "auth-rate-limit-test"));

        string? partition = null;
        var middleware = new AuthRateLimitPartitionMiddleware(async nextContext =>
        {
            partition = AuthRateLimitPartitionKey.Resolve(nextContext, policyName);
            using var reader = new StreamReader(
                nextContext.Request.Body,
                Encoding.UTF8,
                leaveOpen: true);
            Assert.Equal(json, await reader.ReadToEndAsync());
        });

        await middleware.InvokeAsync(context);
        return Assert.IsType<string>(partition);
    }
}
