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

using System.Buffers.Text;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;

namespace Microsoft.AspNetCore.Identity.Test;

public class SignInManagerTest
{
    [Fact]
    public void ConstructorNullChecks()
    {
        Assert.Throws<ArgumentNullException>("userManager", () => new SignInManager<PocoUser>(null, null, null, null, null, null, null));
        var userManager = MockHelpers.MockUserManager<PocoUser>().Object;
        Assert.Throws<ArgumentNullException>("contextAccessor", () => new SignInManager<PocoUser>(userManager, null, null, null, null, null, null));
        var contextAccessor = new Mock<IHttpContextAccessor>();
        var context = new Mock<HttpContext>();
        contextAccessor.Setup(a => a.HttpContext).Returns(context.Object);
        Assert.Throws<ArgumentNullException>("claimsFactory", () => new SignInManager<PocoUser>(userManager, contextAccessor.Object, null, null, null, null, null));
    }

    [Fact]
    public async Task PasswordSignInReturnsLockedOutWhenLockedOut()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var authenticate = new MetricCollector<double>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateDurationName);
        using var signInUserPrincipal = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInsCounterName);

        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(true).Verifiable();

        var context = new Mock<HttpContext>();
        var contextAccessor = new Mock<IHttpContextAccessor>();
        contextAccessor.Setup(a => a.HttpContext).Returns(context.Object);
        var roleManager = MockHelpers.MockRoleManager<PocoRole>();
        var identityOptions = new IdentityOptions();
        var options = new Mock<IOptions<IdentityOptions>>();
        options.Setup(a => a.Value).Returns(identityOptions);
        var claimsFactory = new UserClaimsPrincipalFactory<PocoUser, PocoRole>(manager.Object, roleManager.Object, options.Object);
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = new SignInManager<PocoUser>(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-bogus1", false, false);

        // Assert
        Assert.False(result.Succeeded);
        Assert.True(result.IsLockedOut);
        Assert.Contains($"User is currently locked out.", logger.LogMessages);
        manager.Verify();

        Assert.Collection(authenticate.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertHasDurationAndContainsTags(m.Value, m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.type", "password"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", false),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "locked_out"),
            ]));
        Assert.Empty(signInUserPrincipal.GetMeasurementSnapshot());
    }

    [Fact]
    public async Task CheckPasswordSignInReturnsLockedOutWhenLockedOut()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var checkPasswordSignIn = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.CheckPasswordAttemptsCounterName);

        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(true).Verifiable();

        var context = new Mock<HttpContext>();
        var contextAccessor = new Mock<IHttpContextAccessor>();
        contextAccessor.Setup(a => a.HttpContext).Returns(context.Object);
        var roleManager = MockHelpers.MockRoleManager<PocoRole>();
        var identityOptions = new IdentityOptions();
        var options = new Mock<IOptions<IdentityOptions>>();
        options.Setup(a => a.Value).Returns(identityOptions);
        var claimsFactory = new UserClaimsPrincipalFactory<PocoUser, PocoRole>(manager.Object, roleManager.Object, options.Object);
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = new SignInManager<PocoUser>(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());

        // Act
        var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-bogus1", false);

        // Assert
        Assert.False(result.Succeeded);
        Assert.True(result.IsLockedOut);
        Assert.Contains($"User is currently locked out.", logger.LogMessages);
        manager.Verify();

        Assert.Collection(checkPasswordSignIn.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertContainsTags(m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "locked_out"),
            ]));
    }

    private static Mock<UserManager<PocoUser>> SetupUserManager(
        PocoUser user,
        IMeterFactory meterFactory = null,
        IPasskeyHandler<PocoUser> passkeyHandler = null)
    {
        var manager = MockHelpers.MockUserManager<PocoUser>(meterFactory, passkeyHandler);
        manager.Setup(m => m.FindByNameAsync(user.UserName)).ReturnsAsync(user);
        manager.Setup(m => m.FindByIdAsync(user.Id)).ReturnsAsync(user);
        manager.Setup(m => m.GetUserIdAsync(user)).ReturnsAsync(user.Id.ToString());
        manager.Setup(m => m.GetUserNameAsync(user)).ReturnsAsync(user.UserName);
        return manager;
    }

    private static SignInManager<PocoUser> SetupSignInManager(
        UserManager<PocoUser> manager,
        HttpContext context,
        ILogger logger = null,
        IdentityOptions identityOptions = null,
        IAuthenticationSchemeProvider schemeProvider = null)
    {
        var contextAccessor = new Mock<IHttpContextAccessor>();
        contextAccessor.Setup(a => a.HttpContext).Returns(context);
        var roleManager = MockHelpers.MockRoleManager<PocoRole>();
        identityOptions = identityOptions ?? new IdentityOptions();
        var options = new Mock<IOptions<IdentityOptions>>();
        options.Setup(a => a.Value).Returns(identityOptions);
        var claimsFactory = new UserClaimsPrincipalFactory<PocoUser, PocoRole>(manager, roleManager.Object, options.Object);
        schemeProvider = schemeProvider ?? new MockSchemeProvider();
        var sm = new SignInManager<PocoUser>(
            manager,
            contextAccessor.Object,
            claimsFactory,
            options.Object,
            null,
            schemeProvider,
            new DefaultUserConfirmation<PocoUser>());
        sm.Logger = logger ?? NullLogger<SignInManager<PocoUser>>.Instance;
        return sm;
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task CanPasswordSignIn(bool isPersistent)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        SetupSignIn(context, auth, user.Id, isPersistent, loginProvider: null, amr: "pwd");
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-1a", isPersistent, false);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task CanPasswordSignInWithNoLogger()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        SetupSignIn(context, auth, user.Id, false, loginProvider: null, amr: "pwd");
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-1a", false, false);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task PasswordSignInWorksWithNonTwoFactorStore()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        SetupSignIn(context, auth);
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-1a", false, false);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Theory]
    [InlineData(true, true)]
    [InlineData(true, false)]
    [InlineData(false, false)]
    public async Task CheckPasswordOnlyResetLockoutWhenTfaNotEnabledOrRemembered(bool tfaEnabled, bool tfaRemembered)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.SupportsUserTwoFactor).Returns(tfaEnabled).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);

        if (tfaEnabled)
        {
            manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(true).Verifiable();
            manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).ReturnsAsync(new string[1] { "Fake" }).Verifiable();
        }

        if (tfaRemembered)
        {
            var id = new ClaimsIdentity(IdentityConstants.TwoFactorRememberMeScheme);
            id.AddClaim(new Claim(ClaimTypes.Name, user.Id));
            auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorRememberMeScheme))
                .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(id), null, IdentityConstants.TwoFactorRememberMeScheme))).Verifiable();
        }

        if (!tfaEnabled || tfaRemembered)
        {
            manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
        }

        // Act
        var helper = SetupSignInManager(manager.Object, context);
        var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-1a", false);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
    }

    [Fact]
    public async Task CheckPasswordAlwaysResetLockoutWhenQuirked()
    {
        AppContext.SetSwitch("Microsoft.AspNetCore.Identity.CheckPasswordSignInAlwaysResetLockoutOnSuccess", true);

        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();

        var context = new DefaultHttpContext();
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-1a", false);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();

        AppContext.SetSwitch("Microsoft.AspNetCore.Identity.CheckPasswordSignInAlwaysResetLockoutOnSuccess", false);
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task PasswordSignInRequiresVerification(bool supportsLockout)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable();
        if (supportsLockout)
        {
            manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        }
        IList<string> providers = new List<string>();
        providers.Add("PhoneNumber");
        manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).Returns(Task.FromResult(providers)).Verifiable();
        manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable();
        manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(true).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).ReturnsAsync(new string[1] { "Fake" }).Verifiable();
        var context = new DefaultHttpContext();
        var helper = SetupSignInManager(manager.Object, context);
        var auth = MockAuth(context);
        auth.Setup(a => a.SignInAsync(context, IdentityConstants.TwoFactorUserIdScheme,
            It.Is<ClaimsPrincipal>(id => id.FindFirstValue(ClaimTypes.Name) == user.Id),
            It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-1a", false, false);

        // Assert
        Assert.False(result.Succeeded);
        Assert.True(result.RequiresTwoFactor);
        manager.Verify();
        auth.Verify();
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task ExternalSignInRequiresVerificationIfNotBypassed(bool bypass)
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var authenticate = new MetricCollector<double>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateDurationName);
        using var signInUserPrincipal = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInsCounterName);

        var user = new PocoUser { UserName = "Foo" };
        const string loginProvider = "login";
        const string providerKey = "fookey";
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        manager.Setup(m => m.SupportsUserLockout).Returns(false).Verifiable();
        manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey)).ReturnsAsync(user).Verifiable();
        if (!bypass)
        {
            IList<string> providers = new List<string>();
            providers.Add("PhoneNumber");
            manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).Returns(Task.FromResult(providers)).Verifiable();
            manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable();
            manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(true).Verifiable();
        }
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);

        if (bypass)
        {
            SetupSignIn(context, auth, user.Id, false, loginProvider);
        }
        else
        {
            auth.Setup(a => a.SignInAsync(context, IdentityConstants.TwoFactorUserIdScheme,
                It.Is<ClaimsPrincipal>(id => id.FindFirstValue(ClaimTypes.Name) == user.Id),
                It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
        }

        // Act
        var result = await helper.ExternalLoginSignInAsync(loginProvider, providerKey, isPersistent: false, bypassTwoFactor: bypass);

        // Assert
        Assert.Equal(bypass, result.Succeeded);
        Assert.Equal(!bypass, result.RequiresTwoFactor);
        manager.Verify();
        auth.Verify();

        if (bypass)
        {
            Assert.Collection(authenticate.GetMeasurementSnapshot(),
                m => MetricsHelpers.AssertHasDurationAndContainsTags(m.Value, m.Tags,
                [
                    KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                    KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                    KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.type", "external"),
                    KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", false),
                    KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "success"),
                ]));
            Assert.Collection(signInUserPrincipal.GetMeasurementSnapshot(),
                m => MetricsHelpers.AssertContainsTags(m.Tags,
                [
                    KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                    KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                    KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", false),
                ]));
        }
        else
        {
            Assert.Collection(authenticate.GetMeasurementSnapshot(),
                m => MetricsHelpers.AssertHasDurationAndContainsTags(m.Value, m.Tags,
                [
                    KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                    KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                    KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.type", "external"),
                    KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", false),
                    KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "requires_two_factor"),
                ]));
            Assert.Empty(signInUserPrincipal.GetMeasurementSnapshot());
        }
    }

    [Fact]
    public async Task CanPasskeySignIn()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var authenticate = new MetricCollector<double>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateDurationName);
        using var signInUserPrincipal = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInsCounterName);

        var user = new PocoUser { UserName = "Foo" };
        var passkey = new UserPasskeyInfo(null, null, default, 0, null, false, false, false, null, null);
        var assertionResult = PasskeyAssertionResult.Success(passkey, user);
        var passkeyHandler = new Mock<IPasskeyHandler<PocoUser>>();
        var expectedOptionsJson = "<some-options-json>";
        passkeyHandler
            .Setup(h => h.MakeRequestOptionsAsync(user, It.IsAny<HttpContext>()))
            .Returns(Task.FromResult(new PasskeyRequestOptionsResult
            {
                AssertionState = "<some-assertion-state>",
                RequestOptionsJson = expectedOptionsJson,
            }));
        passkeyHandler
            .Setup(h => h.PerformAssertionAsync(It.IsAny<PasskeyAssertionContext>()))
            .Returns(Task.FromResult(assertionResult));
        var manager = SetupUserManager(user, meterFactory: testMeterFactory, passkeyHandler: passkeyHandler.Object);
        manager
            .Setup(m => m.AddOrUpdatePasskeyAsync(user, passkey))
            .Returns(Task.FromResult(IdentityResult.Success))
            .Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        SetupSignIn(context, auth, user.Id, isPersistent: false, loginProvider: null);
        SetupPasskeyAuth(context, auth);
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var optionsJson = await helper.MakePasskeyRequestOptionsAsync(user);
        var signInResult = await helper.PasskeySignInAsync(credentialJson: "<some-passkey>");

        // Assert
        Assert.Equal(expectedOptionsJson, optionsJson);
        Assert.True(assertionResult.Succeeded);
        Assert.Same(SignInResult.Success, signInResult);
        manager.Verify();
        auth.Verify();

        Assert.Collection(authenticate.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertHasDurationAndContainsTags(m.Value, m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.type", "passkey"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", false),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "success"),
            ]));
        Assert.Collection(signInUserPrincipal.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertContainsTags(m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", false),
            ]));
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task CanRequireConfirmedEmailForPasskeySignIn(bool confirmed)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var passkey = new UserPasskeyInfo(null, null, default, 0, null, false, false, false, null, null);
        var assertionResult = PasskeyAssertionResult.Success(passkey, user);
        var passkeyHandler = new Mock<IPasskeyHandler<PocoUser>>();
        passkeyHandler
            .Setup(h => h.MakeRequestOptionsAsync(user, It.IsAny<HttpContext>()))
            .Returns(Task.FromResult(new PasskeyRequestOptionsResult
            {
                AssertionState = "<some-assertion-state>",
                RequestOptionsJson = "<some-options-json>",
            }));
        passkeyHandler
            .Setup(h => h.PerformAssertionAsync(It.IsAny<PasskeyAssertionContext>()))
            .Returns(Task.FromResult(assertionResult));
        var manager = SetupUserManager(user, passkeyHandler: passkeyHandler.Object);
        manager.Setup(m => m.IsEmailConfirmedAsync(user)).ReturnsAsync(confirmed).Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        if (confirmed)
        {
            manager
                .Setup(m => m.AddOrUpdatePasskeyAsync(user, passkey))
                .Returns(Task.FromResult(IdentityResult.Success))
                .Verifiable();
            SetupSignIn(context, auth, user.Id, isPersistent: false, loginProvider: null);
        }
        SetupPasskeyAuth(context, auth);

        var identityOptions = new IdentityOptions();
        identityOptions.SignIn.RequireConfirmedEmail = true;
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = SetupSignInManager(manager.Object, context, logger, identityOptions);

        // Act
        await helper.MakePasskeyRequestOptionsAsync(user);
        var signInResult = await helper.PasskeySignInAsync(credentialJson: "<some-passkey>");

        // Assert
        Assert.Equal(confirmed, signInResult.Succeeded);
        Assert.NotEqual(confirmed, signInResult.IsNotAllowed);

        var message = $"User cannot sign in without a confirmed email.";
        if (!confirmed)
        {
            Assert.Contains(message, logger.LogMessages);
        }
        else
        {
            Assert.DoesNotContain(message, logger.LogMessages);
        }

        manager.Verify();
        auth.Verify();
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task CanRequireConfirmedPhoneNumberForPasskeySignIn(bool confirmed)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var passkey = new UserPasskeyInfo(null, null, default, 0, null, false, false, false, null, null);
        var assertionResult = PasskeyAssertionResult.Success(passkey, user);
        var passkeyHandler = new Mock<IPasskeyHandler<PocoUser>>();
        passkeyHandler
            .Setup(h => h.MakeRequestOptionsAsync(user, It.IsAny<HttpContext>()))
            .Returns(Task.FromResult(new PasskeyRequestOptionsResult
            {
                AssertionState = "<some-assertion-state>",
                RequestOptionsJson = "<some-options-json>",
            }));
        passkeyHandler
            .Setup(h => h.PerformAssertionAsync(It.IsAny<PasskeyAssertionContext>()))
            .Returns(Task.FromResult(assertionResult));
        var manager = SetupUserManager(user, passkeyHandler: passkeyHandler.Object);
        manager.Setup(m => m.IsPhoneNumberConfirmedAsync(user)).ReturnsAsync(confirmed).Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        if (confirmed)
        {
            manager
                .Setup(m => m.AddOrUpdatePasskeyAsync(user, passkey))
                .Returns(Task.FromResult(IdentityResult.Success))
                .Verifiable();
            SetupSignIn(context, auth, user.Id, isPersistent: false, loginProvider: null);
        }
        SetupPasskeyAuth(context, auth);

        var identityOptions = new IdentityOptions();
        identityOptions.SignIn.RequireConfirmedPhoneNumber = true;
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = SetupSignInManager(manager.Object, context, logger, identityOptions);

        // Act
        await helper.MakePasskeyRequestOptionsAsync(user);
        var signInResult = await helper.PasskeySignInAsync(credentialJson: "<some-passkey>");

        // Assert
        Assert.Equal(confirmed, signInResult.Succeeded);
        Assert.NotEqual(confirmed, signInResult.IsNotAllowed);

        var message = $"User cannot sign in without a confirmed phone number.";
        if (!confirmed)
        {
            Assert.Contains(message, logger.LogMessages);
        }
        else
        {
            Assert.DoesNotContain(message, logger.LogMessages);
        }

        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task PasskeySignInReturnsLockedOutWhenLockedOut()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var passkey = new UserPasskeyInfo(null, null, default, 0, null, false, false, false, null, null);
        var assertionResult = PasskeyAssertionResult.Success(passkey, user);
        var passkeyHandler = new Mock<IPasskeyHandler<PocoUser>>();
        passkeyHandler
            .Setup(h => h.MakeRequestOptionsAsync(user, It.IsAny<HttpContext>()))
            .Returns(Task.FromResult(new PasskeyRequestOptionsResult
            {
                AssertionState = "<some-assertion-state>",
                RequestOptionsJson = "<some-options-json>",
            }));
        passkeyHandler
            .Setup(h => h.PerformAssertionAsync(It.IsAny<PasskeyAssertionContext>()))
            .Returns(Task.FromResult(assertionResult));
        var manager = SetupUserManager(user, passkeyHandler: passkeyHandler.Object);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(true).Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        SetupPasskeyAuth(context, auth);

        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = SetupSignInManager(manager.Object, context, logger);

        // Act
        await helper.MakePasskeyRequestOptionsAsync(user);
        var signInResult = await helper.PasskeySignInAsync(credentialJson: "<some-passkey>");

        // Assert
        Assert.False(signInResult.Succeeded);
        Assert.True(signInResult.IsLockedOut);
        Assert.Contains($"User is currently locked out.", logger.LogMessages);
        manager.Verify();
        auth.Verify();
    }

    private static void SetupPasskeyAuth(HttpContext context, Mock<IAuthenticationService> auth)
    {
        // Calling AuthenticateAsync will return a failure result
        // unless SignInAsync has been called first.
        var failedAuthenticateResult = AuthenticateResult.Fail("Not currently signed in.");
        var authenticateResult = failedAuthenticateResult;

        auth.Setup(a => a.SignInAsync(
            context,
            IdentityConstants.TwoFactorUserIdScheme,
            It.IsAny<ClaimsPrincipal>(),
            It.IsAny<AuthenticationProperties>()))
            .Callback((HttpContext context, string scheme, ClaimsPrincipal claimsPrincipal, AuthenticationProperties authenticationProperties) =>
            {
                var authenticationTicket = new AuthenticationTicket(
                    claimsPrincipal,
                    authenticationProperties,
                    IdentityConstants.TwoFactorUserIdScheme);
                authenticateResult = AuthenticateResult.Success(authenticationTicket);
            })
            .Returns(Task.CompletedTask)
            .Verifiable();

        auth.Setup(a => a.SignOutAsync(
            context,
            IdentityConstants.TwoFactorUserIdScheme,
            It.IsAny<AuthenticationProperties>()))
            .Callback(() =>
            {
                authenticateResult = failedAuthenticateResult;
            })
            .Returns(Task.CompletedTask)
            .Verifiable();

        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .Returns(() => Task.FromResult(authenticateResult))
            .Verifiable();
    }

    private class GoodTokenProvider : AuthenticatorTokenProvider<PocoUser>
    {
        public override Task<bool> ValidateAsync(string purpose, string token, UserManager<PocoUser> manager, PocoUser user)
        {
            return Task.FromResult(true);
        }
    }

    [Theory]
    [InlineData(null, true, true)]
    [InlineData("Authenticator", false, true)]
    [InlineData("Gooblygook", true, false)]
    [InlineData("--", false, false)]
    public async Task CanTwoFactorAuthenticatorSignIn(string providerName, bool isPersistent, bool rememberClient)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        const string code = "3123";
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, providerName ?? TokenOptions.DefaultAuthenticatorProvider, code)).ReturnsAsync(true).Verifiable();
        manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var twoFactorInfo = new SignInManager<PocoUser>.TwoFactorAuthenticationInfo { User = user };
        if (providerName != null)
        {
            helper.Options.Tokens.AuthenticatorTokenProvider = providerName;
        }
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, null);
        SetupSignIn(context, auth, user.Id, isPersistent);
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();
        if (rememberClient)
        {
            auth.Setup(a => a.SignInAsync(context,
                IdentityConstants.TwoFactorRememberMeScheme,
                It.Is<ClaimsPrincipal>(i => i.FindFirstValue(ClaimTypes.Name) == user.Id
                    && i.Identities.First().AuthenticationType == IdentityConstants.TwoFactorRememberMeScheme),
                It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
        }

        // Act
        var result = await helper.TwoFactorAuthenticatorSignInAsync(code, isPersistent, rememberClient);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task TwoFactorAuthenticatorSignInFailWithoutLockout()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        string providerName = "Authenticator";
        const string code = "3123";
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(false).Verifiable();
        manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, providerName ?? TokenOptions.DefaultAuthenticatorProvider, code)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.AccessFailedAsync(user)).Throws(new Exception("Should not get called"));

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var twoFactorInfo = new SignInManager<PocoUser>.TwoFactorAuthenticationInfo { User = user };
        if (providerName != null)
        {
            helper.Options.Tokens.AuthenticatorTokenProvider = providerName;
        }
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, null);
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();

        // Act
        var result = await helper.TwoFactorAuthenticatorSignInAsync(code, isPersistent: false, rememberClient: false);

        // Assert
        Assert.False(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task TwoFactorAuthenticatorSignInAsyncReturnsLockedOut()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        string providerName = "Authenticator";
        const string code = "3123";
        var manager = SetupUserManager(user);
        var lockedout = false;
        manager.Setup(m => m.AccessFailedAsync(user)).Returns(() =>
        {
            lockedout = true;
            return Task.FromResult(IdentityResult.Success);
        }).Verifiable();
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, providerName ?? TokenOptions.DefaultAuthenticatorProvider, code)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).Returns(() => Task.FromResult(lockedout));

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var twoFactorInfo = new SignInManager<PocoUser>.TwoFactorAuthenticationInfo { User = user };
        if (providerName != null)
        {
            helper.Options.Tokens.AuthenticatorTokenProvider = providerName;
        }
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, null);
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();

        // Act
        var result = await helper.TwoFactorAuthenticatorSignInAsync(code, isPersistent: false, rememberClient: false);

        // Assert
        Assert.True(result.IsLockedOut);
        manager.Verify();
        auth.Verify();
    }

    [Theory]
    [InlineData(true, true, true)]
    [InlineData(true, true, false)]
    [InlineData(true, false, true)]
    [InlineData(true, false, false)]
    [InlineData(false, true, true)]
    [InlineData(false, true, false)]
    [InlineData(false, false, true)]
    [InlineData(false, false, false)]
    public async Task IsTwoFactorEnabled(bool userManagerSupportsTwoFactor, bool userTwoFactorEnabled, bool hasValidProviders)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserTwoFactor).Returns(userManagerSupportsTwoFactor).Verifiable();
        if (userManagerSupportsTwoFactor)
        {
            manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(userTwoFactorEnabled).Verifiable();
            if (userTwoFactorEnabled)
            {
                manager
                    .Setup(m => m.GetValidTwoFactorProvidersAsync(user))
                    .ReturnsAsync(hasValidProviders ? new string[1] { "Fake" } : Array.Empty<string>())
                    .Verifiable();
            }
        }

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var result = await helper.IsTwoFactorEnabledAsync(user);

        // Assert
        var expected = userManagerSupportsTwoFactor && userTwoFactorEnabled && hasValidProviders;
        Assert.Equal(expected, result);
        manager.Verify();
        auth.Verify();
    }

    [Theory]
    [InlineData(true, true)]
    [InlineData(true, false)]
    [InlineData(false, true)]
    [InlineData(false, false)]
    public async Task CanTwoFactorRecoveryCodeSignIn(bool supportsLockout, bool externalLogin)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        const string bypassCode = "someCode";
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable();
        manager.Setup(m => m.RedeemTwoFactorRecoveryCodeAsync(user, bypassCode)).ReturnsAsync(IdentityResult.Success).Verifiable();
        if (supportsLockout)
        {
            manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
        }
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var twoFactorInfo = new SignInManager<PocoUser>.TwoFactorAuthenticationInfo { User = user };
        var loginProvider = "loginprovider";
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null);
        if (externalLogin)
        {
            auth.Setup(a => a.SignInAsync(context,
                IdentityConstants.ApplicationScheme,
                It.Is<ClaimsPrincipal>(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider
                    && i.FindFirstValue(ClaimTypes.NameIdentifier) == user.Id),
                It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
            auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ExternalScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
            auth.Setup(a => a.SignOutAsync(context, IdentityConstants.TwoFactorUserIdScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
        }
        else
        {
            SetupSignIn(context, auth, user.Id);
        }
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();

        // Act
        var result = await helper.TwoFactorRecoveryCodeSignInAsync(bypassCode);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Theory]
    [InlineData(true, true)]
    [InlineData(true, false)]
    [InlineData(false, true)]
    [InlineData(false, false)]
    public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        const string loginProvider = "login";
        const string providerKey = "fookey";
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable();
        if (supportsLockout)
        {
            manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        }
        manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey)).ReturnsAsync(user).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        SetupSignIn(context, auth, user.Id, isPersistent, loginProvider);

        // Act
        var result = await helper.ExternalLoginSignInAsync(loginProvider, providerKey, isPersistent);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task SignInAsync_Failure()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var signInUserPrincipal = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInsCounterName);

        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        auth.Setup(a => a.SignInAsync(context, IdentityConstants.ApplicationScheme, It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
            .Throws(new InvalidOperationException("SignInAsync failed")).Verifiable();

        // Act
        await Assert.ThrowsAsync<InvalidOperationException>(() => helper.SignInAsync(user, isPersistent: false));

        // Assert
        manager.Verify();
        auth.Verify();

        Assert.Collection(signInUserPrincipal.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertContainsTags(m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("error.type", "System.InvalidOperationException"),
            ]));
    }

    [Theory]
    [InlineData(true, true)]
    [InlineData(true, false)]
    [InlineData(false, true)]
    [InlineData(false, false)]
    public async Task CanResignIn(bool isPersistent, bool externalLogin)
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var authenticate = new MetricCollector<double>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateDurationName);

        var user = new PocoUser { UserName = "Foo" };
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var loginProvider = "loginprovider";
        var id = new ClaimsIdentity("authscheme");
        if (externalLogin)
        {
            id.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider));
        }

        var claimsPrincipal = new ClaimsPrincipal(id);
        var properties = new AuthenticationProperties { IsPersistent = isPersistent };
        var authResult = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, properties, "authscheme"));
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme))
            .Returns(Task.FromResult(authResult)).Verifiable();
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        manager.Setup(m => m.GetUserId(claimsPrincipal)).Returns(user.Id.ToString());
        var signInManager = new Mock<SignInManager<PocoUser>>(manager.Object,
            new HttpContextAccessor { HttpContext = context },
            new Mock<IUserClaimsPrincipalFactory<PocoUser>>().Object,
            null, null, new Mock<IAuthenticationSchemeProvider>().Object, null)
        { CallBase = true };

        signInManager.Setup(s => s.SignInWithClaimsAsync(user,
                It.Is<AuthenticationProperties>(properties => properties.IsPersistent == isPersistent),
                It.Is<IEnumerable<Claim>>(claims => !externalLogin ||
                    claims.Any(claim => claim.Type == ClaimTypes.AuthenticationMethod && claim.Value == loginProvider))))
            .Returns(Task.FromResult(0)).Verifiable();

        signInManager.Object.Context = context;

        // Act
        await signInManager.Object.RefreshSignInAsync(user);

        // Assert
        auth.Verify();
        signInManager.Verify();

        Assert.Collection(authenticate.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertHasDurationAndContainsTags(m.Value, m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", isPersistent),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "success"),
            ]));
    }

    [Fact]
    public async Task ResignInNoOpsAndLogsErrorIfNotAuthenticated()
    {
        var testMeterFactory = new TestMeterFactory();
        using var authenticate = new MetricCollector<double>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateDurationName);
        using var signInUserPrincipal = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInsCounterName);

        var user = new PocoUser { UserName = "Foo" };
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var signInManager = new Mock<SignInManager<PocoUser>>(manager.Object,
            new HttpContextAccessor { HttpContext = context },
            new Mock<IUserClaimsPrincipalFactory<PocoUser>>().Object,
            null, logger, new Mock<IAuthenticationSchemeProvider>().Object, null)
        { CallBase = true };
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme))
            .Returns(Task.FromResult(AuthenticateResult.NoResult())).Verifiable();

        await signInManager.Object.RefreshSignInAsync(user);

        Assert.Contains("RefreshSignInAsync prevented because the user is not currently authenticated. Use SignInAsync instead for initial sign in.", logger.LogMessages);
        auth.Verify();
        signInManager.Verify(s => s.SignInWithClaimsAsync(It.IsAny<PocoUser>(), It.IsAny<AuthenticationProperties>(), It.IsAny<IEnumerable<Claim>>()),
            Times.Never());

        Assert.Collection(authenticate.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertHasDurationAndContainsTags(m.Value, m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "failure"),
            ]));
        Assert.Empty(signInUserPrincipal.GetMeasurementSnapshot());
    }

    [Fact]
    public async Task ResignInNoOpsAndLogsErrorIfAuthenticatedWithDifferentUser()
    {
        var user = new PocoUser { UserName = "Foo" };
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var manager = SetupUserManager(user);
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var signInManager = new Mock<SignInManager<PocoUser>>(manager.Object,
            new HttpContextAccessor { HttpContext = context },
            new Mock<IUserClaimsPrincipalFactory<PocoUser>>().Object,
            null, logger, new Mock<IAuthenticationSchemeProvider>().Object, null)
        { CallBase = true };
        var id = new ClaimsIdentity("authscheme");
        var claimsPrincipal = new ClaimsPrincipal(id);
        var authResult = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties(), "authscheme"));
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme))
            .Returns(Task.FromResult(authResult)).Verifiable();
        manager.Setup(m => m.GetUserId(claimsPrincipal)).Returns("different");

        await signInManager.Object.RefreshSignInAsync(user);

        Assert.Contains("RefreshSignInAsync prevented because currently authenticated user has a different UserId. Use SignInAsync instead to change users.", logger.LogMessages);
        auth.Verify();
        signInManager.Verify(s => s.SignInWithClaimsAsync(It.IsAny<PocoUser>(), It.IsAny<AuthenticationProperties>(), It.IsAny<IEnumerable<Claim>>()),
            Times.Never());
    }

    [Theory]
    [InlineData(true, true, true, true)]
    [InlineData(true, true, false, true)]
    [InlineData(true, false, true, true)]
    [InlineData(true, false, false, true)]
    [InlineData(false, true, true, true)]
    [InlineData(false, true, false, true)]
    [InlineData(false, false, true, true)]
    [InlineData(false, false, false, true)]
    [InlineData(true, true, true, false)]
    [InlineData(true, true, false, false)]
    [InlineData(true, false, true, false)]
    [InlineData(true, false, false, false)]
    [InlineData(false, true, true, false)]
    [InlineData(false, true, false, false)]
    [InlineData(false, false, true, false)]
    [InlineData(false, false, false, false)]
    public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        var provider = "twofactorprovider";
        var code = "123456";
        manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable();
        if (supportsLockout)
        {
            manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
            manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
        }
        manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(true).Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var twoFactorInfo = new SignInManager<PocoUser>.TwoFactorAuthenticationInfo { User = user };
        var loginProvider = "loginprovider";
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null);
        if (externalLogin)
        {
            auth.Setup(a => a.SignInAsync(context,
                IdentityConstants.ApplicationScheme,
                It.Is<ClaimsPrincipal>(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider
                    && i.FindFirstValue("amr") == "mfa"
                    && i.FindFirstValue(ClaimTypes.NameIdentifier) == user.Id),
                It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
            // REVIEW: restore ability to test is persistent
            //It.Is<AuthenticationProperties>(v => v.IsPersistent == isPersistent))).Verifiable();
            auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ExternalScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
            auth.Setup(a => a.SignOutAsync(context, IdentityConstants.TwoFactorUserIdScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
        }
        else
        {
            SetupSignIn(context, auth, user.Id, isPersistent, null, "mfa");
        }
        if (rememberClient)
        {
            auth.Setup(a => a.SignInAsync(context,
                IdentityConstants.TwoFactorRememberMeScheme,
                It.Is<ClaimsPrincipal>(i => i.FindFirstValue(ClaimTypes.Name) == user.Id
                    && i.Identities.First().AuthenticationType == IdentityConstants.TwoFactorRememberMeScheme),
                It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
            //It.Is<AuthenticationProperties>(v => v.IsPersistent == true))).Returns(Task.FromResult(0)).Verifiable();
        }
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();

        // Act
        var result = await helper.TwoFactorSignInAsync(provider, code, isPersistent, rememberClient);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task TwoFactorSignInAsyncReturnsLockedOut()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        var provider = "twofactorprovider";
        var code = "123456";
        var lockedout = false;
        manager.Setup(m => m.AccessFailedAsync(user)).Returns(() =>
        {
            lockedout = true;
            return Task.FromResult(IdentityResult.Success);
        }).Verifiable();
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).Returns(() => Task.FromResult(lockedout));
        manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(false).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, loginProvider: null);

        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();

        // Act
        var result = await helper.TwoFactorSignInAsync(provider, code, isPersistent: false, rememberClient: false);

        // Assert
        Assert.True(result.IsLockedOut);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task RememberClientStoresUserId()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var rememberTwoFactorClient = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.RememberedTwoFactorCounterName);

        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        auth.Setup(a => a.SignInAsync(
            context,
            IdentityConstants.TwoFactorRememberMeScheme,
            It.Is<ClaimsPrincipal>(i => i.FindFirstValue(ClaimTypes.Name) == user.Id
                && i.Identities.First().AuthenticationType == IdentityConstants.TwoFactorRememberMeScheme),
            It.Is<AuthenticationProperties>(v => v.IsPersistent == true))).Returns(Task.FromResult(0)).Verifiable();

        // Act
        await helper.RememberTwoFactorClientAsync(user);

        // Assert
        manager.Verify();
        auth.Verify();

        Assert.Collection(rememberTwoFactorClient.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertContainsTags(m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.TwoFactorRememberMe"),
            ]));
    }

    [Fact]
    public async Task ForgetTwoFactorClient()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var forgetTwoFactorClient = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.ForgottenTwoFactorCounterName);

        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        auth.Setup(a => a.SignOutAsync(
            context,
            IdentityConstants.TwoFactorRememberMeScheme,
            It.IsAny<AuthenticationProperties>())).Returns(Task.FromException(new InvalidOperationException())).Verifiable();

        // Act
        await Assert.ThrowsAsync<InvalidOperationException>(() => helper.ForgetTwoFactorClientAsync());

        // Assert
        manager.Verify();
        auth.Verify();

        Assert.Collection(forgetTwoFactorClient.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertContainsTags(m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.TwoFactorRememberMe"),
                KeyValuePair.Create<string, object>("error.type", "System.InvalidOperationException"),
            ]));
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task RememberBrowserSkipsTwoFactorVerificationSignIn(bool isPersistent)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(true).Verifiable();
        IList<string> providers = new List<string>();
        providers.Add("PhoneNumber");
        manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).Returns(Task.FromResult(providers)).Verifiable();
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        SetupSignIn(context, auth);
        var id = new ClaimsIdentity(IdentityConstants.TwoFactorRememberMeScheme);
        id.AddClaim(new Claim(ClaimTypes.Name, user.Id));
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorRememberMeScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(id), null, IdentityConstants.TwoFactorRememberMeScheme))).Verifiable();
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-1a", isPersistent, false);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    private Mock<IAuthenticationService> MockAuth(HttpContext context)
    {
        var auth = new Mock<IAuthenticationService>();
        context.RequestServices = new ServiceCollection().AddSingleton(auth.Object).BuildServiceProvider();
        return auth;
    }

    [Fact]
    public async Task SignOutCallsContextResponseSignOut_Success()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var signOutUserPrincipal = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignOutsCounterName);

        var manager = MockHelpers.TestUserManager<PocoUser>(meterFactory: testMeterFactory);
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ApplicationScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
        auth.Setup(a => a.SignOutAsync(context, IdentityConstants.TwoFactorUserIdScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
        auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ExternalScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
        var helper = SetupSignInManager(manager, context, null, manager.Options);

        // Act
        await helper.SignOutAsync();

        // Assert
        auth.Verify();

        Assert.Collection(signOutUserPrincipal.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertContainsTags(m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
            ]));
    }

    [Fact]
    public async Task SignOutCallsContextResponseSignOut_Failure()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var signOutUserPrincipal = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignOutsCounterName);

        var manager = MockHelpers.TestUserManager<PocoUser>(meterFactory: testMeterFactory);
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ApplicationScheme, It.IsAny<AuthenticationProperties>())).Returns(Task.FromException(new InvalidOperationException("error!"))).Verifiable();
        var helper = SetupSignInManager(manager, context, null, manager.Options);

        // Act
        await Assert.ThrowsAsync<InvalidOperationException>(() => helper.SignOutAsync());

        // Assert
        Assert.Collection(signOutUserPrincipal.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertContainsTags(m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("error.type", "System.InvalidOperationException"),
            ]));
    }

    [Fact]
    public async Task PasswordSignInFailsWithWrongPassword()
    {
        // Setup
        var testMeterFactory = new TestMeterFactory();
        using var authenticate = new MetricCollector<double>(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateDurationName);

        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user, meterFactory: testMeterFactory);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-bogus1")).ReturnsAsync(false).Verifiable();
        var context = new Mock<HttpContext>();
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = SetupSignInManager(manager.Object, context.Object, logger);

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-bogus1", false, false);
        var checkResult = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-bogus1", false);

        // Assert
        Assert.False(result.Succeeded);
        Assert.False(checkResult.Succeeded);
        Assert.Contains($"User failed to provide the correct password.", logger.LogMessages);
        manager.Verify();
        context.Verify();

        Assert.Collection(authenticate.GetMeasurementSnapshot(),
            m => MetricsHelpers.AssertHasDurationAndContainsTags(m.Value, m.Tags,
            [
                KeyValuePair.Create<string, object>("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.scheme", "Identity.Application"),
                KeyValuePair.Create<string, object>("aspnetcore.authentication.is_persistent", false),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.result", "failure"),
                KeyValuePair.Create<string, object>("aspnetcore.identity.sign_in.type", "password"),
            ]));
    }

    [Fact]
    public async Task PasswordSignInFailsWithUnknownUser()
    {
        // Setup
        var manager = MockHelpers.MockUserManager<PocoUser>();
        manager.Setup(m => m.FindByNameAsync("unknown-username")).ReturnsAsync(default(PocoUser)).Verifiable();
        var context = new Mock<HttpContext>();
        var helper = SetupSignInManager(manager.Object, context.Object);

        // Act
        var result = await helper.PasswordSignInAsync("unknown-username", "[PLACEHOLDER]-bogus1", false, false);

        // Assert
        Assert.False(result.Succeeded);
        manager.Verify();
        context.Verify();
    }

    [Fact]
    public async Task PasswordSignInFailsWithWrongPasswordCanAccessFailedAndLockout()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        var lockedout = false;
        manager.Setup(m => m.AccessFailedAsync(user)).Returns(() =>
        {
            lockedout = true;
            return Task.FromResult(IdentityResult.Success);
        }).Verifiable();
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).Returns(() => Task.FromResult(lockedout));
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-bogus1")).ReturnsAsync(false).Verifiable();
        var context = new Mock<HttpContext>();
        var helper = SetupSignInManager(manager.Object, context.Object);

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-bogus1", false, true);

        // Assert
        Assert.False(result.Succeeded);
        Assert.True(result.IsLockedOut);
        manager.Verify();
    }

    [Fact]
    public async Task CheckPasswordSignInFailsWithWrongPasswordCanAccessFailedAndLockout()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        var lockedout = false;
        manager.Setup(m => m.AccessFailedAsync(user)).Returns(() =>
        {
            lockedout = true;
            return Task.FromResult(IdentityResult.Success);
        }).Verifiable();
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).Returns(() => Task.FromResult(lockedout));
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-bogus1")).ReturnsAsync(false).Verifiable();
        var context = new Mock<HttpContext>();
        var helper = SetupSignInManager(manager.Object, context.Object);

        // Act
        var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-bogus1", true);

        // Assert
        Assert.False(result.Succeeded);
        Assert.True(result.IsLockedOut);
        manager.Verify();
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task CanRequireConfirmedEmailForPasswordSignIn(bool confirmed)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.IsEmailConfirmedAsync(user)).ReturnsAsync(confirmed).Verifiable();
        if (confirmed)
        {
            manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        }
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        if (confirmed)
        {
            manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
            SetupSignIn(context, auth, user.Id, isPersistent: null, loginProvider: null, amr: "pwd");
        }
        var identityOptions = new IdentityOptions();
        identityOptions.SignIn.RequireConfirmedEmail = true;
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = SetupSignInManager(manager.Object, context, logger, identityOptions);

        // Act
        var result = await helper.PasswordSignInAsync(user, "[PLACEHOLDER]-1a", false, false);

        // Assert

        Assert.Equal(confirmed, result.Succeeded);
        Assert.NotEqual(confirmed, result.IsNotAllowed);

        var message = $"User cannot sign in without a confirmed email.";
        if (!confirmed)
        {
            Assert.Contains(message, logger.LogMessages);
        }
        else
        {
            Assert.DoesNotContain(message, logger.LogMessages);
        }

        manager.Verify();
        auth.Verify();
    }

    private static void SetupSignIn(HttpContext context, Mock<IAuthenticationService> auth, string userId = null, bool? isPersistent = null, string loginProvider = null, string amr = null)
    {
        auth.Setup(a => a.SignInAsync(context,
            IdentityConstants.ApplicationScheme,
            It.Is<ClaimsPrincipal>(id =>
                (userId == null || id.FindFirstValue(ClaimTypes.NameIdentifier) == userId) &&
                (loginProvider == null || id.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider) &&
                (amr == null || id.FindFirstValue("amr") == amr)),
            It.Is<AuthenticationProperties>(v => isPersistent == null || v.IsPersistent == isPersistent))).Returns(Task.FromResult(0)).Verifiable();
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task CanRequireConfirmedPhoneNumberForPasswordSignIn(bool confirmed)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.IsPhoneNumberConfirmedAsync(user)).ReturnsAsync(confirmed).Verifiable();
        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        if (confirmed)
        {
            manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
            SetupSignIn(context, auth, user.Id, isPersistent: null, loginProvider: null, amr: "pwd");
        }

        var identityOptions = new IdentityOptions();
        identityOptions.SignIn.RequireConfirmedPhoneNumber = true;
        var logger = new TestLogger<SignInManager<PocoUser>>();
        var helper = SetupSignInManager(manager.Object, context, logger, identityOptions);

        // Act
        var result = await helper.PasswordSignInAsync(user, "[PLACEHOLDER]-1a", false, false);

        // Assert
        Assert.Equal(confirmed, result.Succeeded);
        Assert.NotEqual(confirmed, result.IsNotAllowed);

        var message = $"User cannot sign in without a confirmed phone number.";
        if (!confirmed)
        {
            Assert.Contains(message, logger.LogMessages);
        }
        else
        {
            Assert.DoesNotContain(message, logger.LogMessages);
        }

        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task GetExternalLoginInfoAsyncReturnsCorrectProviderDisplayName()
    {
        // Arrange
        var user = new PocoUser { Id = "foo", UserName = "Foo" };
        var userManager = SetupUserManager(user);
        var context = new DefaultHttpContext();
        var identity = new ClaimsIdentity();
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "bar"));
        var principal = new ClaimsPrincipal(identity);
        var properties = new AuthenticationProperties();
        properties.Items["LoginProvider"] = "blah";
        var authResult = AuthenticateResult.Success(new AuthenticationTicket(principal, properties, "blah"));
        var auth = MockAuth(context);
        auth.Setup(s => s.AuthenticateAsync(context, IdentityConstants.ExternalScheme)).ReturnsAsync(authResult);
        var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
        var handler = new Mock<IAuthenticationHandler>();
        schemeProvider.Setup(s => s.GetAllSchemesAsync())
            .ReturnsAsync(new[]
            {
                new AuthenticationScheme("blah", "Blah blah", handler.Object.GetType())
            });
        var signInManager = SetupSignInManager(userManager.Object, context, schemeProvider: schemeProvider.Object);

        // Act
        var externalLoginInfo = await signInManager.GetExternalLoginInfoAsync();

        // Assert
        Assert.Equal("Blah blah", externalLoginInfo.ProviderDisplayName);
    }

    [Fact]
    public async Task GetExternalLoginInfoAsyncWithOidcSubClaim()
    {
        // Arrange
        var user = new PocoUser { Id = "foo", UserName = "Foo" };
        var userManager = SetupUserManager(user);
        var context = new DefaultHttpContext();
        var identity = new ClaimsIdentity();
        identity.AddClaim(new Claim("sub", "bar"));
        var principal = new ClaimsPrincipal(identity);
        var properties = new AuthenticationProperties();
        properties.Items["LoginProvider"] = "blah";
        var authResult = AuthenticateResult.Success(new AuthenticationTicket(principal, properties, "blah"));
        var auth = MockAuth(context);
        auth.Setup(s => s.AuthenticateAsync(context, IdentityConstants.ExternalScheme)).ReturnsAsync(authResult);
        var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
        var handler = new Mock<IAuthenticationHandler>();
        schemeProvider.Setup(s => s.GetAllSchemesAsync())
            .ReturnsAsync(new[]
            {
                new AuthenticationScheme("blah", "Blah blah", handler.Object.GetType())
            });
        var signInManager = SetupSignInManager(userManager.Object, context, schemeProvider: schemeProvider.Object);

        // Act
        var externalLoginInfo = await signInManager.GetExternalLoginInfoAsync();

        // Assert
        Assert.Equal("bar", externalLoginInfo.ProviderKey);
    }

    [Fact]
    public async Task ExternalLoginInfoAsyncReturnsAuthenticationPropertiesWithCustomValue()
    {
        // Arrange
        var user = new PocoUser { Id = "foo", UserName = "Foo" };
        var userManager = SetupUserManager(user);
        var context = new DefaultHttpContext();
        var identity = new ClaimsIdentity();
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "bar"));
        var principal = new ClaimsPrincipal(identity);
        var properties = new AuthenticationProperties();
        properties.Items["LoginProvider"] = "blah";
        properties.Items["CustomValue"] = "fizzbuzz";
        var authResult = AuthenticateResult.Success(new AuthenticationTicket(principal, properties, "blah"));
        var auth = MockAuth(context);
        auth.Setup(s => s.AuthenticateAsync(context, IdentityConstants.ExternalScheme)).ReturnsAsync(authResult);
        var schemeProvider = new Mock<IAuthenticationSchemeProvider>();
        var handler = new Mock<IAuthenticationHandler>();
        schemeProvider.Setup(s => s.GetAllSchemesAsync())
            .ReturnsAsync(new[]
                {
                    new AuthenticationScheme("blah", "Blah blah", handler.Object.GetType())
                });
        var signInManager = SetupSignInManager(userManager.Object, context, schemeProvider: schemeProvider.Object);
        var externalLoginInfo = await signInManager.GetExternalLoginInfoAsync();

        // Act
        var externalProperties = externalLoginInfo.AuthenticationProperties;
        var customValue = externalProperties?.Items["CustomValue"];

        // Assert
        Assert.NotNull(externalProperties);
        Assert.Equal("fizzbuzz", customValue);
    }

    public static object[][] SignInManagerTypeNames => new object[][]
    {
        new[] { nameof(SignInManager<PocoUser>) },
        new[] { nameof(NoOverridesSignInManager<PocoUser>) },
        new[] { nameof(OverrideAndAwaitBaseResetSignInManager<PocoUser>) },
        new[] { nameof(OverrideAndPassThroughUserManagerResetSignInManager<PocoUser>) },
    };

    [Theory]
    [MemberData(nameof(SignInManagerTypeNames))]
    public async Task CheckPasswordSignInFailsWhenResetLockoutFails(string signInManagerTypeName)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Failed()).Verifiable();

        var context = new DefaultHttpContext();
        var helper = SetupSignInManagerType(manager.Object, context, signInManagerTypeName);

        // Act
        var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-1a", false);

        // Assert
        Assert.Same(SignInResult.Failed, result);
        manager.Verify();
    }

    [Theory]
    [MemberData(nameof(SignInManagerTypeNames))]
    public async Task PasswordSignInWorksWhenResetLockoutReturnsNullIdentityResult(string signInManagerTypeName)
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable();
        manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync((IdentityResult)null).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        SetupSignIn(context, auth);
        var helper = SetupSignInManagerType(manager.Object, context, signInManagerTypeName);

        // Act
        var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-1a", false, false);

        // Assert
        Assert.True(result.Succeeded);
        manager.Verify();
        auth.Verify();
    }

    [Fact]
    public async Task TwoFactorSignFailsWhenResetLockoutFails()
    {
        // Setup
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        var provider = "twofactorprovider";
        var code = "123456";
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(true).Verifiable();

        manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Failed()).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, null);
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();

        // Act
        var result = await helper.TwoFactorSignInAsync(provider, code, false, false);

        // Assert
        Assert.Same(SignInResult.Failed, result);
        manager.Verify();
        auth.Verify();
    }

    public static object[][] ExpectedLockedOutSignInResultsGivenAccessFailedResults => new object[][]
    {
        new object[] { IdentityResult.Success, SignInResult.LockedOut },
        new object[] { null, SignInResult.LockedOut },
        new object[] { IdentityResult.Failed(), SignInResult.Failed },
    };

    [Theory]
    [MemberData(nameof(ExpectedLockedOutSignInResultsGivenAccessFailedResults))]
    public async Task CheckPasswordSignInLockedOutResultIsDependentOnTheAccessFailedAsyncResult(IdentityResult accessFailedResult, SignInResult expectedSignInResult)
    {
        // Setup
        var isLockedOutCallCount = 0;
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        // Return false initially to allow the password to be checked Only return true the second time after the bogus password is checked.
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(() => isLockedOutCallCount++ > 0).Verifiable();
        manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-bogus1")).ReturnsAsync(false).Verifiable();
        manager.Setup(m => m.AccessFailedAsync(user)).ReturnsAsync(accessFailedResult).Verifiable();

        var context = new DefaultHttpContext();
        // Since the PasswordSignInAsync calls the UserManager directly rather than a virtual SignInManager method like ResetLockout, we don't need to test derived SignInManagers.
        var helper = SetupSignInManager(manager.Object, context);

        // Act
        var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-bogus1", lockoutOnFailure: true);

        // Assert
        Assert.Same(expectedSignInResult, result);
        manager.Verify();
    }

    [Theory]
    [MemberData(nameof(ExpectedLockedOutSignInResultsGivenAccessFailedResults))]
    public async Task TwoFactorSignInLockedOutResultIsDependentOnTheAccessFailedAsyncResult(IdentityResult accessFailedResult, SignInResult expectedSignInResult)
    {
        // Setup
        var isLockedOutCallCount = 0;
        var user = new PocoUser { UserName = "Foo" };
        var manager = SetupUserManager(user);
        var provider = "twofactorprovider";
        var code = "123456";
        manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
        // Return false initially to allow the 2fa code to be checked. Only return true if ever in the future it is called again after failure.
        manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(() => isLockedOutCallCount++ > 0).Verifiable();
        manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(false).Verifiable();

        manager.Setup(m => m.AccessFailedAsync(user)).ReturnsAsync(accessFailedResult).Verifiable();

        var context = new DefaultHttpContext();
        var auth = MockAuth(context);
        var helper = SetupSignInManager(manager.Object, context);
        var id = SignInManager<PocoUser>.StoreTwoFactorInfo(user.Id, null);
        auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme))
            .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable();

        // Act
        var result = await helper.TwoFactorSignInAsync(provider, code, false, false);

        // Assert
        Assert.Same(expectedSignInResult, result);
        manager.Verify();
        auth.Verify();
    }

    private static SignInManager<PocoUser> SetupSignInManagerType(UserManager<PocoUser> manager, HttpContext context, string typeName)
    {
        var contextAccessor = new Mock<IHttpContextAccessor>();
        contextAccessor.Setup(a => a.HttpContext).Returns(context);
        var roleManager = MockHelpers.MockRoleManager<PocoRole>();
        var options = Options.Create(new IdentityOptions());
        var claimsFactory = new UserClaimsPrincipalFactory<PocoUser, PocoRole>(manager, roleManager.Object, options);

        return typeName switch
        {
            nameof(SignInManager<PocoUser>) => new SignInManager<PocoUser>(manager, contextAccessor.Object, claimsFactory, options, NullLogger<SignInManager<PocoUser>>.Instance, Mock.Of<IAuthenticationSchemeProvider>(), new DefaultUserConfirmation<PocoUser>()),
            nameof(NoOverridesSignInManager<PocoUser>) => new NoOverridesSignInManager<PocoUser>(manager, contextAccessor.Object, claimsFactory, options),
            nameof(OverrideAndAwaitBaseResetSignInManager<PocoUser>) => new OverrideAndAwaitBaseResetSignInManager<PocoUser>(manager, contextAccessor.Object, claimsFactory, options),
            nameof(OverrideAndPassThroughUserManagerResetSignInManager<PocoUser>) => new OverrideAndPassThroughUserManagerResetSignInManager<PocoUser>(manager, contextAccessor.Object, claimsFactory, options),
            _ => throw new NotImplementedException(),
        };
    }

    private class NoOverridesSignInManager<TUser> : SignInManager<TUser> where TUser : class
    {
        public NoOverridesSignInManager(
            UserManager<TUser> userManager,
            IHttpContextAccessor contextAccessor,
            IUserClaimsPrincipalFactory<TUser> claimsFactory,
            IOptions<IdentityOptions> optionsAccessor)
            : base(userManager, contextAccessor, claimsFactory, optionsAccessor, NullLogger<SignInManager<TUser>>.Instance, Mock.Of<IAuthenticationSchemeProvider>(), new DefaultUserConfirmation<TUser>())
        {
        }
    }

    private class OverrideAndAwaitBaseResetSignInManager<TUser> : SignInManager<TUser> where TUser : class
    {
        public OverrideAndAwaitBaseResetSignInManager(
            UserManager<TUser> userManager,
            IHttpContextAccessor contextAccessor,
            IUserClaimsPrincipalFactory<TUser> claimsFactory,
            IOptions<IdentityOptions> optionsAccessor)
            : base(userManager, contextAccessor, claimsFactory, optionsAccessor, NullLogger<SignInManager<TUser>>.Instance, Mock.Of<IAuthenticationSchemeProvider>(), new DefaultUserConfirmation<TUser>())
        {
        }

        protected override async Task ResetLockout(TUser user)
        {
            await base.ResetLockout(user);
        }
    }

    private class OverrideAndPassThroughUserManagerResetSignInManager<TUser> : SignInManager<TUser> where TUser : class
    {
        public OverrideAndPassThroughUserManagerResetSignInManager(
            UserManager<TUser> userManager,
            IHttpContextAccessor contextAccessor,
            IUserClaimsPrincipalFactory<TUser> claimsFactory,
            IOptions<IdentityOptions> optionsAccessor)
            : base(userManager, contextAccessor, claimsFactory, optionsAccessor, NullLogger<SignInManager<TUser>>.Instance, Mock.Of<IAuthenticationSchemeProvider>(), new DefaultUserConfirmation<TUser>())
        {
        }

        protected override Task ResetLockout(TUser user)
        {
            if (UserManager.SupportsUserLockout)
            {
                return UserManager.ResetAccessFailedCountAsync(user);
            }

            return Task.CompletedTask;
        }
    }

    private sealed class MockSchemeProvider : IAuthenticationSchemeProvider
    {
        private static AuthenticationScheme CreateCookieScheme(string name) => new(IdentityConstants.ApplicationScheme, displayName: null, typeof(CookieAuthenticationHandler));

        private static readonly Dictionary<string, AuthenticationScheme> _defaultCookieSchemes = new()
        {
            [IdentityConstants.ApplicationScheme] = CreateCookieScheme(IdentityConstants.ApplicationScheme),
            [IdentityConstants.ExternalScheme] = CreateCookieScheme(IdentityConstants.ExternalScheme),
            [IdentityConstants.TwoFactorRememberMeScheme] = CreateCookieScheme(IdentityConstants.TwoFactorRememberMeScheme),
            [IdentityConstants.TwoFactorUserIdScheme] = CreateCookieScheme(IdentityConstants.TwoFactorUserIdScheme),
        };

        public Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync() => Task.FromResult<IEnumerable<AuthenticationScheme>>(_defaultCookieSchemes.Values);
        public Task<AuthenticationScheme> GetSchemeAsync(string name) => Task.FromResult(_defaultCookieSchemes.TryGetValue(name, out var scheme) ? scheme : null);

        public void AddScheme(AuthenticationScheme scheme) => throw new NotImplementedException();
        public void RemoveScheme(string name) => throw new NotImplementedException();
        public Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync() => throw new NotImplementedException();
        public Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync() => throw new NotImplementedException();
        public Task<AuthenticationScheme> GetDefaultForbidSchemeAsync() => throw new NotImplementedException();
        public Task<AuthenticationScheme> GetDefaultSignInSchemeAsync() => throw new NotImplementedException();
        public Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync() => throw new NotImplementedException();
        public Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync() => throw new NotImplementedException();
    }
}
