// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.using Microsoft.AspNetCore.Authorization;

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Authentication.AzureADB2C.UI.AzureADB2C.Controllers.Internal
{
    public class AccountControllerTests
    {
        [Fact]
        public void SignInNoScheme_ChallengesAADAzureADB2CDefaultScheme()
        {
            // Arrange
            var controller = new AccountController(
                new OptionsMonitor(AzureADB2CDefaults.AuthenticationScheme, new AzureADB2COptions()
                {
                    OpenIdConnectSchemeName = AzureADB2CDefaults.OpenIdScheme,
                    CookieSchemeName = AzureADB2CDefaults.CookieScheme
                }))
            {
                Url = new TestUrlHelper("~/", "https://localhost/")
            };

            // Act
            var result = controller.SignIn(null);

            // Assert
            var challenge = Assert.IsAssignableFrom<ChallengeResult>(result);
            var challengedScheme = Assert.Single(challenge.AuthenticationSchemes);
            Assert.Equal(AzureADB2CDefaults.AuthenticationScheme, challengedScheme);
            Assert.NotNull(challenge.Properties.RedirectUri);
            Assert.Equal("https://localhost/", challenge.Properties.RedirectUri);
        }

        [Fact]
        public void SignInProvidedScheme_ChallengesCustomScheme()
        {
            // Arrange
            var controller = new AccountController(new OptionsMonitor("Custom", new AzureADB2COptions()));
            controller.Url = new TestUrlHelper("~/", "https://localhost/");

            // Act
            var result = controller.SignIn("Custom");

            // Assert
            var challenge = Assert.IsAssignableFrom<ChallengeResult>(result);
            var challengedScheme = Assert.Single(challenge.AuthenticationSchemes);
            Assert.Equal("Custom", challengedScheme);
        }

        [Fact]
        public void ResetPasswordNoScheme_ChallengesAADAzureADB2CDefaultSchemeWithResetPassworPolicyAsync()
        {
            // Arrange
            var controller = new AccountController(
                new OptionsMonitor(
                    AzureADB2CDefaults.AuthenticationScheme,
                    new AzureADB2COptions() { ResetPasswordPolicyId = "Reset" }))
            {
                Url = new TestUrlHelper("~/", "https://localhost/")
            };
            controller.ControllerContext = CreateControllerContext(
                CreateAuthenticatedPrincipal(AzureADB2CDefaults.AuthenticationScheme));

            // Act
            var result = controller.ResetPassword(null);

            // Assert
            var challenge = Assert.IsAssignableFrom<ChallengeResult>(result);
            var challengedScheme = Assert.Single(challenge.AuthenticationSchemes);
            Assert.Equal(AzureADB2CDefaults.AuthenticationScheme, challengedScheme);
            Assert.NotNull(challenge.Properties.RedirectUri);
            Assert.Equal("https://localhost/", challenge.Properties.RedirectUri);
            Assert.NotNull(challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
            Assert.Equal("Reset", challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
        }

        [Fact]
        public void ResetPasswordCustomScheme_ChallengesAADAzureADB2CDefaultSchemeWithResetPassworPolicyFromCustomSchemeAsync()
        {
            // Arrange
            var controller = new AccountController(
                new OptionsMonitor(
                    "Custom",
                    new AzureADB2COptions() { ResetPasswordPolicyId = "CustomReset" }))
            {
                Url = new TestUrlHelper("~/", "https://localhost/")
            };
            controller.ControllerContext = CreateControllerContext(
                CreateAuthenticatedPrincipal("Custom"));

            // Act
            var result = controller.ResetPassword("Custom");

            // Assert
            var challenge = Assert.IsAssignableFrom<ChallengeResult>(result);
            var challengedScheme = Assert.Single(challenge.AuthenticationSchemes);
            Assert.Equal("Custom", challengedScheme);
            Assert.NotNull(challenge.Properties.RedirectUri);
            Assert.Equal("https://localhost/", challenge.Properties.RedirectUri);
            Assert.NotNull(challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
            Assert.Equal("CustomReset", challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
        }

        [Fact]
        public async Task EditProfileNoScheme_ChallengesAADAzureADB2CCustomSchemeWithEditProfilePolicyAsync()
        {
            // Arrange
            var controller = new AccountController(
                new OptionsMonitor(
                    AzureADB2CDefaults.AuthenticationScheme,
                    new AzureADB2COptions() { EditProfilePolicyId = "EditProfile" }))
            {
                Url = new TestUrlHelper("~/", "https://localhost/")
            };
            controller.ControllerContext = CreateControllerContext(
                CreateAuthenticatedPrincipal(AzureADB2CDefaults.AuthenticationScheme));

            // Act
            var result = await controller.EditProfile(null);

            // Assert
            var challenge = Assert.IsAssignableFrom<ChallengeResult>(result);
            var challengedScheme = Assert.Single(challenge.AuthenticationSchemes);
            Assert.Equal(AzureADB2CDefaults.AuthenticationScheme, challengedScheme);
            Assert.NotNull(challenge.Properties.RedirectUri);
            Assert.Equal("https://localhost/", challenge.Properties.RedirectUri);
            Assert.NotNull(challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
            Assert.Equal("EditProfile", challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
        }

        private ClaimsPrincipal CreateAuthenticatedPrincipal(string scheme) =>
            new ClaimsPrincipal(new ClaimsIdentity(scheme));

        private static ControllerContext CreateControllerContext(ClaimsPrincipal principal = null)
        {
            principal = principal ?? new ClaimsPrincipal(new ClaimsIdentity());
            var mock = new Mock<IAuthenticationService>();
            mock.Setup(authS => authS.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
                .ReturnsAsync<HttpContext, string, IAuthenticationService, AuthenticateResult>(
                    (ctx, scheme) =>
                    {
                        if (principal.Identity.IsAuthenticated)
                        {
                            return AuthenticateResult.Success(new AuthenticationTicket(principal, scheme));
                        }
                        else
                        {
                            return AuthenticateResult.NoResult();
                        }
                    });
            return new ControllerContext()
            {
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = new ServiceCollection()
                        .AddSingleton(mock.Object)
                        .BuildServiceProvider()
                }
            };
        }

        [Fact]
        public async Task EditProfileCustomScheme_ChallengesAADAzureADB2CCustomSchemeWithEditProfilePolicyFromCustomSchemeAsync()
        {
            // Arrange
            var controller = new AccountController(
                new OptionsMonitor(
                    "Custom",
                    new AzureADB2COptions() { EditProfilePolicyId = "CustomEditProfile" }))
            {
                Url = new TestUrlHelper("~/", "https://localhost/")
            };
            controller.ControllerContext = CreateControllerContext(
                CreateAuthenticatedPrincipal("Custom"));
            // Act
            var result = await controller.EditProfile("Custom");

            // Assert
            var challenge = Assert.IsAssignableFrom<ChallengeResult>(result);
            var challengedScheme = Assert.Single(challenge.AuthenticationSchemes);
            Assert.Equal("Custom", challengedScheme);
            Assert.NotNull(challenge.Properties.RedirectUri);
            Assert.Equal("https://localhost/", challenge.Properties.RedirectUri);
            Assert.NotNull(challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
            Assert.Equal("CustomEditProfile", challenge.Properties.Items[AzureADB2CDefaults.PolicyKey]);
        }

        [Fact]
        public async Task SignOutNoScheme_SignsOutDefaultCookiesAndDefaultOpenIDConnectAADAzureADB2CSchemesAsync()
        {
            // Arrange
            var options = new AzureADB2COptions()
            {
                CookieSchemeName = AzureADB2CDefaults.CookieScheme,
                OpenIdConnectSchemeName = AzureADB2CDefaults.OpenIdScheme
            };

            var controllerContext = CreateControllerContext(
                CreateAuthenticatedPrincipal(AzureADB2CDefaults.AuthenticationScheme));

            var descriptor = new PageActionDescriptor()
            {
                AttributeRouteInfo = new AttributeRouteInfo()
                {
                    Template = "/Account/SignedOut"
                }
            };
            var controller = new AccountController(new OptionsMonitor(AzureADB2CDefaults.AuthenticationScheme, options))
            {
                Url = new TestUrlHelper(
                    controllerContext.HttpContext,
                    new RouteData(),
                    descriptor,
                    "/Account/SignedOut",
                    "https://localhost/Account/SignedOut"),
                ControllerContext = new ControllerContext()
                {
                    HttpContext = controllerContext.HttpContext
                }
            };
            controller.Request.Scheme = "https";

            // Act
            var result = await controller.SignOut(null);

            // Assert
            var signOut = Assert.IsAssignableFrom<SignOutResult>(result);
            Assert.Equal(new[] { AzureADB2CDefaults.CookieScheme, AzureADB2CDefaults.OpenIdScheme }, signOut.AuthenticationSchemes);
            Assert.NotNull(signOut.Properties.RedirectUri);
            Assert.Equal("https://localhost/Account/SignedOut", signOut.Properties.RedirectUri);
        }

        [Fact]
        public async Task SignOutProvidedScheme_SignsOutCustomCookiesAndCustomOpenIDConnectAADAzureADB2CSchemesAsync()
        {
            // Arrange
            var options = new AzureADB2COptions()
            {
                CookieSchemeName = "Cookie",
                OpenIdConnectSchemeName = "OpenID"
            };

            var controllerContext = CreateControllerContext(
                CreateAuthenticatedPrincipal(AzureADB2CDefaults.AuthenticationScheme));
            var descriptor = new PageActionDescriptor()
            {
                AttributeRouteInfo = new AttributeRouteInfo()
                {
                    Template = "/Account/SignedOut"
                }
            };

            var controller = new AccountController(new OptionsMonitor("Custom", options))
            {
                Url = new TestUrlHelper(
                    controllerContext.HttpContext,
                    new RouteData(),
                    descriptor,
                    "/Account/SignedOut",
                    "https://localhost/Account/SignedOut"),
                ControllerContext = new ControllerContext()
                {
                    HttpContext = controllerContext.HttpContext
                }
            };
            controller.Request.Scheme = "https";

            // Act
            var result = await controller.SignOut("Custom");

            // Assert
            var signOut = Assert.IsAssignableFrom<SignOutResult>(result);
            Assert.Equal(new[] { "Cookie", "OpenID" }, signOut.AuthenticationSchemes);
        }

        private class OptionsMonitor : IOptionsMonitor<AzureADB2COptions>
        {
            public OptionsMonitor(string scheme, AzureADB2COptions options)
            {
                Scheme = scheme;
                Options = options;
            }

            public AzureADB2COptions CurrentValue => throw new NotImplementedException();

            public string Scheme { get; }
            public AzureADB2COptions Options { get; }

            public AzureADB2COptions Get(string name)
            {
                if (name == Scheme)
                {
                    return Options;
                }

                return null;
            }

            public IDisposable OnChange(Action<AzureADB2COptions, string> listener)
            {
                throw new NotImplementedException();
            }
        }

        private class TestUrlHelper : IUrlHelper
        {
            public TestUrlHelper(string contentPath, string url)
            {
                ContentPath = contentPath;
                Url = url;
            }

            public TestUrlHelper(
                HttpContext context,
                RouteData routeData,
                ActionDescriptor descriptor,
                string contentPath,
                string url)
            {
                HttpContext = context;
                RouteData = routeData;
                ActionDescriptor = descriptor;
                ContentPath = contentPath;
                Url = url;
            }

            public ActionContext ActionContext =>
                new ActionContext(HttpContext, RouteData, ActionDescriptor);

            public string ContentPath { get; }
            public string Url { get; }
            public HttpContext HttpContext { get; }
            public RouteData RouteData { get; }
            public ActionDescriptor ActionDescriptor { get; }

            public string Action(UrlActionContext actionContext)
            {
                throw new NotImplementedException();
            }

            public string Content(string contentPath)
            {
                if (ContentPath == contentPath)
                {
                    return Url;
                }
                return "";
            }

            public bool IsLocalUrl(string url)
            {
                throw new NotImplementedException();
            }

            public string Link(string routeName, object values)
            {
                throw new NotImplementedException();
            }

            public string RouteUrl(UrlRouteContext routeContext)
            {
                if (routeContext.Values is RouteValueDictionary dicionary &&
                    dicionary.TryGetValue("page", out var page) &&
                    page is string pagePath &&
                    ContentPath == pagePath)
                {
                    return Url;
                }

                return null;
            }
        }
    }
}
