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

using System.Diagnostics;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Components.Authorization;

public class AuthorizeViewTest
{
    // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
    // failures.
    private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);

    [Fact]
    public void RendersNothingIfNotAuthorized()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(
            childContent:
                context => builder => builder.AddContent(0, "This should not be rendered"));

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
        Assert.Empty(diff.Edits);

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Null(call.user.Identity);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void RendersNotAuthorizedIfNotAuthorized()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(
            notAuthorized:
                context => builder => builder.AddContent(0, $"You are not authorized, even though we know you are {context.User.Identity.Name}"));
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
        Assert.Collection(diff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
                "You are not authorized, even though we know you are Nellie");
        });

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Nellie", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void RendersNothingIfAuthorizedButNoChildContentOrAuthorizedProvided()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        authorizationService.NextResult = AuthorizationResult.Success();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView();
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
        Assert.Empty(diff.Edits);

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Nellie", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void RendersChildContentIfAuthorized()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        authorizationService.NextResult = AuthorizationResult.Success();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(
            childContent: context => builder =>
                builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
        Assert.Collection(diff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
                "You are authenticated as Nellie");
        });

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Nellie", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void RendersAuthorizedIfAuthorized()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        authorizationService.NextResult = AuthorizationResult.Success();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(
            authorized: context => builder =>
                builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
        Assert.Collection(diff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
                "You are authenticated as Nellie");
        });

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Nellie", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void RespondsToChangeInAuthorizationState()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        authorizationService.NextResult = AuthorizationResult.Success();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(
            childContent: context => builder =>
                builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Render in initial state. From other tests, we know this renders
        // a single batch with the correct output.
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();
        var authorizeViewComponentId = renderer.Batches.Single()
            .GetComponentFrames<AuthorizeView>().Single().ComponentId;
        authorizationService.AuthorizeCalls.Clear();

        // Act
        rootComponent.AuthenticationState = CreateAuthenticationState("Ronaldo");
        rootComponent.TriggerRender();

        // Assert: It's only one new diff. We skip the intermediate "await" render state
        // because the task was completed synchronously.
        Assert.Equal(2, renderer.Batches.Count);
        var batch = renderer.Batches.Last();
        var diff = batch.DiffsByComponentId[authorizeViewComponentId].Single();
        Assert.Collection(diff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
            AssertFrame.Text(
                batch.ReferenceFrames[edit.ReferenceFrameIndex],
                "You are authenticated as Ronaldo");
        });

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Ronaldo", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void ThrowsIfBothChildContentAndAuthorizedProvided()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(
            authorized: context => builder => { },
            childContent: context => builder => { });

        // Act/Assert
        renderer.AssignRootComponentId(rootComponent);
        var ex = Assert.Throws<InvalidOperationException>(() =>
            rootComponent.TriggerRender());
        Assert.Equal("Do not specify both 'Authorized' and 'ChildContent'.", ex.Message);
    }

    [Fact]
    public void RendersNothingUntilAuthorizationCompleted()
    {
        // Arrange
        var @event = new ManualResetEventSlim();
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
        var rootComponent = WrapInAuthorizeView(
            notAuthorized:
                context => builder => builder.AddContent(0, "You are not authorized"));
        var authTcs = new TaskCompletionSource<AuthenticationState>();
        rootComponent.AuthenticationState = authTcs.Task;

        // Act/Assert 1: Auth pending
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();
        var batch1 = renderer.Batches.Single();
        var authorizeViewComponentId = batch1.GetComponentFrames<AuthorizeView>().Single().ComponentId;
        var diff1 = batch1.DiffsByComponentId[authorizeViewComponentId].Single();
        Assert.Empty(diff1.Edits);

        // Act/Assert 2: Auth process completes asynchronously
        @event.Reset();
        authTcs.SetResult(new AuthenticationState(new ClaimsPrincipal()));

        // We need to wait here because the continuations of SetResult will be scheduled to run asynchronously.
        @event.Wait(Timeout);

        Assert.Equal(2, renderer.Batches.Count);
        var batch2 = renderer.Batches[1];
        var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
        Assert.Collection(diff2.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                batch2.ReferenceFrames[edit.ReferenceFrameIndex],
                "You are not authorized");
        });
    }

    [Fact]
    public async Task RendersAuthorizingUntilAuthorizationCompletedAsync()
    {
        // Covers https://github.com/dotnet/aspnetcore/pull/31794
        // Arrange
        var authorizationService = new TestAsyncAuthorizationService();
        authorizationService.NextResult = AuthorizationResult.Success();
        var renderer = CreateTestRenderer(authorizationService);
        renderer.OnUpdateDisplayComplete = () => { };
        var rootComponent = WrapInAuthorizeView(
            authorizing: builder => builder.AddContent(0, "Auth pending..."),
            authorized: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));

        var authTcs = new TaskCompletionSource<AuthenticationState>();
        rootComponent.AuthenticationState = authTcs.Task;

        // Act/Assert 1: Auth pending
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();
        var batch1 = Assert.Single(renderer.Batches);
        var authorizeViewComponentId = Assert.Single(batch1.GetComponentFrames<AuthorizeView>()).ComponentId;
        var diff1 = Assert.Single(batch1.DiffsByComponentId[authorizeViewComponentId]);
        Assert.Collection(diff1.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                batch1.ReferenceFrames[edit.ReferenceFrameIndex],
                "Auth pending...");
        });

        // We need to do this because the continuation from the TCS might run asynchronously
        // (This wouldn't happen under the sync context or in wasm)
        var renderTcs = new TaskCompletionSource();
        renderer.OnUpdateDisplayComplete = () => renderTcs.SetResult();
        authTcs.SetResult(CreateAuthenticationState("Monsieur").Result);

        await renderTcs.Task;
        Assert.Equal(2, renderer.Batches.Count);
        var batch2 = renderer.Batches[1];
        var diff2 = Assert.Single(batch2.DiffsByComponentId[authorizeViewComponentId]);
        Assert.Collection(diff2.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
            Assert.Equal(0, edit.SiblingIndex);
            AssertFrame.Text(
                batch2.ReferenceFrames[edit.ReferenceFrameIndex],
                "Hello, Monsieur!");
        });

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Monsieur", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void RendersAuthorizingUntilAuthorizationCompleted()
    {
        // Arrange
        var @event = new ManualResetEventSlim();
        var authorizationService = new TestAuthorizationService();
        authorizationService.NextResult = AuthorizationResult.Success();
        var renderer = CreateTestRenderer(authorizationService);
        renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
        var rootComponent = WrapInAuthorizeView(
            authorizing: builder => builder.AddContent(0, "Auth pending..."),
            authorized: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));
        var authTcs = new TaskCompletionSource<AuthenticationState>();
        rootComponent.AuthenticationState = authTcs.Task;

        // Act/Assert 1: Auth pending
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();
        var batch1 = renderer.Batches.Single();
        var authorizeViewComponentId = batch1.GetComponentFrames<AuthorizeView>().Single().ComponentId;
        var diff1 = batch1.DiffsByComponentId[authorizeViewComponentId].Single();
        Assert.Collection(diff1.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                batch1.ReferenceFrames[edit.ReferenceFrameIndex],
                "Auth pending...");
        });

        // Act/Assert 2: Auth process completes asynchronously
        @event.Reset();
        authTcs.SetResult(CreateAuthenticationState("Monsieur").Result);

        // We need to wait here because the continuations of SetResult will be scheduled to run asynchronously.
        @event.Wait(Timeout);

        Assert.Equal(2, renderer.Batches.Count);
        var batch2 = renderer.Batches[1];
        var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
        Assert.Collection(diff2.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
            Assert.Equal(0, edit.SiblingIndex);
            AssertFrame.Text(
                batch2.ReferenceFrames[edit.ReferenceFrameIndex],
                "Hello, Monsieur!");
        });

        // Assert: The IAuthorizationService was given expected criteria
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Monsieur", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void IncludesPolicyInAuthorizeCall()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(policy: "MyTestPolicy");
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Nellie", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements,
                req => Assert.Equal("MyTestPolicy", ((TestPolicyRequirement)req).PolicyName));
        });
    }

    [Fact]
    public void IncludesRolesInAuthorizeCall()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = WrapInAuthorizeView(roles: "SuperTestRole1, SuperTestRole2");
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Nellie", call.user.Identity.Name);
            Assert.Null(call.resource);
            Assert.Collection(call.requirements, req => Assert.Equal(
                new[] { "SuperTestRole1", "SuperTestRole2" },
                ((RolesAuthorizationRequirement)req).AllowedRoles));
        });
    }

    [Fact]
    public void IncludesResourceInAuthorizeCall()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        var resource = new object();
        var rootComponent = WrapInAuthorizeView(resource: resource);
        rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");

        // Act
        renderer.AssignRootComponentId(rootComponent);
        rootComponent.TriggerRender();

        // Assert
        Assert.Collection(authorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal("Nellie", call.user.Identity.Name);
            Assert.Same(resource, call.resource);
            Assert.Collection(call.requirements, req =>
                Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
        });
    }

    [Fact]
    public void RejectsNonemptyScheme()
    {
        // Arrange
        var authorizationService = new TestAuthorizationService();
        var renderer = CreateTestRenderer(authorizationService);
        var rootComponent = new TestAuthStateProviderComponent(builder =>
        {
            builder.OpenComponent<AuthorizeViewCoreWithScheme>(0);
            builder.CloseComponent();
        });
        renderer.AssignRootComponentId(rootComponent);

        // Act/Assert
        var ex = Assert.Throws<NotSupportedException>(rootComponent.TriggerRender);
        Assert.Equal("The authorization data specifies an authentication scheme with value 'test scheme'. Authentication schemes cannot be specified for components.", ex.Message);
    }

    private static TestAuthStateProviderComponent WrapInAuthorizeView(
        RenderFragment<AuthenticationState> childContent = null,
        RenderFragment<AuthenticationState> authorized = null,
        RenderFragment<AuthenticationState> notAuthorized = null,
        RenderFragment authorizing = null,
        string policy = null,
        string roles = null,
        object resource = null)
    {
        return new TestAuthStateProviderComponent(builder =>
        {
            builder.OpenComponent<AuthorizeView>(0);
            builder.AddComponentParameter(1, nameof(AuthorizeView.ChildContent), childContent);
            builder.AddComponentParameter(2, nameof(AuthorizeView.Authorized), authorized);
            builder.AddComponentParameter(3, nameof(AuthorizeView.NotAuthorized), notAuthorized);
            builder.AddComponentParameter(4, nameof(AuthorizeView.Authorizing), authorizing);
            builder.AddComponentParameter(5, nameof(AuthorizeView.Policy), policy);
            builder.AddComponentParameter(6, nameof(AuthorizeView.Roles), roles);
            builder.AddComponentParameter(7, nameof(AuthorizeView.Resource), resource);
            builder.CloseComponent();
        });
    }

    class TestAuthStateProviderComponent : AutoRenderComponent
    {
        private readonly RenderFragment _childContent;

        public Task<AuthenticationState> AuthenticationState { get; set; }
            = Task.FromResult(new AuthenticationState(new ClaimsPrincipal()));

        public TestAuthStateProviderComponent(RenderFragment childContent)
        {
            _childContent = childContent;
        }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
            builder.AddComponentParameter(1, nameof(CascadingValue<Task<AuthenticationState>>.Value), AuthenticationState);
            builder.AddComponentParameter(2, "ChildContent", (RenderFragment)(builder =>
            {
                builder.OpenComponent<NeverReRenderComponent>(0);
                builder.AddComponentParameter(1, "ChildContent", _childContent);
                builder.CloseComponent();
            }));
            builder.CloseComponent();
        }
    }

    // This is useful to show that the reason why a CascadingValue refreshes is because the
    // value itself changed, not just that we're re-rendering the entire tree and have to
    // recurse into all descendants because we're passing ChildContent
    class NeverReRenderComponent : ComponentBase
    {
        [Parameter] public RenderFragment ChildContent { get; set; }

        protected override bool ShouldRender() => false;

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, ChildContent);
        }
    }

    public static Task<AuthenticationState> CreateAuthenticationState(string username)
        => Task.FromResult(new AuthenticationState(
            new ClaimsPrincipal(new TestIdentity { Name = username })));

    public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService)
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddSingleton(authorizationService);
        serviceCollection.AddSingleton<IAuthorizationPolicyProvider>(new TestAuthorizationPolicyProvider());
        return new TestRenderer(serviceCollection.BuildServiceProvider());
    }

    public class AuthorizeViewCoreWithScheme : AuthorizeViewCore
    {
        protected override IAuthorizeData[] GetAuthorizeData()
            => new[] { new AuthorizeAttribute { AuthenticationSchemes = "test scheme" } };
    }
}
