// 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.Authorization;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Components.Authorization;

public class AuthorizeRouteViewTest
{
    private static readonly IReadOnlyDictionary<string, object> EmptyParametersDictionary = new Dictionary<string, object>();
    private readonly TestAuthenticationStateProvider _authenticationStateProvider;
    private readonly TestRenderer _renderer;
    private readonly RouteView _authorizeRouteViewComponent;
    private readonly int _authorizeRouteViewComponentId;
    private readonly TestAuthorizationService _testAuthorizationService;

    public AuthorizeRouteViewTest()
    {
        _authenticationStateProvider = new TestAuthenticationStateProvider();
        _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(
            new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));

        _testAuthorizationService = new TestAuthorizationService();

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddSingleton<AuthenticationStateProvider>(_authenticationStateProvider);
        serviceCollection.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();
        serviceCollection.AddSingleton<IAuthorizationService>(_testAuthorizationService);
        serviceCollection.AddSingleton<NavigationManager, TestNavigationManager>();

        var services = serviceCollection.BuildServiceProvider();
        _renderer = new TestRenderer(services);
        var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer);
        _authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null, null);
        _authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
    }

    [Fact]
    public void WhenAuthorized_RendersPageInsideLayout()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary<string, object>
            {
                { nameof(TestPageRequiringAuthorization.Message), "Hello, world!" }
            });
        _testAuthorizationService.NextResult = AuthorizationResult.Success();

        // Act
        _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
            }));

        // Assert: renders layout
        var batch = _renderer.Batches.Single();
        var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
        Assert.Collection(layoutDiff.Edits,
            edit => AssertPrependText(batch, edit, "Layout starts here"),
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Component<TestPageRequiringAuthorization>(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
            },
            edit => AssertPrependText(batch, edit, "Layout ends here"));

        // Assert: renders page
        var pageDiff = batch.GetComponentDiffs<TestPageRequiringAuthorization>().Single();
        Assert.Collection(pageDiff.Edits,
            edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!"));
    }

    [Fact]
    public void AuthorizesWhenResourceIsSet()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary<string, object>
            {
                { nameof(TestPageRequiringAuthorization.Message), "Hello, world!" }
            });
        var resource = "foo";
        _testAuthorizationService.NextResult = AuthorizationResult.Success();

        // Act
        _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
                { nameof(AuthorizeRouteView.Resource), resource }
            }));

        // Assert: renders layout
        var batch = _renderer.Batches.Single();
        var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
        Assert.Collection(layoutDiff.Edits,
            edit => AssertPrependText(batch, edit, "Layout starts here"),
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Component<TestPageRequiringAuthorization>(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
            },
            edit => AssertPrependText(batch, edit, "Layout ends here"));

        // Assert: renders page
        var pageDiff = batch.GetComponentDiffs<TestPageRequiringAuthorization>().Single();
        Assert.Collection(pageDiff.Edits,
            edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!"));

        // Assert: Asserts that the Resource is present and set to "foo"
        Assert.Collection(_testAuthorizationService.AuthorizeCalls, call =>
        {
            Assert.Equal(resource, call.resource.ToString());
        });
    }

    [Fact]
    public void NotAuthorizedWhenResourceMissing()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
        _testAuthorizationService.NextResult = AuthorizationResult.Failed();

        // Act
        _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
            }));

        // Assert: renders layout containing "not authorized" message
        var batch = _renderer.Batches.Single();
        var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
        Assert.Collection(layoutDiff.Edits,
            edit => AssertPrependText(batch, edit, "Layout starts here"),
            edit => AssertPrependText(batch, edit, "Not authorized"),
            edit => AssertPrependText(batch, edit, "Layout ends here"));

        // Assert: Asserts that the Resource is Null
        Assert.Collection(_testAuthorizationService.AuthorizeCalls, call =>
        {
            Assert.Null(call.resource);
        });
    }

    [Fact]
    public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
        _testAuthorizationService.NextResult = AuthorizationResult.Failed();

        // Act
        _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
            }));

        // Assert: renders layout containing "not authorized" message
        var batch = _renderer.Batches.Single();
        var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
        Assert.Collection(layoutDiff.Edits,
            edit => AssertPrependText(batch, edit, "Layout starts here"),
            edit => AssertPrependText(batch, edit, "Not authorized"),
            edit => AssertPrependText(batch, edit, "Layout ends here"));
    }

    [Fact]
    public void WhenNotAuthorized_RendersCustomNotAuthorizedContentInsideLayout()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
        _testAuthorizationService.NextResult = AuthorizationResult.Failed();
        _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(new AuthenticationState(
            new ClaimsPrincipal(new TestIdentity { Name = "Bert" })));

        // Act
        RenderFragment<AuthenticationState> customNotAuthorized =
            state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");
        _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
                { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized },
            }));

        // Assert: renders layout containing "not authorized" message
        var batch = _renderer.Batches.Single();
        var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
        Assert.Collection(layoutDiff.Edits,
            edit => AssertPrependText(batch, edit, "Layout starts here"),
            edit => AssertPrependText(batch, edit, "Go away, Bert"),
            edit => AssertPrependText(batch, edit, "Layout ends here"));
    }

    [Fact]
    public async Task WhenAuthorizing_RendersDefaultAuthorizingContentInsideLayout()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
        var authStateTcs = new TaskCompletionSource<AuthenticationState>();
        _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task;
        RenderFragment<AuthenticationState> customNotAuthorized =
            state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");

        // Act
        var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
                { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized },
            }));

        // Assert: renders layout containing "authorizing" message
        Assert.False(firstRenderTask.IsCompleted);
        var batch = _renderer.Batches.Single();
        var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
        Assert.Collection(layoutDiff.Edits,
            edit => AssertPrependText(batch, edit, "Layout starts here"),
            edit => AssertPrependText(batch, edit, "Authorizing..."),
            edit => AssertPrependText(batch, edit, "Layout ends here"));

        // Act 2: updates when authorization completes
        authStateTcs.SetResult(new AuthenticationState(
            new ClaimsPrincipal(new TestIdentity { Name = "Bert" })));
        await firstRenderTask;

        // Assert 2: Only the layout is updated
        batch = _renderer.Batches.Skip(1).Single();
        var nonEmptyDiff = batch.DiffsInOrder.Where(d => d.Edits.Any()).Single();
        Assert.Equal(layoutDiff.ComponentId, nonEmptyDiff.ComponentId);
        Assert.Collection(nonEmptyDiff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
            Assert.Equal(1, edit.SiblingIndex);
            AssertFrame.Text(batch.ReferenceFrames[edit.ReferenceFrameIndex], "Go away, Bert");
        });
    }

    [Fact]
    public void WhenAuthorizing_RendersCustomAuthorizingContentInsideLayout()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
        var authStateTcs = new TaskCompletionSource<AuthenticationState>();
        _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task;
        RenderFragment customAuthorizing =
            builder => builder.AddContent(0, "Hold on, we're checking your papers.");

        // Act
        var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
                { nameof(AuthorizeRouteView.Authorizing), customAuthorizing },
            }));

        // Assert: renders layout containing "authorizing" message
        Assert.False(firstRenderTask.IsCompleted);
        var batch = _renderer.Batches.Single();
        var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
        Assert.Collection(layoutDiff.Edits,
            edit => AssertPrependText(batch, edit, "Layout starts here"),
            edit => AssertPrependText(batch, edit, "Hold on, we're checking your papers."),
            edit => AssertPrependText(batch, edit, "Layout ends here"));
    }

    [Fact]
    public void WithoutCascadedAuthenticationState_WrapsOutputInCascadingAuthenticationState()
    {
        // Arrange/Act
        var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
        _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData }
            }));

        // Assert
        var batch = _renderer.Batches.Single();
        var componentInstances = batch.ReferenceFrames
            .Where(f => f.FrameType == RenderTreeFrameType.Component)
            .Select(f => f.Component);

        Assert.Collection(componentInstances,
            // This is the hierarchy inside the AuthorizeRouteView, which contains its
            // own CascadingAuthenticationState
            component => Assert.IsType<CascadingAuthenticationState>(component),
            component => Assert.IsType<CascadingValue<Task<AuthenticationState>>>(component),
            component => Assert.IsAssignableFrom<AuthorizeViewCore>(component),
            component => Assert.IsType<LayoutView>(component),
            component => Assert.IsType<TestPageWithNoAuthorization>(component));
    }

    [Fact]
    public void WithCascadedAuthenticationState_DoesNotWrapOutputInCascadingAuthenticationState()
    {
        // Arrange
        var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
        var rootComponent = new AuthorizeRouteViewWithExistingCascadedAuthenticationState(
            _authenticationStateProvider.CurrentAuthStateTask,
            routeData);
        var rootComponentId = _renderer.AssignRootComponentId(rootComponent);

        // Act
        _renderer.RenderRootComponent(rootComponentId);

        // Assert
        var batch = _renderer.Batches.Single();
        var componentInstances = batch.ReferenceFrames
            .Where(f => f.FrameType == RenderTreeFrameType.Component)
            .Select(f => f.Component);

        Assert.Collection(componentInstances,
            // This is the externally-supplied cascading value
            component => Assert.IsType<CascadingValue<Task<AuthenticationState>>>(component),
            component => Assert.IsType<AuthorizeRouteView>(component),

            // This is the hierarchy inside the AuthorizeRouteView. It doesn't contain a
            // further CascadingAuthenticationState
            component => Assert.IsAssignableFrom<AuthorizeViewCore>(component),
            component => Assert.IsType<LayoutView>(component),
            component => Assert.IsType<TestPageWithNoAuthorization>(component));
    }

    [Fact]
    public void UpdatesOutputWhenRouteDataChanges()
    {
        // Arrange/Act 1: Start on some route
        // Not asserting about the initial output, as that is covered by other tests
        var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
        _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData },
                { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
            }));

        // Act 2: Move to another route
        var routeData2 = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
        var render2Task = _renderer.Dispatcher.InvokeAsync(() => _authorizeRouteViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(AuthorizeRouteView.RouteData), routeData2 },
            })));

        // Assert: we retain the layout instance, and mutate its contents
        Assert.True(render2Task.IsCompletedSuccessfully);
        Assert.Equal(2, _renderer.Batches.Count);
        var batch2 = _renderer.Batches[1];
        var diff = batch2.DiffsInOrder.Where(d => d.Edits.Any()).Single();
        Assert.Collection(diff.Edits,
            edit =>
            {
                // Inside the layout, we add the new content
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                Assert.Equal(1, edit.SiblingIndex);
                AssertFrame.Text(batch2.ReferenceFrames[edit.ReferenceFrameIndex], "Not authorized");
            },
            edit =>
            {
                // ... and remove the old content
                Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
                Assert.Equal(2, edit.SiblingIndex);
            });
    }

    private static void AssertPrependText(CapturedBatch batch, RenderTreeEdit edit, string text)
    {
        Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
        ref var referenceFrame = ref batch.ReferenceFrames[edit.ReferenceFrameIndex];
        AssertFrame.Text(referenceFrame, text);
    }

    class TestPageWithNoAuthorization : ComponentBase { }

    [Authorize]
    class TestPageRequiringAuthorization : ComponentBase
    {
        [Parameter] public string Message { get; set; }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, $"Hello from the page with message: {Message}");
        }
    }

    class TestLayout : LayoutComponentBase
    {
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, "Layout starts here");
            builder.AddContent(1, Body);
            builder.AddContent(2, "Layout ends here");
        }
    }

    class AuthorizeRouteViewWithExistingCascadedAuthenticationState : AutoRenderComponent
    {
        private readonly Task<AuthenticationState> _authenticationState;
        private readonly RouteData _routeData;

        public AuthorizeRouteViewWithExistingCascadedAuthenticationState(
            Task<AuthenticationState> authenticationState,
            RouteData routeData)
        {
            _authenticationState = authenticationState;
            _routeData = routeData;
        }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
            builder.AddComponentParameter(1, nameof(CascadingValue<object>.Value), _authenticationState);
            builder.AddComponentParameter(2, nameof(CascadingValue<object>.ChildContent), (RenderFragment)(builder =>
            {
                builder.OpenComponent<AuthorizeRouteView>(0);
                builder.AddComponentParameter(1, nameof(AuthorizeRouteView.RouteData), _routeData);
                builder.CloseComponent();
            }));
            builder.CloseComponent();
        }
    }

    class TestNavigationManager : NavigationManager
    {
        public TestNavigationManager()
        {
            Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash");
        }
    }
}
