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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using AngleSharp.Dom.Html;
using Identity.DefaultUI.WebSite;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Identity.FunctionalTests
{
    public abstract class ManagementTests<TStartup, TContext> : IClassFixture<ServerFactory<TStartup, TContext>>
        where TStartup : class
        where TContext : DbContext
    {
        public ManagementTests(ServerFactory<TStartup, TContext> serverFactory)
        {
            ServerFactory = serverFactory;
        }

        public ServerFactory<TStartup, TContext> ServerFactory { get; }

        [Fact]
        public async Task CanEnableTwoFactorAuthentication()
        {
            // Arrange
            var client = ServerFactory
                .CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var password = $"[PLACEHOLDER]-1a";

            var index = await UserStories.RegisterNewUserAsync(client, userName, password);

            // Act & Assert
            Assert.NotNull(await UserStories.EnableTwoFactorAuthentication(index));
        }

        [Fact]
        public async Task CannotEnableTwoFactorAuthenticationWithoutCookieConsent()
        {
            // Arrange
            var client = ServerFactory
                .CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var password = $"[PLACEHOLDER]-1a";

            var index = await UserStories.RegisterNewUserAsync(client, userName, password);

            // Act & Assert
            Assert.Null(await UserStories.EnableTwoFactorAuthentication(index, consent: false));
        }

        [Fact]
        public async Task CanConfirmEmail()
        {
            // Arrange
            var emails = new ContosoEmailSender();
            void ConfigureTestServices(IServiceCollection services) =>
                services.SetupTestEmailSender(emails);

            var server = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
            var client = server.CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var password = $"[PLACEHOLDER]-1a";

            var index = await UserStories.RegisterNewUserAsync(client, userName, password);
            var manageIndex = await UserStories.SendEmailConfirmationLinkAsync(index);

            // Act & Assert
            Assert.Equal(2, emails.SentEmails.Count);
            var email = emails.SentEmails[1];
            await UserStories.ConfirmEmailAsync(email, client);
        }

        [Fact]
        public async Task CanChangeEmail()
        {
            // Arrange
            var emails = new ContosoEmailSender();
            void ConfigureTestServices(IServiceCollection services) =>
                services.SetupTestEmailSender(emails);

            var server = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
            var client = server.CreateClient();
            var newClient = server.CreateClient();
            var failedClient = server.CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var password = $"[PLACEHOLDER]-1a";
            var newEmail = "updatedEmail@example.com";

            var index = await UserStories.RegisterNewUserAsync(client, userName, password);
            var email = await UserStories.SendUpdateEmailAsync(index, newEmail);

            // Act & Assert
            Assert.Equal(2, emails.SentEmails.Count);
            await UserStories.ConfirmEmailAsync(emails.SentEmails[1], client);

            // Verify can login with new email, fails with old
            await UserStories.LoginExistingUserAsync(newClient, newEmail, password);
            await UserStories.LoginFailsWithWrongPasswordAsync(failedClient, userName, password);

        }

        [Fact]
        public async Task CanChangePassword()
        {
            // Arrange
            var principals = new List<ClaimsPrincipal>();
            void ConfigureTestServices(IServiceCollection services) =>
                services.SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme);

            var server = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureTestServices(ConfigureTestServices));

            var client = server.CreateClient();
            var newClient = server.CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var password = "[PLACEHOLDER]-1a";
            var newPassword = "[PLACEHOLDER]-1a-updated";

            var index = await UserStories.RegisterNewUserAsync(client, userName, password);

            // Act 1
            var changedPassword = await UserStories.ChangePasswordAsync(index, password, newPassword);

            // Assert 1
            // RefreshSignIn generates a new security stamp claim
            AssertClaimsNotEqual(principals[0], principals[1], "AspNet.Identity.SecurityStamp");

            // Act 2
            await UserStories.LoginExistingUserAsync(newClient, userName, newPassword);

            // Assert 2
            // Signing in again with a different client uses the same security stamp claim
            AssertClaimsEqual(principals[1], principals[2], "AspNet.Identity.SecurityStamp");
        }

        [Fact]
        public async Task CanSetPasswordWithExternalLogin()
        {
            // Arrange
            var principals = new List<ClaimsPrincipal>();
            void ConfigureTestServices(IServiceCollection services) =>
                services
                    .SetupTestThirdPartyLogin()
                    .SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme);

            var server = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureTestServices(ConfigureTestServices));

            var client = server.CreateClient();
            var newClient = server.CreateClient();
            var loginAfterSetPasswordClient = server.CreateClient();

            var guid = Guid.NewGuid();
            var userName = $"{guid}";
            var email = $"{guid}@example.com";

            // Act 1
            var index = await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email);
            index = await UserStories.LoginWithSocialLoginAsync(newClient, userName);

            // Assert 1
            Assert.NotNull(principals[1].Identities.Single().Claims.Single(c => c.Type == ClaimTypes.AuthenticationMethod).Value);

            // Act 2
            await UserStories.SetPasswordAsync(index, "[PLACEHOLDER]-1a-updated");

            // Assert 2
            // RefreshSignIn uses the same AuthenticationMethod claim value
            AssertClaimsEqual(principals[1], principals[2], ClaimTypes.AuthenticationMethod);

            // Act & Assert 3
            // Can log in with the password set above
            await UserStories.LoginExistingUserAsync(loginAfterSetPasswordClient, email, "[PLACEHOLDER]-1a-updated");
        }

        [Fact]
        public async Task CanRemoveExternalLogin()
        {
            // Arrange
            var principals = new List<ClaimsPrincipal>();
            void ConfigureTestServices(IServiceCollection services) =>
                services
                    .SetupTestThirdPartyLogin()
                    .SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme);

            var server = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureTestServices(ConfigureTestServices));

            var client = server.CreateClient();

            var guid = Guid.NewGuid();
            var userName = $"{guid}";
            var email = $"{guid}@example.com";

            // Act
            var index = await UserStories.RegisterNewUserAsync(client, email, "[PLACEHOLDER]-1a");
            var linkLogin = await UserStories.LinkExternalLoginAsync(index, email);
            await UserStories.RemoveExternalLoginAsync(linkLogin, email);

            // RefreshSignIn generates a new security stamp claim
            AssertClaimsNotEqual(principals[0], principals[1], "AspNet.Identity.SecurityStamp");
        }

        [Fact]
        public async Task CanSeeExternalLoginProviderDisplayName()
        {
            // Arrange
            void ConfigureTestServices(IServiceCollection services) => services.SetupTestThirdPartyLogin();

            var server = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureTestServices(ConfigureTestServices));

            var client = server.CreateClient();

            // Act
            var userName = Guid.NewGuid().ToString();
            var email = $"{userName}@example.com";
            var index = await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email);
            var manage = await index.ClickManageLinkWithExternalLoginAsync();
            var externalLogins = await manage.ClickExternalLoginsAsync();

            // Assert
            Assert.Contains("Contoso", externalLogins.ExternalLoginDisplayName.TextContent);
        }

        [Fact]
        public async Task CanResetAuthenticator()
        {
            // Arrange
            var principals = new List<ClaimsPrincipal>();
            void ConfigureTestServices(IServiceCollection services) =>
                services
                    .SetupTestThirdPartyLogin()
                    .SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme);

            var server = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureTestServices(ConfigureTestServices));

            var client = server.CreateClient();
            var newClient = server.CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var password = $"[PLACEHOLDER]-1a";

            // Act
            var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password);
            var showRecoveryCodes = await UserStories.EnableTwoFactorAuthentication(loggedIn);
            var twoFactorKey = showRecoveryCodes.Context.AuthenticatorKey;

            // Use a new client to simulate a new browser session.
            await UserStories.AcceptCookiePolicy(newClient);
            var index = await UserStories.LoginExistingUser2FaAsync(newClient, userName, password, twoFactorKey);
            await UserStories.ResetAuthenticator(index);

            // RefreshSignIn generates a new security stamp claim
            AssertClaimsNotEqual(principals[1], principals[2], "AspNet.Identity.SecurityStamp");
        }

        [Theory]
        [InlineData(false, false)]
        [InlineData(false, true)]
        [InlineData(true, false)]
        [InlineData(true, true)]
        public async Task CanDownloadPersonalData(bool twoFactor, bool social)
        {
            // Arrange
            void ConfigureTestServices(IServiceCollection services) =>
                services.SetupTestThirdPartyLogin();

            var client = ServerFactory
                .WithWebHostBuilder(whb => whb.ConfigureTestServices(ConfigureTestServices))
                .CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var guid = Guid.NewGuid();
            var email = userName;

            var index = social
                ? await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email)
                : await UserStories.RegisterNewUserAsync(client, email, "[PLACEHOLDER]-1a");

            if (twoFactor)
            {
                await UserStories.EnableTwoFactorAuthentication(index);
            }

            // Act & Assert
            var jsonData = await UserStories.DownloadPersonalData(index, userName);
            Assert.NotNull(jsonData);
            Assert.True(jsonData.ContainsKey("Id"));
            Assert.NotNull(jsonData["Id"]);
            Assert.True(jsonData.ContainsKey("UserName"));
            Assert.Equal(userName, (string)jsonData["UserName"]);
            Assert.True(jsonData.ContainsKey("Email"));
            Assert.Equal(userName, (string)jsonData["Email"]);
            Assert.True(jsonData.ContainsKey("EmailConfirmed"));
            Assert.False((bool)jsonData["EmailConfirmed"]);
            Assert.True(jsonData.ContainsKey("PhoneNumber"));
            Assert.Equal("null", (string)jsonData["PhoneNumber"]);
            Assert.True(jsonData.ContainsKey("PhoneNumberConfirmed"));
            Assert.False((bool)jsonData["PhoneNumberConfirmed"]);
            Assert.Equal(twoFactor, (bool)jsonData["TwoFactorEnabled"]);

            if (twoFactor)
            {
                Assert.NotNull(jsonData["Authenticator Key"]);
            }
            else
            {
                Assert.Null((string)jsonData["Authenticator Key"]);
            }

            if (social)
            {
                Assert.Equal(userName, (string)jsonData["Contoso external login provider key"]);
            }
            else
            {
                Assert.Null((string)jsonData["Contoso external login provider key"]);
            }
        }

        [Fact]
        public async Task GetOnDownloadPersonalData_ReturnsNotFound()
        {
            // Arrange
            var client = ServerFactory
                .CreateClient();

            await UserStories.RegisterNewUserAsync(client);

            // Act
            var response = await client.GetAsync("/Identity/Account/Manage/DownloadPersonalData");

            // Assert
            Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
        }

        [Fact]
        public async Task CanDeleteUser()
        {
            // Arrange
            var client = ServerFactory
                .CreateClient();

            var userName = $"{Guid.NewGuid()}@example.com";
            var password = $"[PLACEHOLDER]-1a";

            var index = await UserStories.RegisterNewUserAsync(client, userName, password);

            // Act & Assert
            await UserStories.DeleteUser(index, password);
        }

        private void AssertClaimsEqual(ClaimsPrincipal expectedPrincipal, ClaimsPrincipal actualPrincipal, string claimType)
        {
            var expectedPrincipalClaim = expectedPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value;
            var actualPrincipalClaim = actualPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value;
            Assert.Equal(expectedPrincipalClaim, actualPrincipalClaim);
        }

        private void AssertClaimsNotEqual(ClaimsPrincipal expectedPrincipal, ClaimsPrincipal actualPrincipal, string claimType)
        {
            var expectedPrincipalClaim = expectedPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value;
            var actualPrincipalClaim = actualPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value;
            Assert.NotEqual(expectedPrincipalClaim, actualPrincipalClaim);
        }
    }
}
