// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;

namespace Microsoft.AspNetCore.Mvc.Authorization;

public class AuthorizeFilterTest
{
    private readonly ActionContext ActionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());

    [Fact]
    public void InvalidUser()
    {
        var authorizationContext = GetAuthorizationContext();
        Assert.Contains(authorizationContext.HttpContext.User.Identities, i => i.IsAuthenticated);
    }

    [Fact]
    public async Task DefaultConstructor_DeniesAnonymousUsers()
    {
        // Arrange
        var authorizationContext = GetAuthorizationContext(anonymous: true);

        // The type 'AuthorizeFilter' is both a filter by itself and also a filter factory.
        // The default filter provider first checks if a type is a filter factory and creates an instance of
        // this filter.
        var authorizeFilterFactory = new AuthorizeFilter();
        var filterFactory = authorizeFilterFactory as IFilterFactory;
        var authorizeFilter = (AuthorizeFilter)filterFactory.CreateInstance(
            authorizationContext.HttpContext.RequestServices);
        authorizationContext.Filters.Add(authorizeFilter);

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ChallengeResult>(authorizationContext.Result);
    }

    [Fact]
    public async Task AuthorizeFilter_CreatedWithAuthorizeData_ThrowsWhenOnAuthorizationAsyncIsCalled()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new[] { new AuthorizeAttribute() });
        var authorizationContext = GetAuthorizationContext();
        authorizationContext.Filters.Add(authorizeFilter);
        var expected = "An AuthorizationPolicy cannot be created without a valid instance of " +
            "IAuthorizationPolicyProvider.";

        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => authorizeFilter.OnAuthorizationAsync(authorizationContext));
        Assert.Equal(expected, ex.Message);
    }

    [Fact]
    public async Task AuthorizeFilter_CreatedWithPolicy_ThrowsWhenOnAuthorizationAsyncIsCalled()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new[] { new AuthorizeAttribute() });
        var authorizationContext = GetAuthorizationContext();
        authorizationContext.Filters.Add(authorizeFilter);
        var expected = "An AuthorizationPolicy cannot be created without a valid instance of " +
            "IAuthorizationPolicyProvider.";

        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => authorizeFilter.OnAuthorizationAsync(authorizationContext));
        Assert.Equal(expected, ex.Message);
    }

    [Fact]
    public async Task AuthorizeFilterCanAuthorizeNonAuthenticatedUser()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
        var authorizationContext = GetAuthorizationContext(anonymous: true);
        authorizationContext.HttpContext.User = new ClaimsPrincipal();

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    [Fact]
    public async Task AuthorizeFilterWillCallPolicyProviderOnAuthorization()
    {
        // Arrange
        var policyProvider = new Mock<IAuthorizationPolicyProvider>();
        var getPolicyCount = 0;
        policyProvider.Setup(p => p.GetPolicyAsync(It.IsAny<string>())).ReturnsAsync(new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build())
            .Callback(() => getPolicyCount++);
        var authorizeFilter = new AuthorizeFilter(policyProvider.Object, new AuthorizeAttribute[] { new AuthorizeAttribute("whatever") });
        var authorizationContext = GetAuthorizationContext();
        authorizationContext.Filters.Add(authorizeFilter);

        // Act & Assert
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);
        Assert.Equal(1, getPolicyCount);
        Assert.Null(authorizationContext.Result);

        await authorizeFilter.OnAuthorizationAsync(authorizationContext);
        Assert.Equal(2, getPolicyCount);
        Assert.Null(authorizationContext.Result);

        await authorizeFilter.OnAuthorizationAsync(authorizationContext);
        Assert.Equal(3, getPolicyCount);
        Assert.Null(authorizationContext.Result);

        // Make sure we don't cache the policy
        Assert.Null(authorizeFilter.Policy);
    }

    [Fact]
    public async Task AuthorizeFilterCanAuthorizeNullUser()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
        var authorizationContext = GetAuthorizationContext(anonymous: true);

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    [Fact]
    public async Task Invoke_ValidClaimShouldNotFail()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage").Build());
        var authorizationContext = GetAuthorizationContext();

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    [Fact]
    public async Task Invoke_EmptyClaimsShouldChallengeAnonymousUser()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
        var authorizationContext = GetAuthorizationContext(anonymous: true);
        authorizationContext.Filters.Add(authorizeFilter);

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ChallengeResult>(authorizationContext.Result);
    }

    [Fact]
    public async Task Invoke_EmptyClaimsWithAllowAnonymousAttributeShouldNotRejectAnonymousUser()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
        var authorizationContext = GetAuthorizationContext(anonymous: true);

        authorizationContext.Filters.Add(new AllowAnonymousFilter());

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    [Fact]
    public async Task Invoke_EmptyClaimsShouldAuthorizeAuthenticatedUser()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
        var authorizationContext = GetAuthorizationContext();

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    [Fact]
    public async Task Invoke_AuthSchemesFailShouldSetEmptyPrincipalOnContext()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder("Fails")
            .RequireAuthenticatedUser()
            .Build());
        var authorizationContext = GetAuthorizationContext();

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.NotNull(authorizationContext.HttpContext.User?.Identity);
    }

    [Fact]
    public async Task Invoke_SingleValidClaimShouldSucceed()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewComment", "CanViewPage").Build());
        var authorizationContext = GetAuthorizationContext();

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    private class TestPolicyProvider : IAuthorizationPolicyProvider
    {
        private readonly AuthorizationPolicy _true =
            new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build();
        private readonly AuthorizationPolicy _false =
            new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build();

        public int GetPolicyCalls = 0;

        public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
            => Task.FromResult(_true);

        public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
        {
            GetPolicyCalls++;
            return Task.FromResult(policyName == "true" ? _true : _false);
        }

        public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
            => Task.FromResult<AuthorizationPolicy>(null);
    }

    [Fact]
    public async Task AuthorizationFilterCombinesMultipleFiltersWithPolicyProvider()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new TestPolicyProvider(), new IAuthorizeData[]
        {
                new AuthorizeAttribute { Policy = "true"},
                new AuthorizeAttribute { Policy = "false"}
        });
        var authorizationContext = GetAuthorizationContext(anonymous: false);
        // Effective policy should fail, if both are combined
        authorizationContext.Filters.Add(authorizeFilter);
        var secondFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => true).Build());
        authorizationContext.Filters.Add(secondFilter);

        // Act
        await secondFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ForbidResult>(authorizationContext.Result);
    }

    [Fact]
    public async Task AuthorizationFilterCombinesMultipleFiltersWithDifferentPolicyProvider()
    {
        // Arrange
        var testProvider1 = new TestPolicyProvider();
        var testProvider2 = new TestPolicyProvider();
        var authorizeFilter = new AuthorizeFilter(testProvider1, new IAuthorizeData[]
        {
                new AuthorizeAttribute { Policy = "true"},
                new AuthorizeAttribute { Policy = "false"}
        });
        var authorizationContext = GetAuthorizationContext(anonymous: false);
        // Effective policy should fail, if both are combined
        authorizationContext.Filters.Add(authorizeFilter);
        var secondFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => true).Build());
        authorizationContext.Filters.Add(secondFilter);
        var thirdFilter = new AuthorizeFilter(testProvider2, new IAuthorizeData[] { new AuthorizeAttribute(policy: "something") });
        authorizationContext.Filters.Add(thirdFilter);

        // Act
        await thirdFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ForbidResult>(authorizationContext.Result);
        Assert.Equal(2, testProvider1.GetPolicyCalls);
        Assert.Equal(1, testProvider2.GetPolicyCalls);

        // Make sure the policy calls are not cached
        await thirdFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ForbidResult>(authorizationContext.Result);
        Assert.Equal(4, testProvider1.GetPolicyCalls);
        Assert.Equal(2, testProvider2.GetPolicyCalls);
    }

    [Fact]
    public async Task AuthorizationFilterCombinesMultipleFilters()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => false).Build());
        var authorizationContext = GetAuthorizationContext(anonymous: false);
        // Effective policy should fail, if both are combined
        authorizationContext.Filters.Add(authorizeFilter);
        var secondFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => true).Build());
        authorizationContext.Filters.Add(secondFilter);

        // Act
        await secondFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ForbidResult>(authorizationContext.Result);
    }

    [Fact]
    public async Task AuthorizationFilterIgnoresFirstFilterWhenCombining()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => false).Build());
        var authorizationContext = GetAuthorizationContext(anonymous: false);
        // Effective policy should fail, if both are combined
        authorizationContext.Filters.Add(authorizeFilter);
        var secondFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => false).Build());
        authorizationContext.Filters.Add(secondFilter);

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    [Fact]
    public async Task AuthorizationFilterCombinesDerivedFilters()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => true).Build());
        var authorizationContext = GetAuthorizationContext(anonymous: false);
        // Effective policy should fail, if both are combined
        authorizationContext.Filters.Add(authorizeFilter);
        authorizationContext.Filters.Add(new DerivedAuthorizeFilter());
        authorizationContext.Filters.Add(new DerivedAuthorizeFilter());
        var lastFilter = new DerivedAuthorizeFilter();
        authorizationContext.Filters.Add(lastFilter);

        // Act
        await lastFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ForbidResult>(authorizationContext.Result);
    }

    public class DerivedAuthorizeFilter : AuthorizeFilter
    {
        public DerivedAuthorizeFilter() : base(new AuthorizationPolicyBuilder().RequireAssertion(a => false).Build())
        { }
    }

    [Fact]
    public async Task AuthZResourceShouldBeAuthorizationFilterContext()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(c => c.Resource is AuthorizationFilterContext).Build());
        var authorizationContext = GetAuthorizationContext();

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.Null(authorizationContext.Result);
    }

    [Fact]
    public async Task Invoke_RequireUnknownRoleShouldForbid()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("Wut").Build());
        var authorizationContext = GetAuthorizationContext();
        authorizationContext.Filters.Add(authorizeFilter);

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ForbidResult>(authorizationContext.Result);
    }

    [Fact]
    public async Task Invoke_InvalidClaimShouldForbid()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder()
            .RequireClaim("Permission", "CanViewComment")
            .Build());
        var authorizationContext = GetAuthorizationContext();
        authorizationContext.Filters.Add(authorizeFilter);

        // Act
        await authorizeFilter.OnAuthorizationAsync(authorizationContext);

        // Assert
        Assert.IsType<ForbidResult>(authorizationContext.Result);
    }

    [Fact]
    public void CreateInstance_ReturnsSelfIfPolicyIsSet()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder()
            .RequireAssertion(_ => true)
            .Build());
        var factory = (IFilterFactory)authorizeFilter;

        // Act
        var result = factory.CreateInstance(new ServiceCollection().BuildServiceProvider());

        // Assert
        Assert.Same(authorizeFilter, result);
    }

    [Fact]
    public void CreateInstance_ReturnsSelfIfPolicyProviderIsSet()
    {
        // Arrange
        var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder()
            .RequireAssertion(_ => true)
            .Build());
        var factory = (IFilterFactory)authorizeFilter;

        // Act
        var result = factory.CreateInstance(new ServiceCollection().BuildServiceProvider());

        // Assert
        Assert.Same(authorizeFilter, result);
    }

    public static TheoryData AuthorizeFiltersCreatedWithoutPolicyOrPolicyProvider
    {
        get
        {
            return new TheoryData<AuthorizeFilter>
                {
                    new AuthorizeFilter(new[] { new AuthorizeAttribute()}),
                    new AuthorizeFilter("some-policy"),
                };
        }
    }

    [Theory]
    [MemberData(nameof(AuthorizeFiltersCreatedWithoutPolicyOrPolicyProvider))]
    public void CreateInstance_ReturnsNewFilterIfPolicyAndPolicyProviderAreNotSet(AuthorizeFilter authorizeFilter)
    {
        // Arrange
        var factory = (IFilterFactory)authorizeFilter;
        var serviceProvider = new ServiceCollection()
            .AddOptions()
            .AddAuthorization(options =>
            {
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAssertion(_ => true)
                    .Build();
                options.AddPolicy("some-policy", policy);
            })
            .BuildServiceProvider();

        // Act
        var result = factory.CreateInstance(serviceProvider);

        // Assert
        Assert.NotSame(authorizeFilter, result);
        var actual = Assert.IsType<AuthorizeFilter>(result);
        Assert.NotNull(actual.Policy);
    }

    [Theory]
    [MemberData(nameof(AuthorizeFiltersCreatedWithoutPolicyOrPolicyProvider))]
    public void CreateInstance_ReturnsNewFilterIfPolicyAndPolicyProviderAreNotSetAndCustomProviderIsUsed(
        AuthorizeFilter authorizeFilter)
    {
        // Arrange
        var factory = (IFilterFactory)authorizeFilter;
        var policyProvider = Mock.Of<IAuthorizationPolicyProvider>();
        var serviceProvider = new ServiceCollection()
            .AddSingleton(policyProvider)
            .BuildServiceProvider();

        // Act
        var result = factory.CreateInstance(serviceProvider);

        // Assert
        Assert.NotSame(authorizeFilter, result);
        var actual = Assert.IsType<AuthorizeFilter>(result);
        Assert.Same(policyProvider, actual.PolicyProvider);
    }

    [Fact]
    public async Task GetEffectivePolicyAsync_CombinesPoliciesFromAuthFilters()
    {
        // Arrange
        var policy1 = new AuthorizationPolicyBuilder()
            .RequireClaim("Claim1")
            .Build();

        var policy2 = new AuthorizationPolicyBuilder()
            .RequireClaim("Claim2")
            .Build();
        var filter1 = new AuthorizeFilter(policy1);
        var filter2 = new AuthorizeFilter(policy2);

        var context = new AuthorizationFilterContext(ActionContext, new[] { filter1, filter2 });

        // Act
        var effectivePolicy = await filter1.GetEffectivePolicyAsync(context);

        // Assert
        Assert.NotSame(policy1, effectivePolicy);
        Assert.NotSame(policy2, effectivePolicy);
        Assert.Equal(new[] { "Claim1", "Claim2" }, effectivePolicy.Requirements.Cast<ClaimsAuthorizationRequirement>().Select(c => c.ClaimType));
    }

    [Fact]
    public async Task GetEffectivePolicyAsync_CombinesPoliciesFromEndpoint()
    {
        // Arrange
        var policy1 = new AuthorizationPolicyBuilder()
            .RequireClaim("Claim1")
            .Build();

        var policy2 = new AuthorizationPolicyBuilder()
            .RequireClaim("Claim2")
            .Build();

        var filter = new AuthorizeFilter(policy1);
        var options = new AuthorizationOptions();
        options.AddPolicy("policy2", policy2);
        var policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(options));

        ActionContext.HttpContext.RequestServices = new ServiceCollection()
            .AddSingleton<IAuthorizationPolicyProvider>(policyProvider)
            .BuildServiceProvider();

        ActionContext.HttpContext.SetEndpoint(new Endpoint(
            _ => null,
            new EndpointMetadataCollection(new AuthorizeAttribute("policy2")),
            "test"));
        var context = new AuthorizationFilterContext(ActionContext, new[] { filter, });

        // Act
        var effectivePolicy = await filter.GetEffectivePolicyAsync(context);

        // Assert
        Assert.NotSame(policy1, effectivePolicy);
        Assert.NotSame(policy2, effectivePolicy);
        Assert.Equal(new[] { "Claim1", "Claim2" }, effectivePolicy.Requirements.Cast<ClaimsAuthorizationRequirement>().Select(c => c.ClaimType));
    }

    private AuthorizationFilterContext GetAuthorizationContext(
        bool anonymous = false,
        Action<IServiceCollection> registerServices = null)
    {
        var basicPrincipal = new ClaimsPrincipal(
            new ClaimsIdentity(
                new Claim[] {
                        new Claim("Permission", "CanViewPage"),
                        new Claim(ClaimTypes.Role, "Administrator"),
                        new Claim(ClaimTypes.Role, "User"),
                        new Claim(ClaimTypes.NameIdentifier, "John")},
                    "Basic"));

        var validUser = basicPrincipal;

        var bearerIdentity = new ClaimsIdentity(
                new Claim[] {
                        new Claim("Permission", "CupBearer"),
                        new Claim(ClaimTypes.Role, "Token"),
                        new Claim(ClaimTypes.NameIdentifier, "John Bear")},
                    "Bearer");

        var bearerPrincipal = new ClaimsPrincipal(bearerIdentity);

        validUser.AddIdentity(bearerIdentity);

        // ServiceProvider
        var serviceCollection = new ServiceCollection();

        var auth = new Mock<IAuthenticationService>();

        serviceCollection.AddOptions();
        serviceCollection.AddLogging();
        serviceCollection.AddSingleton(auth.Object);
        serviceCollection.AddAuthorization();
        registerServices?.Invoke(serviceCollection);

        var serviceProvider = serviceCollection.BuildServiceProvider();

        // HttpContext
        var httpContext = new Mock<HttpContext>();
        auth.Setup(c => c.AuthenticateAsync(httpContext.Object, "Bearer")).ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(bearerPrincipal, "Bearer")));
        auth.Setup(c => c.AuthenticateAsync(httpContext.Object, "Basic")).ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(basicPrincipal, "Basic")));
        auth.Setup(c => c.AuthenticateAsync(httpContext.Object, "Fails")).ReturnsAsync(AuthenticateResult.Fail("Fails"));
        httpContext.SetupProperty(c => c.User);
        if (!anonymous)
        {
            httpContext.Object.User = validUser;
        }
        httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider);
        var contextItems = new Dictionary<object, object>();
        httpContext.SetupGet(c => c.Items).Returns(contextItems);
        httpContext.SetupGet(c => c.Features).Returns(Mock.Of<IFeatureCollection>());

        // AuthorizationFilterContext
        var actionContext = new ActionContext(
            httpContext: httpContext.Object,
            routeData: new RouteData(),
            actionDescriptor: new ActionDescriptor());

        var authorizationContext = new AuthorizationFilterContext(
            actionContext,
            Enumerable.Empty<IFilterMetadata>().ToList()
        );

        return authorizationContext;
    }
}
