using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using Xunit;

namespace StellaOps.Auth.ServerIntegration.Tests;

public class StellaOpsScopeAuthorizationHandlerTests
{
    [Fact]
    public async Task HandleRequirement_Succeeds_WhenScopePresent()
    {
        var optionsMonitor = CreateOptionsMonitor(options =>
        {
            options.Authority = "https://authority.example";
            options.Validate();
        });

        var (handler, accessor) = CreateHandler(optionsMonitor, remoteAddress: IPAddress.Parse("10.0.0.1"));
        var requirement = new StellaOpsScopeRequirement(new[] { StellaOpsScopes.FeedserJobsTrigger });
        var principal = new StellaOpsPrincipalBuilder()
            .WithSubject("user-1")
            .WithScopes(new[] { StellaOpsScopes.FeedserJobsTrigger })
            .Build();

        var context = new AuthorizationHandlerContext(new[] { requirement }, principal, accessor.HttpContext);

        await handler.HandleAsync(context);

        Assert.True(context.HasSucceeded);
    }

    [Fact]
    public async Task HandleRequirement_Succeeds_WhenBypassNetworkMatches()
    {
        var optionsMonitor = CreateOptionsMonitor(options =>
        {
            options.Authority = "https://authority.example";
            options.BypassNetworks.Add("127.0.0.1/32");
            options.Validate();
        });

        var (handler, accessor) = CreateHandler(optionsMonitor, remoteAddress: IPAddress.Parse("127.0.0.1"));
        var requirement = new StellaOpsScopeRequirement(new[] { StellaOpsScopes.FeedserJobsTrigger });
        var principal = new ClaimsPrincipal(new ClaimsIdentity());
        var context = new AuthorizationHandlerContext(new[] { requirement }, principal, accessor.HttpContext);

        await handler.HandleAsync(context);

        Assert.True(context.HasSucceeded);
    }

    [Fact]
    public async Task HandleRequirement_Fails_WhenScopeMissingAndNoBypass()
    {
        var optionsMonitor = CreateOptionsMonitor(options =>
        {
            options.Authority = "https://authority.example";
            options.Validate();
        });

        var (handler, accessor) = CreateHandler(optionsMonitor, remoteAddress: IPAddress.Parse("203.0.113.10"));
        var requirement = new StellaOpsScopeRequirement(new[] { StellaOpsScopes.FeedserJobsTrigger });
        var principal = new ClaimsPrincipal(new ClaimsIdentity());
        var context = new AuthorizationHandlerContext(new[] { requirement }, principal, accessor.HttpContext);

        await handler.HandleAsync(context);

        Assert.False(context.HasSucceeded);
    }

    private static (StellaOpsScopeAuthorizationHandler Handler, IHttpContextAccessor Accessor) CreateHandler(IOptionsMonitor<StellaOpsResourceServerOptions> optionsMonitor, IPAddress remoteAddress)
    {
        var accessor = new HttpContextAccessor();
        var httpContext = new DefaultHttpContext();
        httpContext.Connection.RemoteIpAddress = remoteAddress;
        accessor.HttpContext = httpContext;

        var bypassEvaluator = new StellaOpsBypassEvaluator(optionsMonitor, NullLogger<StellaOpsBypassEvaluator>.Instance);

        var handler = new StellaOpsScopeAuthorizationHandler(
            accessor,
            bypassEvaluator,
            NullLogger<StellaOpsScopeAuthorizationHandler>.Instance);
        return (handler, accessor);
    }

    private static IOptionsMonitor<StellaOpsResourceServerOptions> CreateOptionsMonitor(Action<StellaOpsResourceServerOptions> configure)
        => new TestOptionsMonitor<StellaOpsResourceServerOptions>(configure);

    private sealed class TestOptionsMonitor<TOptions> : IOptionsMonitor<TOptions>
        where TOptions : class, new()
    {
        private readonly TOptions value;

        public TestOptionsMonitor(Action<TOptions> configure)
        {
            value = new TOptions();
            configure(value);
        }

        public TOptions CurrentValue => value;

        public TOptions Get(string? name) => value;

        public IDisposable OnChange(Action<TOptions, string> listener) => NullDisposable.Instance;

        private sealed class NullDisposable : IDisposable
        {
            public static NullDisposable Instance { get; } = new();
            public void Dispose()
            {
            }
        }
    }
}
