using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Exceptions;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services.Implementations;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;

#nullable enable

namespace Bit.Core.Test.Auth.Services;

[SutProviderCustomize]
public class AuthRequestServiceTests
{
    [Theory, BitAutoData]
    public async Task GetAuthRequestAsync_IfDifferentUser_ReturnsNull(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest,
        Guid authRequestId,
        Guid userId)
    {
        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequestId)
            .Returns(authRequest);

        var foundAuthRequest = await sutProvider.Sut.GetAuthRequestAsync(authRequestId, userId);

        Assert.Null(foundAuthRequest);
    }

    [Theory, BitAutoData]
    public async Task GetAuthRequestAsync_IfSameUser_ReturnsAuthRequest(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest,
        Guid authRequestId)
    {
        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequestId)
            .Returns(authRequest);

        var foundAuthRequest = await sutProvider.Sut.GetAuthRequestAsync(authRequestId, authRequest.UserId);

        Assert.NotNull(foundAuthRequest);
    }

    [Theory, BitAutoData]
    public async Task GetValidatedAuthRequestAsync_IfCodeNotValid_ReturnsNull(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest,
        string accessCode)
    {
        authRequest.CreationDate = DateTime.UtcNow;

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, accessCode);

        Assert.Null(foundAuthRequest);
    }

    /// <summary>
    /// Story: AdminApproval AuthRequests should have a longer expiration time by default and non-AdminApproval ones
    /// should expire after 15 minutes by default.
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AdminApproval, "-10.00:00:00")]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, "-00:16:00")]
    [BitAutoData(AuthRequestType.Unlock, "-00:16:00")]
    public async Task GetValidatedAuthRequestAsync_IfExpired_ReturnsNull(
        AuthRequestType authRequestType,
        TimeSpan creationTimeBeforeNow,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        authRequest.Type = authRequestType;
        authRequest.CreationDate = DateTime.UtcNow.Add(creationTimeBeforeNow);
        authRequest.Approved = false;

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);

        Assert.Null(foundAuthRequest);
    }

    /// <summary>
    /// Story: Once a AdminApproval type has been approved it has a different expiration time based on time
    /// after the response.
    /// </summary>
    [Theory]
    [BitAutoData]
    public async Task GetValidatedAuthRequestAsync_AdminApprovalApproved_HasLongerExpiration_ReturnsRequest(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        authRequest.Type = AuthRequestType.AdminApproval;
        authRequest.Approved = true;
        authRequest.ResponseDate = DateTime.UtcNow.Add(TimeSpan.FromHours(-13));

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        var validatedAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);

        Assert.Null(validatedAuthRequest);
    }

    [Theory, BitAutoData]
    public async Task GetValidatedAuthRequestAsync_IfValid_ReturnsAuthRequest(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-2);

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth
            .Returns(new GlobalSettings.PasswordlessAuthSettings());

        var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);

        Assert.NotNull(foundAuthRequest);
    }

    [Theory, BitAutoData]
    public async Task CreateAuthRequestAsync_NoUser_ThrowsBadRequest(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequestCreateRequestModel createModel)
    {
        sutProvider.GetDependency<ICurrentContext>()
            .DeviceType
            .Returns(DeviceType.Android);

        sutProvider.GetDependency<IUserRepository>()
            .GetByEmailAsync(createModel.Email)
            .Returns((User?)null);

        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
    }

    [Theory, BitAutoData]
    public async Task CreateAuthRequestAsync_NoKnownDevice_ThrowsBadRequest(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequestCreateRequestModel createModel,
        User user)
    {
        user.Email = createModel.Email;

        sutProvider.GetDependency<IUserRepository>()
            .GetByEmailAsync(createModel.Email)
            .Returns(user);

        sutProvider.GetDependency<ICurrentContext>()
            .DeviceType
            .Returns(DeviceType.Android);

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth.KnownDevicesOnly
            .Returns(true);

        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
    }

    /// <summary>
    /// Story: Non-AdminApproval requests should be created without a known device if the settings is set to <c>false</c>
    /// Non-AdminApproval ones should also have a push notification sent about them.
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
    [BitAutoData(AuthRequestType.Unlock)]
    [BitAutoData(new object?[1] { null })]
    public async Task CreateAuthRequestAsync_CreatesAuthRequest(
        AuthRequestType? authRequestType,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequestCreateRequestModel createModel,
        User user)
    {
        user.Email = createModel.Email;
        createModel.Type = authRequestType;

        sutProvider.GetDependency<IUserRepository>()
            .GetByEmailAsync(createModel.Email)
            .Returns(user);

        sutProvider.GetDependency<ICurrentContext>()
            .DeviceType
            .Returns(DeviceType.Android);

        sutProvider.GetDependency<ICurrentContext>()
            .IpAddress
            .Returns("1.1.1.1");

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth.KnownDevicesOnly
            .Returns(false);

        sutProvider.GetDependency<IAuthRequestRepository>()
            .CreateAsync(Arg.Any<AuthRequest>())
            .Returns(c => c.ArgAt<AuthRequest>(0));

        var createdAuthRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);

        await sutProvider.GetDependency<IPushNotificationService>()
            .Received()
            .PushAuthRequestAsync(createdAuthRequest);

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received()
            .CreateAsync(createdAuthRequest);

        await sutProvider.GetDependency<IMailService>()
            .DidNotReceiveWithAnyArgs()
            .SendDeviceApprovalRequestedNotificationEmailAsync(
                Arg.Any<IEnumerable<string>>(),
                Arg.Any<Guid>(),
                Arg.Any<string>(),
                Arg.Any<string>());
    }

    /// <summary>
    /// Story: Since an AllowAnonymous endpoint calls this method we need
    /// to verify that a device was able to be found via ICurrentContext
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
    [BitAutoData(AuthRequestType.Unlock)]
    public async Task CreateAuthRequestAsync_NoDeviceType_ThrowsBadRequest(
        AuthRequestType authRequestType,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequestCreateRequestModel createModel,
        User user)
    {
        user.Email = createModel.Email;
        createModel.Type = authRequestType;

        sutProvider.GetDependency<IUserRepository>()
            .GetByEmailAsync(createModel.Email)
            .Returns(user);

        sutProvider.GetDependency<ICurrentContext>()
            .DeviceType
            .Returns((DeviceType?)null);

        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
    }

    /// <summary>
    /// Story: If a user happens to exist to more than one organization, we will send the device approval request to
    /// each of them.
    /// </summary>
    [Theory, BitAutoData]
    public async Task CreateAuthRequestAsync_AdminApproval_CreatesForEachOrganization_SendsEmails(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequestCreateRequestModel createModel,
        User user,
        OrganizationUser organizationUser1,
        OrganizationUserUserDetails admin1,
        OrganizationUserUserDetails customUser1,
        OrganizationUser organizationUser2,
        OrganizationUserUserDetails admin2,
        OrganizationUserUserDetails admin3,
        OrganizationUserUserDetails customUser2)
    {
        createModel.Type = AuthRequestType.AdminApproval;
        user.Email = createModel.Email;
        organizationUser1.UserId = user.Id;
        organizationUser2.UserId = user.Id;
        customUser1.Permissions = CoreHelpers.ClassToJsonData(new Permissions
        {
            ManageResetPassword = false,
        });
        customUser2.Permissions = CoreHelpers.ClassToJsonData(new Permissions
        {
            ManageResetPassword = true,
        });

        sutProvider.GetDependency<IUserRepository>()
            .GetByEmailAsync(user.Email)
            .Returns(user);

        sutProvider.GetDependency<ICurrentContext>()
            .DeviceType
            .Returns(DeviceType.ChromeExtension);

        sutProvider.GetDependency<ICurrentContext>()
            .UserId
            .Returns(user.Id);

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth.KnownDevicesOnly
            .Returns(false);


        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyByUserAsync(user.Id)
            .Returns(new List<OrganizationUser>
            {
                organizationUser1,
                organizationUser2,
            });

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyByMinimumRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Admin)
            .Returns(
            [
                admin1,
            ]);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyDetailsByRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Custom)
            .Returns(
            [
                customUser1,
            ]);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyByMinimumRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Admin)
            .Returns(
            [
                admin2,
                admin3,
            ]);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyDetailsByRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Custom)
            .Returns(
            [
                customUser2,
            ]);

        sutProvider.GetDependency<IAuthRequestRepository>()
            .CreateAsync(Arg.Any<AuthRequest>())
            .Returns(c => c.ArgAt<AuthRequest>(0));

        var authRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);

        Assert.Equal(organizationUser1.OrganizationId, authRequest.OrganizationId);

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received(1)
            .CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser1.OrganizationId));

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received(1)
            .CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser2.OrganizationId));

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received(2)
            .CreateAsync(Arg.Any<AuthRequest>());

        await sutProvider.GetDependency<IEventService>()
            .Received(1)
            .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);

        await sutProvider.GetDependency<IMailService>()
            .Received(1)
            .SendDeviceApprovalRequestedNotificationEmailAsync(
                Arg.Is<IEnumerable<string>>(emails => emails.Count() == 1 && emails.Contains(admin1.Email)),
                organizationUser1.OrganizationId,
                user.Email,
                user.Name);

        await sutProvider.GetDependency<IMailService>()
            .Received(1)
            .SendDeviceApprovalRequestedNotificationEmailAsync(
                Arg.Is<IEnumerable<string>>(emails => emails.Count() == 3 &&
                                                      emails.Contains(admin2.Email) && emails.Contains(admin3.Email) &&
                                                      emails.Contains(customUser2.Email)),
                organizationUser2.OrganizationId,
                user.Email,
                user.Name);
    }


    [Theory, BitAutoData]
    public async Task CreateAuthRequestAsync_AdminApproval_AndNoAdminEmails_ShouldNotSendNotificationEmails(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequestCreateRequestModel createModel,
        User user,
        OrganizationUser organizationUser1)
    {
        createModel.Type = AuthRequestType.AdminApproval;
        user.Email = createModel.Email;
        organizationUser1.UserId = user.Id;

        sutProvider.GetDependency<IUserRepository>()
            .GetByEmailAsync(user.Email)
            .Returns(user);

        sutProvider.GetDependency<ICurrentContext>()
            .DeviceType
            .Returns(DeviceType.ChromeExtension);

        sutProvider.GetDependency<ICurrentContext>()
            .UserId
            .Returns(user.Id);

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth.KnownDevicesOnly
            .Returns(false);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyByUserAsync(user.Id)
            .Returns(new List<OrganizationUser>
            {
                organizationUser1,
            });

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyByMinimumRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Admin)
            .Returns([]);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetManyDetailsByRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Custom)
            .Returns([]);

        sutProvider.GetDependency<IAuthRequestRepository>()
            .CreateAsync(Arg.Any<AuthRequest>())
            .Returns(c => c.ArgAt<AuthRequest>(0));

        var authRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);

        Assert.Equal(organizationUser1.OrganizationId, authRequest.OrganizationId);

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received(1)
            .CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser1.OrganizationId));

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received(1)
            .CreateAsync(Arg.Any<AuthRequest>());

        await sutProvider.GetDependency<IEventService>()
            .Received(1)
            .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);

        await sutProvider.GetDependency<IMailService>()
            .Received(0)
            .SendDeviceApprovalRequestedNotificationEmailAsync(
                Arg.Any<IEnumerable<string>>(),
                Arg.Any<Guid>(),
                Arg.Any<string>(),
                Arg.Any<string>());

        var expectedLogMessage = "There are no admin emails to send to.";
        sutProvider.GetDependency<ILogger<AuthRequestService>>()
            .Received(1)
            .LogWarning(expectedLogMessage);
    }

    /// <summary>
    /// Story: When an <see cref="AuthRequest"> is approved we want to update it in the database so it cannot have
    /// it's status changed again and we want to push a notification to let the user know of the approval.
    /// In the case of the AdminApproval we also want to log an event.
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AdminApproval, "7b055ea1-38be-42d0-b2e4-becb2340f8df")]
    [BitAutoData(AuthRequestType.Unlock, null)]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]
    public async Task UpdateAuthRequestAsync_ValidResponse_SendsResponse(
        AuthRequestType authRequestType,
        Guid? organizationId,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
        authRequest.Approved = null;
        authRequest.OrganizationId = organizationId;
        authRequest.Type = authRequestType;

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        var device = new Device
        {
            Id = Guid.NewGuid(),
            Identifier = "test_identifier",
        };

        sutProvider.GetDependency<IDeviceRepository>()
            .GetByIdentifierAsync(device.Identifier, authRequest.UserId)
            .Returns(device);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())
            .Returns(new OrganizationUser
            {
                UserId = authRequest.UserId,
                OrganizationId = organizationId.GetValueOrDefault(),
            });

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth
            .Returns(new GlobalSettings.PasswordlessAuthSettings());

        var updateModel = new AuthRequestUpdateRequestModel
        {
            Key = "test_key",
            DeviceIdentifier = "test_identifier",
            RequestApproved = true,
            MasterPasswordHash = "my_hash",
        };

        var udpatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);

        Assert.Equal("my_hash", udpatedAuthRequest.MasterPasswordHash);

        // On approval, the response date should be set to current date
        Assert.NotNull(udpatedAuthRequest.ResponseDate);
        AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received(1)
            .ReplaceAsync(udpatedAuthRequest);

        await sutProvider.GetDependency<IPushNotificationService>()
            .Received(1)
            .PushAuthRequestResponseAsync(udpatedAuthRequest);

        var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;
        await sutProvider.GetDependency<IEventService>()
            .Received(expectedNumberOfCalls)
            .LogOrganizationUserEventAsync(
                Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),
                EventType.OrganizationUser_ApprovedAuthRequest);
    }

    /// <summary>
    /// Story: When an <see cref="AuthRequest"> is rejected we want to update it in the database so it cannot have
    /// it's status changed again but we do not want to send a push notification to the original device
    /// so as to not leak that it was rejected. In the case of an AdminApproval type we do want to log an event though
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AdminApproval, "7b055ea1-38be-42d0-b2e4-becb2340f8df")]
    [BitAutoData(AuthRequestType.Unlock, null)]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]
    public async Task UpdateAuthRequestAsync_ResponseNotApproved_DoesNotLeakRejection(
        AuthRequestType authRequestType,
        Guid? organizationId,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        // Give it a recent creation time which is valid for all types of AuthRequests
        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
        authRequest.Type = authRequestType;
        // Has not been decided already
        authRequest.Approved = null;
        authRequest.OrganizationId = organizationId;

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        // Setup a device for all requests even though it will not be called for verification in a AdminApproval
        var device = new Device
        {
            Id = Guid.NewGuid(),
            Identifier = "test_identifier",
        };

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth
            .Returns(new GlobalSettings.PasswordlessAuthSettings());

        sutProvider.GetDependency<IDeviceRepository>()
            .GetByIdentifierAsync(device.Identifier, authRequest.UserId)
            .Returns(device);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())
            .Returns(new OrganizationUser
            {
                UserId = authRequest.UserId,
                OrganizationId = organizationId.GetValueOrDefault(),
            });

        var updateModel = new AuthRequestUpdateRequestModel
        {
            Key = "test_key",
            DeviceIdentifier = "test_identifier",
            RequestApproved = false,
            MasterPasswordHash = "my_hash",
        };

        var udpatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);

        Assert.Equal(udpatedAuthRequest.MasterPasswordHash, authRequest.MasterPasswordHash);
        Assert.False(udpatedAuthRequest.Approved);
        Assert.NotNull(udpatedAuthRequest.ResponseDate);
        AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);

        await sutProvider.GetDependency<IAuthRequestRepository>()
            .Received()
            .ReplaceAsync(udpatedAuthRequest);

        await sutProvider.GetDependency<IPushNotificationService>()
            .DidNotReceiveWithAnyArgs()
            .PushAuthRequestResponseAsync(udpatedAuthRequest);

        var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;

        await sutProvider.GetDependency<IEventService>()
            .Received(expectedNumberOfCalls)
            .LogOrganizationUserEventAsync(
                Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),
                EventType.OrganizationUser_RejectedAuthRequest);
    }

    /// <summary>
    /// Story: A bad actor is able to get ahold of the request id of a valid <see cref="AuthRequest" />
    /// and tries to approve it from their own Bitwarden account. We need to validate that the currently signed in user
    /// is the same user that originally created the request and we want to pretend it does not exist at all by throwing
    /// NotFoundException.
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
    [BitAutoData(AuthRequestType.Unlock)]
    public async Task UpdateAuthRequestAsync_InvalidUser_ThrowsNotFound(
        AuthRequestType authRequestType,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest,
        Guid authenticatedUserId)
    {
        // Give it a recent creation date so that it is valid
        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
        // The request hasn't been Approved/Disapproved already
        authRequest.Approved = null;
        // Has an type that needs the UserId property validated
        authRequest.Type = authRequestType;

        // Auth request should not be null
        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        var updateModel = new AuthRequestUpdateRequestModel
        {
            Key = "test_key",
            DeviceIdentifier = "test_identifier",
            RequestApproved = true,
            MasterPasswordHash = "my_hash",
        };

        // Give it a randomly generated userId such that it won't be valid for the AuthRequest
        await Assert.ThrowsAsync<NotFoundException>(
            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authenticatedUserId, updateModel));
    }

    /// <summary>
    /// Story: A user created this auth request and does not approve/reject the request
    /// for 16 minutes, which is past the default expiration time. This auth request
    /// will be purged from the database soon but might exist for some amount of time after it's expiration
    /// this method should throw a NotFoundException since it theoretically should not exist, this
    /// could be a user finally clicking Approve after the request sitting on their phone for a while.
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, "-00:16:00")]
    [BitAutoData(AuthRequestType.Unlock, "-00:16:00")]
    [BitAutoData(AuthRequestType.AdminApproval, "-8.00:00:00")]
    public async Task UpdateAuthRequestAsync_OldAuthRequest_ThrowsNotFound(
        AuthRequestType authRequestType,
        TimeSpan timeBeforeCreation,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        // AuthRequest's have a default valid lifetime of only 15 minutes, make it older than that
        authRequest.CreationDate = DateTime.UtcNow.Add(timeBeforeCreation);
        // Make it so that the user has not made a decision on this request
        authRequest.Approved = null;
        // Make it one of the types that doesn't have longer expiration i.e AdminApproval
        authRequest.Type = authRequestType;

        // The item should still exist in the database
        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        // Represents the user finally clicking approve.
        var updateModel = new AuthRequestUpdateRequestModel
        {
            Key = "test_key",
            DeviceIdentifier = "test_identifier",
            RequestApproved = true,
            MasterPasswordHash = "my_hash",
        };

        await Assert.ThrowsAsync<NotFoundException>(
            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
    }

    /// <summary>
    /// Story: non-AdminApproval types need to validate that the device used to respond to the
    /// request is a known device to the authenticated user.
    /// </summary>
    [Theory]
    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
    [BitAutoData(AuthRequestType.Unlock)]
    public async Task UpdateAuthRequestAsync_InvalidDeviceIdentifier_ThrowsBadRequest(
        AuthRequestType authRequestType,
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
        authRequest.Approved = null;
        authRequest.Type = authRequestType;

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        sutProvider.GetDependency<IDeviceRepository>()
            .GetByIdentifierAsync("invalid_identifier", authRequest.UserId)
            .Returns((Device?)null);

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth
            .Returns(new GlobalSettings.PasswordlessAuthSettings());

        var updateModel = new AuthRequestUpdateRequestModel
        {
            Key = "test_key",
            DeviceIdentifier = "invalid_identifier",
            RequestApproved = true,
            MasterPasswordHash = "my_hash",
        };

        await Assert.ThrowsAsync<BadRequestException>(
            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
    }

    /// <summary>
    /// Story: Once the destiny of an AuthRequest has been decided, it should be considered immutable
    /// and new update request should be blocked.
    /// </summary>
    [Theory, BitAutoData]
    public async Task UpdateAuthRequestAsync_AlreadyApprovedOrRejected_ThrowsDuplicateAuthRequestException(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest)
    {
        authRequest.Approved = true;

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        var updateModel = new AuthRequestUpdateRequestModel
        {
            Key = "test_key",
            DeviceIdentifier = "test_identifier",
            RequestApproved = true,
            MasterPasswordHash = "my_hash",
        };

        await Assert.ThrowsAsync<DuplicateAuthRequestException>(
            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
    }

    /// <summary>
    /// Story: An admin approves a request for one of their org users. For auditing purposes we need to
    /// log an event that correlates the action for who the request was approved for. On approval we also need to
    /// push the notification to the user.
    /// </summary>
    [Theory, BitAutoData]
    public async Task UpdateAuthRequestAsync_AdminApproved_LogsEvent(
        SutProvider<AuthRequestService> sutProvider,
        AuthRequest authRequest,
        OrganizationUser organizationUser)
    {
        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
        authRequest.Type = AuthRequestType.AdminApproval;
        authRequest.OrganizationId = organizationUser.OrganizationId;
        authRequest.Approved = null;

        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequest.Id)
            .Returns(authRequest);

        sutProvider.GetDependency<IOrganizationUserRepository>()
            .GetByOrganizationAsync(authRequest.OrganizationId!.Value, authRequest.UserId)
            .Returns(organizationUser);

        sutProvider.GetDependency<IGlobalSettings>()
            .PasswordlessAuth
            .Returns(new GlobalSettings.PasswordlessAuthSettings());

        var updateModel = new AuthRequestUpdateRequestModel
        {
            Key = "test_key",
            RequestApproved = true,
            MasterPasswordHash = "my_hash",
        };

        var updatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);

        Assert.Equal("my_hash", updatedAuthRequest.MasterPasswordHash);
        Assert.Equal("test_key", updatedAuthRequest.Key);
        Assert.True(updatedAuthRequest.Approved);
        Assert.NotNull(updatedAuthRequest.ResponseDate);
        AssertHelper.AssertRecent(updatedAuthRequest.ResponseDate!.Value);

        await sutProvider.GetDependency<IEventService>()
            .Received(1)
            .LogOrganizationUserEventAsync(
                Arg.Is(organizationUser), Arg.Is(EventType.OrganizationUser_ApprovedAuthRequest));

        await sutProvider.GetDependency<IPushNotificationService>()
            .Received(1)
            .PushAuthRequestResponseAsync(authRequest);
    }

    [Theory, BitAutoData]
    public async Task UpdateAuthRequestAsync_BadId_ThrowsNotFound(
        SutProvider<AuthRequestService> sutProvider,
        Guid authRequestId)
    {
        sutProvider.GetDependency<IAuthRequestRepository>()
            .GetByIdAsync(authRequestId)
            .Returns((AuthRequest?)null);

        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAuthRequestAsync(
            authRequestId, Guid.NewGuid(), new AuthRequestUpdateRequestModel()));
    }
}
