// 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 System.Security.Principal;
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 CascadingAuthenticationStateTest
{
    [Fact]
    public void RequiresRegisteredService()
    {
        // Arrange
        var renderer = new TestRenderer();
        var component = new AutoRenderFragmentComponent(builder =>
        {
            builder.OpenComponent<CascadingAuthenticationState>(0);
            builder.CloseComponent();
        });

        // Act/Assert
        renderer.AssignRootComponentId(component);
        var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
        Assert.Contains($"There is no registered service of type '{typeof(AuthenticationStateProvider).FullName}'.", ex.Message);
    }

    [Fact]
    public void SuppliesSynchronouslyAvailableAuthStateToChildContent()
    {
        // Arrange: Service
        var services = new ServiceCollection();
        var authStateProvider = new TestAuthenticationStateProvider()
        {
            CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState("Bert"))
        };
        services.AddSingleton<AuthenticationStateProvider>(authStateProvider);

        // Arrange: Renderer and component
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new UseCascadingAuthenticationStateComponent();

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

        // Assert
        var batch = renderer.Batches.Single();
        var receiveAuthStateId = batch.GetComponentFrames<ReceiveAuthStateComponent>().Single().ComponentId;
        var receiveAuthStateDiff = batch.DiffsByComponentId[receiveAuthStateId].Single();
        Assert.Collection(receiveAuthStateDiff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                batch.ReferenceFrames[edit.ReferenceFrameIndex],
                "Authenticated: True; Name: Bert; Pending: False; Renders: 1");
        });
    }

    [Fact]
    public void SuppliesAsynchronouslyAvailableAuthStateToChildContent()
    {
        // Arrange: Service
        var services = new ServiceCollection();
        var authStateTaskCompletionSource = new TaskCompletionSource<AuthenticationState>();
        var authStateProvider = new TestAuthenticationStateProvider()
        {
            CurrentAuthStateTask = authStateTaskCompletionSource.Task
        };
        services.AddSingleton<AuthenticationStateProvider>(authStateProvider);

        // Arrange: Renderer and component
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new UseCascadingAuthenticationStateComponent();

        // Act 1: Initial synchronous render
        renderer.AssignRootComponentId(component);
        component.TriggerRender();

        // Assert 1: Empty state
        var batch1 = renderer.Batches.Single();
        var receiveAuthStateFrame = batch1.GetComponentFrames<ReceiveAuthStateComponent>().Single();
        var receiveAuthStateId = receiveAuthStateFrame.ComponentId;
        var receiveAuthStateComponent = (ReceiveAuthStateComponent)receiveAuthStateFrame.Component;
        var receiveAuthStateDiff1 = batch1.DiffsByComponentId[receiveAuthStateId].Single();
        Assert.Collection(receiveAuthStateDiff1.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                batch1.ReferenceFrames[edit.ReferenceFrameIndex],
                "Authenticated: False; Name: ; Pending: True; Renders: 1");
        });

        // Act/Assert 2: Auth state fetch task completes in background
        // No new renders yet, because the cascading parameter itself hasn't changed
        authStateTaskCompletionSource.SetResult(CreateAuthenticationState("Bert"));
        Assert.Single(renderer.Batches);

        // Act/Assert 3: Refresh display
        receiveAuthStateComponent.TriggerRender();
        Assert.Equal(2, renderer.Batches.Count);
        var batch2 = renderer.Batches.Last();
        var receiveAuthStateDiff2 = batch2.DiffsByComponentId[receiveAuthStateId].Single();
        Assert.Collection(receiveAuthStateDiff2.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
            AssertFrame.Text(
                batch2.ReferenceFrames[edit.ReferenceFrameIndex],
                "Authenticated: True; Name: Bert; Pending: False; Renders: 2");
        });
    }

    [Fact]
    public void RespondsToNotificationsFromAuthenticationStateProvider()
    {
        // Arrange: Service
        var services = new ServiceCollection();
        var authStateProvider = new TestAuthenticationStateProvider()
        {
            CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState(null))
        };
        services.AddSingleton<AuthenticationStateProvider>(authStateProvider);

        // Arrange: Renderer and component, initially rendered
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new UseCascadingAuthenticationStateComponent();
        renderer.AssignRootComponentId(component);
        component.TriggerRender();
        var receiveAuthStateId = renderer.Batches.Single()
            .GetComponentFrames<ReceiveAuthStateComponent>().Single().ComponentId;

        // Act 2: AuthenticationStateProvider issues notification
        authStateProvider.TriggerAuthenticationStateChanged(
            Task.FromResult(CreateAuthenticationState("Bert")));

        // Assert 2: Re-renders content
        Assert.Equal(2, renderer.Batches.Count);
        var batch = renderer.Batches.Last();
        var receiveAuthStateDiff = batch.DiffsByComponentId[receiveAuthStateId].Single();
        Assert.Collection(receiveAuthStateDiff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
            AssertFrame.Text(
                batch.ReferenceFrames[edit.ReferenceFrameIndex],
                "Authenticated: True; Name: Bert; Pending: False; Renders: 2");
        });
    }

    class ReceiveAuthStateComponent : AutoRenderComponent
    {
        int numRenders;

        [CascadingParameter] Task<AuthenticationState> AuthStateTask { get; set; }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            numRenders++;

            if (AuthStateTask.IsCompleted)
            {
                var identity = AuthStateTask.Result.User.Identity;
                builder.AddContent(0, $"Authenticated: {identity.IsAuthenticated}; Name: {identity.Name}; Pending: False; Renders: {numRenders}");
            }
            else
            {
                builder.AddContent(0, $"Authenticated: False; Name: ; Pending: True; Renders: {numRenders}");
            }
        }
    }

    class UseCascadingAuthenticationStateComponent : AutoRenderComponent
    {
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenComponent<CascadingAuthenticationState>(0);
            builder.AddComponentParameter(1, "ChildContent", new RenderFragment(childBuilder =>
            {
                childBuilder.OpenComponent<ReceiveAuthStateComponent>(0);
                childBuilder.CloseComponent();
            }));
            builder.CloseComponent();
        }
    }

    public static AuthenticationState CreateAuthenticationState(string username)
        => new AuthenticationState(new ClaimsPrincipal(username == null
            ? new ClaimsIdentity()
            : (IIdentity)new TestIdentity { Name = username }));

    class TestIdentity : IIdentity
    {
        public string AuthenticationType => "Test";

        public bool IsAuthenticated => true;

        public string Name { get; set; }
    }
}
