Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/test/Registration/RegistrationCoordinatorTest.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import LibSignalClient
import Testing

@testable import Signal
@testable import SignalServiceKit

public class RegistrationCoordinatorTest {
    // Default to the SSK AEP.
    typealias AccountEntropyPool = SignalServiceKit.AccountEntropyPool

    private var stubs = Stubs()

    private var date: Date { self.stubs.date }

    private var appExpiry: AppExpiry!
    private var changeNumberPniManager: ChangePhoneNumberPniManagerMock!
    private var contactsStore: RegistrationCoordinatorImpl.TestMocks.ContactsStore!
    private var dateProvider: DateProvider!
    private var db: (any DB)!
    private var experienceManager: RegistrationCoordinatorImpl.TestMocks.ExperienceManager!
    private var accountKeyStore: AccountKeyStore!
    private var localUsernameManagerMock: MockLocalUsernameManager!
    private var mockIdentityManager: OWSIdentityManager!
    private var mockMessagePipelineSupervisor: RegistrationCoordinatorImpl.TestMocks.MessagePipelineSupervisor!
    private var mockMessageProcessor: RegistrationCoordinatorImpl.TestMocks.MessageProcessor!
    private var mockURLSession: TSRequestOWSURLSessionMock!
    private var networkManagerMock: MockNetworkManager!
    private var ows2FAManagerMock: RegistrationCoordinatorImpl.TestMocks.OWS2FAManager!
    private var phoneNumberDiscoverabilityManagerMock: MockPhoneNumberDiscoverabilityManager!
    private var preKeyManagerMock: RegistrationCoordinatorImpl.TestMocks.PreKeyManager!
    private var profileManagerMock: RegistrationCoordinatorImpl.TestMocks.ProfileManager!
    private var pushRegistrationManagerMock: RegistrationCoordinatorImpl.TestMocks.PushRegistrationManager!
    private var quickRestoreManager: QuickRestoreManager!
    private var receiptManagerMock: RegistrationCoordinatorImpl.TestMocks.ReceiptManager!
    private var registrationCoordinatorLoader: RegistrationCoordinatorLoaderImpl!
    private var registrationStateChangeManagerMock: MockRegistrationStateChangeManager!
    private var sessionManager: RegistrationSessionManagerMock!
    private var storageServiceManagerMock: RegistrationCoordinatorImpl.TestMocks.StorageServiceManager!
    private var svr: SecureValueRecoveryMock!
    private var svrLocalStorageMock: SVRLocalStorageMock!
    private var svrAuthCredentialStore: SVRAuthCredentialStorageMock!
    private var timeoutProviderMock: RegistrationCoordinatorImpl.TestMocks.TimeoutProvider!
    private var tsAccountManagerMock: MockTSAccountManager!
    private var usernameApiClientMock: RegistrationCoordinatorImpl.TestMocks.UsernameApiClient!
    private var usernameLinkManagerMock: MockUsernameLinkManager!

    class RegistrationTestRun {
        private(set) var recordedSteps = [TestStep]()
        func addObservedStep(_ step: TestStep) {
            recordedSteps.append(step)
        }
    }

    private var testRun = RegistrationTestRun()

    init() {
        dateProvider = { self.date }
        db = InMemoryDB()

        appExpiry = .forUnitTests()
        accountKeyStore = AccountKeyStore(backupSettingsStore: BackupSettingsStore())
        let preKeyStore = PreKeyStore()
        changeNumberPniManager = ChangePhoneNumberPniManagerMock(
            mockKyberStore: KyberPreKeyStoreImpl(for: .pni, dateProvider: dateProvider, preKeyStore: preKeyStore),
        )
        contactsStore = RegistrationCoordinatorImpl.TestMocks.ContactsStore()
        experienceManager = RegistrationCoordinatorImpl.TestMocks.ExperienceManager()
        localUsernameManagerMock = {
            let mock = MockLocalUsernameManager()
            // This should result in no username reclamation. Tests that want to
            // test reclamation should overwrite this.
            mock.startingUsernameState = .unset
            return mock
        }()
        svr = SecureValueRecoveryMock()
        svrAuthCredentialStore = SVRAuthCredentialStorageMock()
        mockMessagePipelineSupervisor = RegistrationCoordinatorImpl.TestMocks.MessagePipelineSupervisor()
        mockMessageProcessor = RegistrationCoordinatorImpl.TestMocks.MessageProcessor()
        networkManagerMock = MockNetworkManager()
        ows2FAManagerMock = RegistrationCoordinatorImpl.TestMocks.OWS2FAManager()
        phoneNumberDiscoverabilityManagerMock = MockPhoneNumberDiscoverabilityManager()
        preKeyManagerMock = RegistrationCoordinatorImpl.TestMocks.PreKeyManager(run: testRun)
        profileManagerMock = RegistrationCoordinatorImpl.TestMocks.ProfileManager()
        pushRegistrationManagerMock = RegistrationCoordinatorImpl.TestMocks.PushRegistrationManager(run: testRun)
        receiptManagerMock = RegistrationCoordinatorImpl.TestMocks.ReceiptManager()
        registrationStateChangeManagerMock = MockRegistrationStateChangeManager()
        sessionManager = RegistrationSessionManagerMock()
        svrLocalStorageMock = SVRLocalStorageMock()
        storageServiceManagerMock = RegistrationCoordinatorImpl.TestMocks.StorageServiceManager(run: testRun)
        timeoutProviderMock = RegistrationCoordinatorImpl.TestMocks.TimeoutProvider()
        tsAccountManagerMock = MockTSAccountManager()
        usernameApiClientMock = RegistrationCoordinatorImpl.TestMocks.UsernameApiClient()
        usernameLinkManagerMock = MockUsernameLinkManager()

        let recipientDbTable = RecipientDatabaseTable()
        let recipientFetcher = RecipientFetcher(
            recipientDatabaseTable: recipientDbTable,
            searchableNameIndexer: MockSearchableNameIndexer(),
        )
        let recipientIdFinder = RecipientIdFinder(
            recipientDatabaseTable: recipientDbTable,
            recipientFetcher: recipientFetcher,
        )
        mockIdentityManager = MockIdentityManager(recipientIdFinder: recipientIdFinder)

        quickRestoreManager = QuickRestoreManager(
            accountKeyStore: accountKeyStore,
            backupNonceStore: BackupNonceMetadataStore(),
            backupSettingsStore: BackupSettingsStore(),
            db: db,
            deviceProvisioningService: DeviceProvisioningServiceImpl(networkManager: networkManagerMock),
            identityManager: mockIdentityManager,
            networkManager: networkManagerMock,
            tsAccountManager: tsAccountManagerMock,
        )

        let mockURLSession = TSRequestOWSURLSessionMock()
        self.mockURLSession = mockURLSession
        let mockSignalService = OWSSignalServiceMock()
        mockSignalService.mockUrlSessionBuilder = { _, _, _ in
            return mockURLSession
        }

        let dependencies = RegistrationCoordinatorDependencies(
            appExpiry: appExpiry,
            accountEntropyPoolGenerator: { Stubs.accountEntropyPoolToGenerate },
            accountKeyStore: accountKeyStore,
            backupArchiveManager: BackupArchiveManagerMock(),
            backupIdService: MockBackupIdService(),
            backupNonceStore: BackupNonceMetadataStore(),
            backupRequestManager: BackupRequestManagerMock(),
            changeNumberPniManager: changeNumberPniManager,
            contactsManager: RegistrationCoordinatorImpl.TestMocks.ContactsManager(),
            contactsStore: contactsStore,
            dateProvider: { self.dateProvider() },
            db: db,
            deviceTransferService: DeviceTransferServiceMock(),
            experienceManager: experienceManager,
            identityManager: RegistrationCoordinatorImpl.TestMocks.IdentityManager(),
            localUsernameManager: localUsernameManagerMock,
            messagePipelineSupervisor: mockMessagePipelineSupervisor,
            messageProcessor: mockMessageProcessor,
            networkManager: networkManagerMock,
            ows2FAManager: ows2FAManagerMock,
            phoneNumberDiscoverabilityManager: phoneNumberDiscoverabilityManagerMock,
            preKeyManager: preKeyManagerMock,
            profileManager: profileManagerMock,
            pushRegistrationManager: pushRegistrationManagerMock,
            quickRestoreManager: quickRestoreManager,
            receiptManager: receiptManagerMock,
            registrationBackupErrorPresenter: RegistrationCoordinatorBackupErrorPresenterMock(),
            registrationStateChangeManager: registrationStateChangeManagerMock,
            registrationWebSocketManager: MockRegistrationWebSocketManager(),
            remoteConfigManager: StubbableRemoteConfigManager(),
            sessionManager: sessionManager,
            signalService: mockSignalService,
            storageServiceManager: storageServiceManagerMock,
            svr: svr,
            svrAuthCredentialStore: svrAuthCredentialStore,
            timeoutProvider: timeoutProviderMock,
            tsAccountManager: tsAccountManagerMock,
            udManager: RegistrationCoordinatorImpl.TestMocks.UDManager(),
            usernameApiClient: usernameApiClientMock,
            usernameLinkManager: usernameLinkManagerMock,
        )
        registrationCoordinatorLoader = RegistrationCoordinatorLoaderImpl(dependencies: dependencies)
    }

    enum KeyType: CustomDebugStringConvertible {
        case none
        case masterKey
        case accountEntropyPool

        var debugDescription: String {
            switch self {
            case .none: return "none"
            case .masterKey: return "masterKey"
            case .accountEntropyPool: return "AEP"
            }
        }

        static var testCases: [(old: Self, new: Self)] {
            return [
                (.masterKey, .accountEntropyPool),
                (.accountEntropyPool, .accountEntropyPool),
            ]
        }
    }

    static let testModes: [RegistrationMode] = [
        RegistrationMode.registering,
        RegistrationMode.reRegistering(.init(e164: Stubs.e164, aci: Stubs.aci)),
    ]

    typealias TestCase = (mode: RegistrationMode, oldKey: KeyType, newKey: KeyType)

    static func onlyReRegisteringTestCases() -> [TestCase] {
        return buildTestCases(for: [RegistrationMode.reRegistering(.init(e164: Stubs.e164, aci: Stubs.aci))])
    }

    static func testCases() -> [TestCase] {
        return buildTestCases(for: testModes)
    }

    static func buildTestCases(for modes: [RegistrationMode]) -> [TestCase] {
        var results = [(mode: RegistrationMode, oldKey: KeyType, newKey: KeyType)]()
        for mode in modes {
            for keys in KeyType.testCases {
                results.append((mode, keys.old, keys.new))
            }
        }
        return results
    }

    func setupTest(_ testCase: TestCase) -> RegistrationCoordinatorImpl {
        return db.write {
            return registrationCoordinatorLoader.coordinator(
                forDesiredMode: testCase.mode,
                transaction: $0,
                logger: .empty(),
            ) as! RegistrationCoordinatorImpl
        }
    }

    enum TestStep: String, Equatable, CustomDebugStringConvertible {
        case restoreKeys
        case requestPushToken
        case createPreKeys
        case createAccount
        case finalizePreKeys
        case rotateOneTimePreKeys
        case restoreStorageService
        case backupMasterKey
        case confirmReservedUsername
        case rotateManifest
        case updateAccountAttribute
        case failedRequest

        var debugDescription: String {
            switch self {
            case .restoreKeys: return "restoreKeys"
            case .requestPushToken: return "requestPushToken"
            case .createPreKeys: return "createPreKeys"
            case .createAccount: return "createAccount"
            case .finalizePreKeys: return "finalizePreKeys"
            case .rotateOneTimePreKeys: return "rotateOneTimePreKeys"
            case .restoreStorageService: return "restoreStorageService"
            case .backupMasterKey: return "backupMasterKey"
            case .confirmReservedUsername: return "confirmReservedUsername"
            case .rotateManifest: return "rotateManifest"
            case .updateAccountAttribute: return "updateAccountAttribute"
            case .failedRequest: return "failedRequest"
            }
        }
    }

    // MARK: - Opening Path

    @MainActor @Test(arguments: Self.testCases())
    func testOpeningPath_splash(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        setupDefaultAccountAttributes()

        switch mode {
        case .registering:
            // With no state set up, should show the splash.
            #expect(await coordinator.nextStep() == .registrationSplash)
            // Once we show it, don't show it again.
            #expect(await coordinator.continueFromSplash().awaitable() != .registrationSplash)
        case .reRegistering, .changingNumber:
            #expect(await coordinator.nextStep() != .registrationSplash)
        }
    }

    @MainActor @Test(arguments: Self.testCases())
    func testOpeningPath_appExpired(testCase: TestCase) async {
        let coordinator = setupTest(testCase)

        self.stubs.date = .distantFuture

        setupDefaultAccountAttributes()

        // We should start with the banner.
        #expect(await coordinator.nextStep() == .appUpdateBanner)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testOpeningPath_permissions(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        setupDefaultAccountAttributes()

        contactsStore.doesNeedContactsAuthorization = true
        pushRegistrationManagerMock.doesNeedNotificationAuthorization = true

        var nextStep: RegistrationStep
        switch mode {
        case .registering:
            // Gotta get the splash out of the way.
            #expect(await coordinator.nextStep() == .registrationSplash)
            nextStep = await coordinator.continueFromSplash().awaitable()
        case .reRegistering, .changingNumber:
            // No splash for these.
            nextStep = await coordinator.nextStep()
        }

        // Now we should show the permissions.
        #expect(nextStep == .permissions)
        // Doesn't change even if we try and proceed.
        #expect(await coordinator.nextStep() == .permissions)

        // Once the state is updated we can proceed.
        nextStep = await coordinator.requestPermissions().awaitable()
        #expect(nextStep != .registrationSplash)
        #expect(nextStep != .permissions)
    }

    // MARK: - Reg Recovery Password Path

    @MainActor @Test(arguments: Self.testCases(), [true, false])
    func runRegRecoverPwPathTestHappyPath(testCase: TestCase, wasReglockEnabled: Bool) async throws {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        ows2FAManagerMock.isReglockEnabledMock = { wasReglockEnabled }

        // Set a PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }

        let (initialMasterKey, finalMasterKey) = buildKeyDataMocks(testCase)

        // Give it the pin code, which should make it try and register.

        // It needs an apns token to register.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        // It needs prekeys as well.
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        // And will finalize prekeys after success.
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        let identityResponse = Stubs.accountIdentityResponse()
        var authPassword: String!
        let expectedRequest = createAccountWithRecoveryPw(initialMasterKey)
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            matcher: { request in
                // The password is generated internally by RegistrationCoordinator.
                // Extract it so we can check that the same password sent to the server
                // to register is used later for other requests.
                authPassword = request.authPassword
                let requestAttributes = Self.attributesFromCreateAccountRequest(request)
                let recoveryPw = initialMasterKey.regRecoveryPw
                #expect(recoveryPw == (request.parameters["recoveryPassword"] as? String) ?? "")
                #expect(recoveryPw == requestAttributes.registrationRecoveryPassword)
                if wasReglockEnabled {
                    #expect(initialMasterKey.reglockToken == requestAttributes.registrationLockToken)
                } else {
                    #expect(requestAttributes.registrationLockToken == nil)
                }
                return request.url == expectedRequest.url
            },
            statusCode: 200,
            bodyData: try JSONEncoder().encode(identityResponse),
        ))

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: identityResponse.aci,
                pni: identityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // When registered, we should create pre-keys.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        if wasReglockEnabled {
            // If we had reglock before registration, it should be re-enabled.
            let expectedReglockRequest = OWSRequestFactory.enableRegistrationLockV2Request(
                token: finalMasterKey.reglockToken,
                logger: .empty(),
            )
            networkManagerMock.asyncRequestHandlers.append({ request, _ in
                if request.url == expectedReglockRequest.url {
                    #expect(finalMasterKey.reglockToken == request.parameters["registrationLock"] as! String)
                    return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
                }
                throw OWSAssertionError("")
            })
        }

        // We haven't done a SVR backup; that should happen now.
        svr.backupMasterKeyMock = { pin, masterKey, authMethod in
            #expect(pin == Stubs.pinCode)
            // We don't have a SVR auth credential, it should use chat server creds.
            #expect(masterKey.rawData == finalMasterKey.rawData)
            #expect(authMethod == .chatServerAuth(expectedAuthedAccount()))
            self.svr.hasMasterKey = true
            return .value(masterKey)
        }

        // Once we sync push tokens, we should restore from storage service.
        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(initialMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRotateManifestMock({ _, _ in return .value(()) })

        // Once we restore from storage service, we should attempt to reclaim
        // our username.
        let mockUsernameLink: Usernames.UsernameLink = .mocked
        localUsernameManagerMock.startingUsernameState = .available(username: "boba.42", usernameLink: mockUsernameLink)
        usernameApiClientMock.confirmReservedUsernameMocks = [{ _, _, chatServiceAuth in
            #expect(chatServiceAuth == .explicit(
                aci: identityResponse.aci,
                deviceId: .primary,
                password: authPassword,
            ))
            return .success(usernameLinkHandle: mockUsernameLink.handle)
        }]

        // Once we do the username reclamation,
        // we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(finalMasterKey),
            auth: .implicit(), // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.
        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        #expect(await coordinator.submitPINCode(Stubs.pinCode).awaitable() == .done)

        // Since we set profile info, we should have scheduled a reupload.
        #expect(profileManagerMock.didScheduleReuploadLocalProfile)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testRegRecoveryPwPath_wrongPIN(testCase: TestCase) async throws {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        let wrongPinCode = "ABCD"

        // Set a different PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }

        let (initialMasterKey, finalMasterKey) = buildKeyDataMocks(testCase)
        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.

        // Give it the right pin code, which should make it try and register.

        // It needs an apns token to register.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        // Every time we register we also ask for prekeys.
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        // And we finalize them after.
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        let identityResponse = Stubs.accountIdentityResponse()
        var authPassword: String!
        let expectedRequest = createAccountWithRecoveryPw(initialMasterKey)
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            matcher: { request in
                authPassword = request.authPassword
                let requestAttributes = Self.attributesFromCreateAccountRequest(request)
                let recoveryPw = initialMasterKey.regRecoveryPw
                #expect(recoveryPw == (request.parameters["recoveryPassword"] as? String) ?? "")
                #expect(recoveryPw == requestAttributes.registrationRecoveryPassword)
                #expect(requestAttributes.registrationLockToken == nil)
                return request.url == expectedRequest.url
            },
            statusCode: 200,
            bodyData: try JSONEncoder().encode(identityResponse),
        ))

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: identityResponse.aci,
                pni: identityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // When registered, we should create pre-keys.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        // We haven't done a SVR backup; that should happen now.
        svr.backupMasterKeyMock = { pin, masterKey, authMethod in
            #expect(pin == Stubs.pinCode)
            #expect(masterKey.rawData == finalMasterKey.rawData)
            // We don't have a SVR auth credential, it should use chat server creds.
            #expect(authMethod == .chatServerAuth(expectedAuthedAccount()))
            self.svr.hasMasterKey = true
            return .value(masterKey)
        }

        // Once we sync push tokens, we should restore from storage service.
        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(initialMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRotateManifestMock({ _, _ in return .value(()) })

        // Once we restore from storage service, we should attempt to reclaim
        // our username. For this test, let's have a corrupted username (and
        // skip reclamation). This should have no impact on the rest of
        // registration.
        localUsernameManagerMock.startingUsernameState = .usernameAndLinkCorrupted

        // Once we do the storage service restore,
        // we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(finalMasterKey),
            auth: .implicit(), // // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                #expect(finalMasterKey.regRecoveryPw == (request.parameters["recoveryPassword"] as? String) ?? "")
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.

        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        // Give it the wrong PIN, it should reject and give us the same step again.
        #expect(
            await coordinator.submitPINCode(wrongPinCode).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(
                    mode: mode,
                    error: .wrongPin(wrongPin: wrongPinCode),
                    remainingAttempts: 9,
                )),
        )

        #expect(await coordinator.submitPINCode(Stubs.pinCode).awaitable() == .done)

        // Since we set profile info, we should have scheduled a reupload.
        #expect(profileManagerMock.didScheduleReuploadLocalProfile)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testRegRecoveryPwPath_wrongPassword(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Set a PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }

        // Make SVR give us back a reg recovery password.
        let masterKey = AccountEntropyPool().getMasterKey()
        await db.awaitableWrite { accountKeyStore.setMasterKey(masterKey, tx: $0) }
        svr.hasMasterKey = true

        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.

        // Before registering, it should ask for push tokens to give the registration.
        // It will also ask again later when account creation fails and it needs
        // to create a new session.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Every time we register we also ask for prekeys.
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        // And we finalize them after.
        // Set up a list of mocks that should be returned in order
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed.negated)
            return Task {}
        }
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        // Fail the request; the reg recovery pw is invalid.
        let expectedRecoveryPwRequest = createAccountWithRecoveryPw(masterKey)
        let failResponse = TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedRecoveryPwRequest.url.absoluteString,
            statusCode: RegistrationServiceResponses.AccountCreationResponseCodes.unauthorized.rawValue,
        )
        mockURLSession.addResponse(failResponse)

        // Once the first request fails, it should try an start a session. Resolve with a session.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Before requesting a session, it should ask for push tokens to give the session.
        // This was set up above.

        // Then when it gets back the session, it should immediately ask for a verification code to be sent.

        // We'll ask for a push challenge, though we don't need to resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({ "PUSH TOKEN" })

        // Resolve with an updated session.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.
        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        // Check we have the master key now, to be safe.
        #expect(svr.hasMasterKey)

        // Give it the pin code, which should make it try and register.
        // Now we should expect to be at verification code entry since we already set the phone number.
        // No exit allowed since we've already started trying to create the account.
        #expect(
            await coordinator.submitPINCode(Stubs.pinCode).awaitable() ==
                .verificationCodeEntry(
                    stubs.verificationCodeEntryState(mode: mode, exitConfigOverride: .noExitAllowed),
                ),
        )

        // We want to have kept the master key; we failed the reg recovery pw check
        // but that could happen even if the key is valid. Once we finish session based
        // re-registration we want to be able to recover the key.
        #expect(svr.hasMasterKey)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testRegRecoveryPwPath_failedReglock(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Set a PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }

        // Make SVR give us back a reg recovery password.
        let masterKey = AccountEntropyPool().getMasterKey()
        db.write { accountKeyStore.setMasterKey(masterKey, tx: $0) }
        svr.hasMasterKey = true

        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.

        // First we try and create an account with reg recovery
        // password; we will fail with reglock error.
        // First we get apns tokens, then prekeys, then register
        // then finalize prekeys (with failure) after.

        // Once we fail, we try again immediately with the reglock
        // token we fetch.
        // Same sequence as the first request.

        // When that fails, we try and create a session.
        // No prekey stuff this time, just apns token and session requests.

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed.negated)
            return Task {}
        }
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed.negated)
            return Task {}
        }

        // Fail the first request; the reglock is invalid.
        let expectedRecoveryPwRequest = createAccountWithRecoveryPw(masterKey)
        let failResponse = TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedRecoveryPwRequest.url.absoluteString,
            statusCode: RegistrationServiceResponses.AccountCreationResponseCodes.reglockFailed.rawValue,
            bodyJson: EncodableRegistrationLockFailureResponse(
                timeRemainingMs: 10,
                svr2AuthCredential: Stubs.svr2AuthCredential,
            ),
        )
        mockURLSession.addResponse(failResponse)

        // Once the request fails, we should try again with the reglock
        // token, this time.
        mockURLSession.addResponse(failResponse)

        // Once the second request fails, it should try an start a session.

        // We'll ask for a push challenge, though we don't need to resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Resolve with a session.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Then when it gets back the session, it should immediately ask for
        // a verification code to be sent.

        // Resolve with an updated session.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.
        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        #expect(svr.hasMasterKey)

        // Give it the pin code, which should make it try and register.
        // Now we should expect to be at verification code entry since we already set the phone number.
        // No exit allowed since we've already started trying to create the account.
        // We want to have wiped our master key; we failed reglock, which means the key itself is wrong
        #expect(
            await coordinator.submitPINCode(Stubs.pinCode).awaitable() ==
                .verificationCodeEntry(
                    stubs.verificationCodeEntryState(mode: mode, exitConfigOverride: .noExitAllowed),
                ),
        )

        #expect(svr.hasMasterKey.negated)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testRegRecoveryPwPath_retryNetworkError(testCase: TestCase) async throws {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Set a PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }

        let (initialMasterKey, finalMasterKey) = buildKeyDataMocks(testCase)
        svr.hasMasterKey = true

        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.

        // Before registering, it should ask for push tokens to give the registration.
        // When it retries, it will ask again.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Every time we register we also ask for prekeys.
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        // And we finalize them after.
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed.negated)
            return Task {}
        }
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        // Fail the request with a network error.
        let expectedRecoveryPwRequest = createAccountWithRecoveryPw(initialMasterKey)
        let failResponse = TSRequestOWSURLSessionMock.Response.networkError(
            matcher: { _ in
                self.testRun.addObservedStep(.failedRequest)
                return true
            },
            url: expectedRecoveryPwRequest.url,
        )
        mockURLSession.addResponse(failResponse)

        let identityResponse = Stubs.accountIdentityResponse()
        var authPassword: String!

        // Once the first request fails, it should retry. Resolve with success
        let expectedRequest = createAccountWithRecoveryPw(initialMasterKey)
        mockURLSession.addResponse(
            TSRequestOWSURLSessionMock.Response(
                matcher: { request in
                    if request.url == expectedRequest.url {
                        self.testRun.addObservedStep(.createAccount)
                        // The password is generated internally by RegistrationCoordinator.
                        // Extract it so we can check that the same password sent to the server
                        // to register is used later for other requests.
                        authPassword = request.authPassword
                        return true
                    }
                    return false
                },
                statusCode: 200,
                bodyData: try! JSONEncoder().encode(identityResponse),
            ),
        )

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: identityResponse.aci,
                pni: identityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // When registered, it should try and sync pre-keys.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        // We haven't done a SVR backup; that should happen.
        svr.backupMasterKeyMock = { pin, masterKey, authMethod in
            self.testRun.addObservedStep(.backupMasterKey)
            #expect(pin == Stubs.pinCode)
            #expect(masterKey.rawData == finalMasterKey.rawData)
            // We don't have a SVR auth credential, it should use chat server creds.
            #expect(authMethod == .chatServerAuth(expectedAuthedAccount()))
            self.svr.hasMasterKey = true
            return .value(masterKey)
        }

        // Once we back up to svr, we should restore from storage service.
        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(initialMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(finalMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRotateManifestMock({ _, _ in return .value(()) })

        // Once we restore from storage service, we should attempt to reclaim our username.
        let mockUsernameLink: Usernames.UsernameLink = .mocked
        localUsernameManagerMock.startingUsernameState = .available(username: "boba.42", usernameLink: mockUsernameLink)
        usernameApiClientMock.confirmReservedUsernameMocks = [{ _, _, chatServiceAuth in
            self.testRun.addObservedStep(.confirmReservedUsername)
            #expect(chatServiceAuth == .explicit(
                aci: identityResponse.aci,
                deviceId: .primary,
                password: authPassword,
            ))
            return .success(usernameLinkHandle: mockUsernameLink.handle)
        }]

        // Once we do the storage service restore,
        // we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(finalMasterKey),
            auth: .implicit(), // // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                self.testRun.addObservedStep(.updateAccountAttribute)
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.
        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        // Give it the pin code, which should make it try and register.
        #expect(await coordinator.submitPINCode(Stubs.pinCode).awaitable() == .done)

        var expectedSteps: [TestStep] = [
            .requestPushToken,
            .createPreKeys,
            .failedRequest,
            .finalizePreKeys,
            .requestPushToken,
            .createPreKeys,
            .createAccount,
            .finalizePreKeys,
            .rotateOneTimePreKeys,
            // .restoreStorageService, // If going from MasterKey -> AEP
            .backupMasterKey,
            // .restoreStorageService,
            .confirmReservedUsername,
            .rotateManifest,
            .updateAccountAttribute,
        ]

        if testCase.newKey == .accountEntropyPool, testCase.oldKey != .accountEntropyPool {
            expectedSteps.insert(.restoreStorageService, at: 9)
        } else {
            expectedSteps.insert(.restoreStorageService, at: 10)
        }
        #expect(testRun.recordedSteps == expectedSteps)

        // Since we set profile info, we should have scheduled a reupload.
        #expect(profileManagerMock.didScheduleReuploadLocalProfile)
    }

    // Test the reglock path when a user has a local password
    // Tests a similar path to testRegRecoveryPwPath_failedReglock above,
    // but returns a `regRecoveryPasswordRejected` error in the first
    // createAccount attempt, since this is the path that happens in the app.
    // Keeping 'testRegRecoveryPwPath_failedReglock' around since it's still
    // technically a possible path and should still be validated.
    @MainActor @Test(arguments: Self.testCases())
    func testRegRecoveryPwPath_failedReglock2(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Set a PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }
        ows2FAManagerMock.isReglockEnabledMock = { true }

        // Make SVR give us back a reg recovery password.
        let masterKey = AccountEntropyPool().getMasterKey()
        db.write { accountKeyStore.setMasterKey(masterKey, tx: $0) }
        svr.hasMasterKey = true

        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.

        // First we try and create an account with reg recovery
        // password; we will fail with reglock error.
        // First we get apns tokens, then prekeys, then register
        // then finalize prekeys (with failure) after.

        // Once we fail, we try again immediately with the reglock
        // token we fetch.
        // Same sequence as the first request.

        // When that fails, we try and create a session.
        // No prekey stuff this time, just apns token and session requests.

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        preKeyManagerMock.addFinalizePreKeyMock({ _ in Task {} })
        preKeyManagerMock.addFinalizePreKeyMock({ _ in Task {} })

        // Fail the first request;
        let expectedRecoveryPwRequest = createAccountWithRecoveryPw(masterKey)
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedRecoveryPwRequest.url.absoluteString,
            statusCode: RegistrationServiceResponses.AccountCreationResponseCodes.regRecoveryPasswordRejected.rawValue,
            bodyJson: EncodableRegistrationLockFailureResponse(
                timeRemainingMs: 10,
                svr2AuthCredential: Stubs.svr2AuthCredential,
            ),
        ))

        // Once the first request fails, it should try an start a session.
        // We'll ask for a push challenge, though we don't need to resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Resolve with a session.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Then when it gets back the session, it should immediately ask for
        // a verification code to be sent.
        // Resolve with an updated session.

        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        // Give back an valid session.
        sessionManager.addSubmitCodeResponseMock(.success(stubs.session(
            receivedDate: date,
            verified: true,
        )))

        // Once the request fails, we should try again with the reglock
        // token, this time.
        let expectedRecoveryPwRequest2 = createAccountWithSession(masterKey)
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedRecoveryPwRequest2.url.absoluteString,
            statusCode: RegistrationServiceResponses.AccountCreationResponseCodes.reglockFailed.rawValue,
            bodyJson: EncodableRegistrationLockFailureResponse(
                timeRemainingMs: 10000,
                svr2AuthCredential: Stubs.svr2AuthCredential,
            ),
        ))

        #expect(svr.hasMasterKey)

        let acknowledgeAction: RegistrationReglockTimeoutAcknowledgeAction = switch testCase.mode {
        case .registering: .resetPhoneNumber
        case .changingNumber, .reRegistering: .none
        }

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.
        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        // Give it the pin code, which should make it try and register.
        _ = await coordinator.submitPINCode(Stubs.pinCode).awaitable()

        #expect(
            await coordinator.submitVerificationCode(Stubs.pinCode).awaitable() ==
                .reglockTimeout(
                    RegistrationReglockTimeoutState(
                        reglockExpirationDate: dateProvider().addingTimeInterval(TimeInterval(10)),
                        acknowledgeAction: acknowledgeAction,
                    ),
                ),
        )

        // We want to have wiped our master key; we failed reglock, which means the key itself is wrong.
        #expect(svr.hasMasterKey)
    }

    // Test the path where a the local masterkey is no longer in sync with the one storedin SVR
    // This can happen a lot more often in an AEP enabled world, which means that during registration
    // we may need to go fetch the current key from SVR after failing the first registration attempt
    @MainActor @Test(arguments: Self.onlyReRegisteringTestCases())
    func testRegRecoveryPwPath_reglock_failedLocalCredentials(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Set a PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }
        ows2FAManagerMock.isReglockEnabledMock = { true }

        // Make SVR give us back a reg recovery password.
        let (masterKey, newMasterKey) = buildKeyDataMocks(testCase)
        let remoteMasterKey = MasterKey()
        // For non-AEP, we will replace the local key with the remote key.
        // For AEP, we'll rotate to a new AEP (or use the existing local AEP if it's present)
        let finalMasterKey = testCase.newKey == .masterKey ? remoteMasterKey : newMasterKey
        svr.hasMasterKey = true

        // Put some auth credentials in storage.
        let svr2CredentialCandidates: [SVR2AuthCredential] = [
            Stubs.svr2AuthCredential,
        ]
        svrAuthCredentialStore.svr2Dict = Dictionary(grouping: svr2CredentialCandidates, by: \.credential.username).mapValues { $0.first! }

        // Give it a phone number, which should cause it to check the auth credentials.
        // Match the main auth credential.
        let expectedSVR2CheckRequest = RegistrationRequestFactory.svr2AuthCredentialCheckRequest(
            e164: Stubs.e164,
            credentials: svr2CredentialCandidates,
            logger: .empty(),
        )
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedSVR2CheckRequest.url.absoluteString,
            statusCode: 200,
            bodyJson: RegistrationServiceResponses.SVR2AuthCheckResponse(matches: [
                "\(Stubs.svr2AuthCredential.credential.username):\(Stubs.svr2AuthCredential.credential.password)": .match,
            ]),
        ))

        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.
        svr.restoreKeysMock = { pin, authMethod in
            #expect(pin == Stubs.pinCode)
            #expect(authMethod == .svrAuth(Stubs.svr2AuthCredential, backup: nil))
            self.svr.hasMasterKey = true
            return .value(.success(remoteMasterKey))
        }

        // First we try and create an account with reg recovery
        // password; we will fail with reglock error.
        // First we get apns tokens, then prekeys, then register
        // then finalize prekeys (with failure) after.

        // Once we fail, attempt to fetch the remote SVR credential and attempt RRP again
        // Same sequence as the first request.

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed.negated)
            return Task {}
        }
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        // Fail the first request; the reglock is invalid.
        let expectedRecoveryPwRequest = createAccountWithRecoveryPw(masterKey)
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedRecoveryPwRequest.url.absoluteString,
            statusCode: RegistrationServiceResponses.AccountCreationResponseCodes.regRecoveryPasswordRejected.rawValue,
            bodyJson: EncodableRegistrationLockFailureResponse(
                timeRemainingMs: 10,
                svr2AuthCredential: Stubs.svr2AuthCredential,
            ),
        ))

        // Once the request fails, we should try again with the reglock
        // token, this time.
        let accountIdentityResponse = Stubs.accountIdentityResponse()
        var authPassword: String!
        let expectedRecoveryPwRequest2 = createAccountWithRecoveryPw(remoteMasterKey)
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            matcher: { request in
                authPassword = request.authPassword
                let requestAttributes = Self.attributesFromCreateAccountRequest(request)
                #expect((request.parameters["recoveryPassword"] as? String) == remoteMasterKey.regRecoveryPw)
                #expect(remoteMasterKey.reglockToken == requestAttributes.registrationLockToken)
                return request.url == expectedRecoveryPwRequest2.url
            },
            statusCode: 200,
            bodyJson: accountIdentityResponse,
        ))

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: accountIdentityResponse.aci,
                pni: accountIdentityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // When registered, we should create pre-keys.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        // If we had reglock before registration, it should be re-enabled.
        let expectedReglockRequest = OWSRequestFactory.enableRegistrationLockV2Request(
            token: finalMasterKey.reglockToken,
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedReglockRequest.url {
                #expect(finalMasterKey.reglockToken == request.parameters["registrationLock"] as! String)
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // We haven't done a SVR backup; that should happen now.
        svr.backupMasterKeyMock = { pin, masterKey, authMethod in
            #expect(pin == Stubs.pinCode)
            // We don't have a SVR auth credential, it should use chat server creds.
            #expect(masterKey.rawData == finalMasterKey.rawData)
            #expect(authMethod == .svrAuth(
                Stubs.svr2AuthCredential,
                backup: .chatServerAuth(expectedAuthedAccount()),
            ))
            self.svr.hasMasterKey = true
            return .value(masterKey)
        }

        // Once we sync push tokens, we should restore from storage service.
        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(remoteMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        // Once we restore from storage service, we should attempt to reclaim
        // our username.
        let mockUsernameLink: Usernames.UsernameLink = .mocked
        localUsernameManagerMock.startingUsernameState = .available(username: "boba.42", usernameLink: mockUsernameLink)
        usernameApiClientMock.confirmReservedUsernameMocks = [{ _, _, chatServiceAuth in
            #expect(chatServiceAuth == .explicit(
                aci: accountIdentityResponse.aci,
                deviceId: .primary,
                password: authPassword,
            ))
            return .success(usernameLinkHandle: mockUsernameLink.handle)
        }]

        storageServiceManagerMock.addRotateManifestMock({ _, _ in return .value(()) })

        // Once we do the username reclamation,
        // we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(finalMasterKey),
            auth: .implicit(), // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.
        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        #expect(svrAuthCredentialStore.svr2Dict[Stubs.svr2AuthCredential.credential.username] != nil)

        #expect(svr.hasMasterKey)

        // Give it the pin code, which should make it try and register.
        #expect(await coordinator.submitPINCode(Stubs.pinCode).awaitable() == .done)

        #expect(svr.hasMasterKey)
    }

    /// Test the path where both local and remote RRP are rejected due to a reglock challenge
    /// This should result in the following high level flow:
    /// 1. Fail with local master key RRP.  This can be from th remote key being rotated, or a reglock challenge
    /// 2. Fetch the remote master key from SVR
    /// 3. Fail with the remote master key RRP.  This is usually from a reglock challenge
    /// 4. Clear SVR state and attempt to register via session
    /// 5. Fail due to reglock
    /// This should result in the app being in a reglock timeout
    @MainActor @Test(arguments: Self.onlyReRegisteringTestCases())
    func testRegRecoveryPwPath_reglock_localAndRemoteKeysRejected(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Set a PIN on disk.
        ows2FAManagerMock.pinCodeMock = { Stubs.pinCode }
        ows2FAManagerMock.isReglockEnabledMock = { true }

        // Make SVR give us back a reg recovery password.
        let (masterKey, _) = buildKeyDataMocks(testCase)
        let remoteMasterKey = MasterKey()
        // For non-AEP, we will replace the local key with the remote key.
        // For AEP, we'll rotate to a new AEP (or use the existing local AEP if it's present)
        svr.hasMasterKey = true

        // Put some auth credentials in storage.
        let svr2CredentialCandidates: [SVR2AuthCredential] = [
            Stubs.svr2AuthCredential,
        ]
        svrAuthCredentialStore.svr2Dict = Dictionary(grouping: svr2CredentialCandidates, by: \.credential.username).mapValues { $0.first! }

        // Give it a phone number, which should cause it to check the auth credentials.
        // Match the main auth credential.
        let expectedSVR2CheckRequest = RegistrationRequestFactory.svr2AuthCredentialCheckRequest(
            e164: Stubs.e164,
            credentials: svr2CredentialCandidates,
            logger: .empty(),
        )
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedSVR2CheckRequest.url.absoluteString,
            statusCode: 200,
            bodyJson: RegistrationServiceResponses.SVR2AuthCheckResponse(matches: [
                "\(Stubs.svr2AuthCredential.credential.username):\(Stubs.svr2AuthCredential.credential.password)": .match,
            ]),
        ))

        // NOTE: We expect to skip opening path steps because
        // if we have a SVR master key locally, this _must_ be
        // a previously registered device, and we can skip intros.

        svr.restoreKeysMock = { pin, authMethod in
            #expect(pin == Stubs.pinCode)
            #expect(authMethod == .svrAuth(Stubs.svr2AuthCredential, backup: nil))
            self.svr.hasMasterKey = true
            return .value(.success(remoteMasterKey))
        }

        // First we try and create an account with reg recovery
        // password; we will fail with reglock error.
        // First we get apns tokens, then prekeys, then register
        // then finalize prekeys (with failure) after.

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        preKeyManagerMock.addFinalizePreKeyMock({ _ in Task {} })
        preKeyManagerMock.addFinalizePreKeyMock({ _ in Task {} })
        preKeyManagerMock.addFinalizePreKeyMock({ _ in Task {} })

        // Fail the first request; the local key is invalid.
        let expectedRecoveryPwRequest = createAccountWithRecoveryPw(masterKey)
        let failResponse = TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedRecoveryPwRequest.url.absoluteString,
            statusCode: RegistrationServiceResponses.AccountCreationResponseCodes.regRecoveryPasswordRejected.rawValue,
            bodyJson: EncodableRegistrationLockFailureResponse(
                timeRemainingMs: 10000,
                svr2AuthCredential: Stubs.svr2AuthCredential,
            ),
        )
        mockURLSession.addResponse(failResponse)
        mockURLSession.addResponse(failResponse)

        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Resolve with an updated session.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        // Resolve with a session.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Once the request fails, we should try again with the reglock
        // token, this time.
        // The third attempt should fall back to session using the remote key(?)
        let expectedRecoveryPwRequest3 = createAccountWithSession(remoteMasterKey)
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedRecoveryPwRequest3.url.absoluteString,
            statusCode: RegistrationServiceResponses.AccountCreationResponseCodes.reglockFailed.rawValue,
            bodyJson: EncodableRegistrationLockFailureResponse(
                timeRemainingMs: 10000,
                svr2AuthCredential: Stubs.svr2AuthCredential,
            ),
        ))

        // Give back a verified session.
        sessionManager.addSubmitCodeResponseMock(.success(stubs.session(verified: true)))

        let acknowledgeAction: RegistrationReglockTimeoutAcknowledgeAction = switch testCase.mode {
        case .registering: .resetPhoneNumber
        case .changingNumber, .reRegistering: .none
        }

        // We haven't set a phone number so it should ask for that.
        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it a phone number, which should show the PIN entry step.
        // Now it should ask for the PIN to confirm the user knows it.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForRegRecoveryPath(mode: mode)),
        )

        // Give it the pin code, which should make it try and register.
        #expect(
            await coordinator.submitPINCode(Stubs.pinCode).awaitable() ==
                .verificationCodeEntry(
                    stubs.verificationCodeEntryState(
                        mode: mode,
                        // TODO: [Refactor]: Is 'noExitAllowed' the correct value to expect here?
                        exitConfigOverride: .noExitAllowed,
                    ),
                ),
        )

        #expect(svr.hasMasterKey)

        // Submit verification code
        #expect(
            await coordinator.submitVerificationCode(Stubs.verificationCode).awaitable() ==
                .reglockTimeout(
                    RegistrationReglockTimeoutState(
                        reglockExpirationDate: dateProvider().addingTimeInterval(TimeInterval(10)),
                        acknowledgeAction: acknowledgeAction,
                    ),
                ),
        )

        // We want to have wiped our master key; we failed reglock, which means the key itself is wrong.
        #expect(svr.hasMasterKey)
    }

    // MARK: - SVR Auth Credential Path

    @MainActor @Test(arguments: Self.testCases())
    func testSVRAuthCredentialPath_happyPath(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        setupDefaultAccountAttributes()

        // Set profile info so we skip those steps.
        setAllProfileInfo()

        mockSVRCredentials(isMatch: true)

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        let (initialMasterKey, finalMasterKey) = buildKeyDataMocks(testCase)

        // Resolve the key restoration from SVR and have it start returning the key.
        svr.restoreKeysMock = { pin, authMethod in
            self.testRun.addObservedStep(.restoreKeys)
            #expect(pin == Stubs.pinCode)
            #expect(authMethod == .svrAuth(Stubs.svr2AuthCredential, backup: nil))
            self.svr.hasMasterKey = true
            return .value(.success(initialMasterKey))
        }

        // Before registering, it should ask for push tokens to give the registration.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Every time we register we also ask for prekeys.
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        // And we finalize them after.
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        // Now still at it should make a reg recovery pw request
        let accountIdentityResponse = Stubs.accountIdentityResponse()
        var authPassword: String!
        let expectedRegRecoveryPwRequest = createAccountWithRecoveryPw(initialMasterKey)
        mockURLSession.addResponse(
            TSRequestOWSURLSessionMock.Response(
                matcher: { request in
                    self.testRun.addObservedStep(.createAccount)
                    authPassword = request.authPassword
                    return request.url == expectedRegRecoveryPwRequest.url
                },
                statusCode: 200,
                bodyJson: accountIdentityResponse,
            ),
        )

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: accountIdentityResponse.aci,
                pni: accountIdentityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // When registered, it should try and create pre-keys.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        // Once we create pre-keys, we should back up to svr.
        svr.backupMasterKeyMock = { pin, masterKey, authMethod in
            self.testRun.addObservedStep(.backupMasterKey)
            #expect(pin == Stubs.pinCode)
            #expect(masterKey.rawData == finalMasterKey.rawData)
            #expect(authMethod == .svrAuth(
                Stubs.svr2AuthCredential,
                backup: .chatServerAuth(expectedAuthedAccount()),
            ))
            return .value(masterKey)
        }

        // Once we back up to svr, we should restore from storage service.
        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(initialMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(finalMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRotateManifestMock({ _, _ in return .value(()) })

        // Once we restore from storage service, we should attempt to reclaim our username.
        let mockUsernameLink: Usernames.UsernameLink = .mocked
        localUsernameManagerMock.startingUsernameState = .available(username: "boba.42", usernameLink: mockUsernameLink)
        usernameApiClientMock.confirmReservedUsernameMocks = [{ _, _, chatServiceAuth in
            self.testRun.addObservedStep(.confirmReservedUsername)
            #expect(chatServiceAuth == .explicit(
                aci: accountIdentityResponse.aci,
                deviceId: .primary,
                password: authPassword,
            ))
            return .success(usernameLinkHandle: mockUsernameLink.handle)
        }]

        // Once we do the storage service restore, we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(finalMasterKey),
            auth: .implicit(), // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                self.testRun.addObservedStep(.updateAccountAttribute)
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // At this point, we should be asking for PIN entry so we can use the credential
        // to recover the SVR master key.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForSVRAuthCredentialPath(mode: mode)),
        )

        // We should have wiped the invalid and unknown credentials.
        let remainingCredentials = svrAuthCredentialStore.svr2Dict
        #expect(remainingCredentials[Stubs.svr2AuthCredential.credential.username] != nil)
        #expect(remainingCredentials["aaaa"] != nil)
        #expect(remainingCredentials["zzzz"] == nil)
        #expect(remainingCredentials["0000"] == nil)
        // SVR should be untouched.
        #expect(svrAuthCredentialStore.svr2Dict[Stubs.svr2AuthCredential.credential.username] != nil)

        // Enter the PIN, which should try and recover from SVR.
        // Once we do that, it should follow the Reg Recovery Password Path.
        #expect(await coordinator.submitPINCode(Stubs.pinCode).awaitable() == .done)

        var expectedSteps: [TestStep] = [
            .restoreKeys,
            .requestPushToken,
            .createPreKeys,
            .createAccount,
            .finalizePreKeys,
            .rotateOneTimePreKeys,
            //            "restoreStorageService",
            .backupMasterKey,
            //            "restoreStorageService",
            .confirmReservedUsername,
            .rotateManifest,
            .updateAccountAttribute,
        ]

        if testCase.newKey == .accountEntropyPool {
            expectedSteps.insert(.restoreStorageService, at: 6)
        } else {
            expectedSteps.insert(.restoreStorageService, at: 7)
        }

        #expect(testRun.recordedSteps == expectedSteps)

        // Since we set profile info, we should have scheduled a reupload.
        #expect(profileManagerMock.didScheduleReuploadLocalProfile)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSVRAuthCredentialPath_noMatchingCredentials(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Put some auth credentials in storage.
        mockSVRCredentials(isMatch: false)

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Once the first request fails, it should try an start a session.
        // We'll ask for a push challenge, though we don't need to resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Resolve with a session.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Then when it gets back the session, it should immediately ask for
        // a verification code to be sent.
        // Resolve with an updated session.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Give it a phone number, which should cause it to check the auth credentials.
        // Now we should expect to be at verification code entry since we already set the phone number.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )

        // We should have wipted the invalid and unknown credentials.
        let remainingCredentials = svrAuthCredentialStore.svr2Dict
        #expect(remainingCredentials[Stubs.svr2AuthCredential.credential.username] != nil)
        #expect(remainingCredentials["aaaa"] != nil)
        #expect(remainingCredentials["zzzz"] == nil)
        #expect(remainingCredentials["0000"] == nil)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSVRAuthCredentialPath_noMatchingCredentialsThenChangeNumber(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        let originalE164 = E164("+17875550100")!
        let changedE164 = E164("+17875550101")!

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Put some auth credentials in storage.
        let credentialCandidates: [SVR2AuthCredential] = [Stubs.svr2AuthCredential]
        svrAuthCredentialStore.svr2Dict = Dictionary(grouping: credentialCandidates, by: \.credential.username).mapValues { $0.first! }

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Don't give back any matches, which means we will want to create a session as a fallback.
        var expectedSVRCheckRequest = RegistrationRequestFactory.svr2AuthCredentialCheckRequest(
            e164: originalE164,
            credentials: credentialCandidates,
            logger: .empty(),
        )
        mockURLSession.addResponse(
            TSRequestOWSURLSessionMock.Response(
                urlSuffix: expectedSVRCheckRequest.url.absoluteString,
                statusCode: 200,
                bodyJson: RegistrationServiceResponses.SVR2AuthCheckResponse(matches: [
                    "\(Stubs.svr2AuthCredential.credential.username):\(Stubs.svr2AuthCredential.credential.password)": .notMatch,
                ]),
            ),
        )

        // Once the first request fails, it should try an start a session.
        // We'll ask for a push challenge, though we don't need to resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Resolve with a session.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(e164: originalE164)))

        // Then when it gets back the session, it should immediately ask for a verification code to be sent.
        // Resolve with an updated session.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Give a match, so it registers via SVR auth credential.
        expectedSVRCheckRequest = RegistrationRequestFactory.svr2AuthCredentialCheckRequest(
            e164: changedE164,
            credentials: credentialCandidates,
            logger: .empty(),
        )
        mockURLSession.addResponse(
            TSRequestOWSURLSessionMock.Response(
                urlSuffix: expectedSVRCheckRequest.url.absoluteString,
                statusCode: 200,
                bodyJson: RegistrationServiceResponses.SVR2AuthCheckResponse(matches: [
                    "\(Stubs.svr2AuthCredential.credential.username):\(Stubs.svr2AuthCredential.credential.password)": .match,
                ]),
            ),
        )

        // Give it a phone number, which should cause it to check the auth credentials.
        // Now we should expect to be at verification code entry since we already set the phone number.
        #expect(
            await coordinator.submitE164(originalE164).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )

        // We should have wiped the invalid and unknown credentials.
        #expect(svrAuthCredentialStore.svr2Dict[Stubs.svr2AuthCredential.credential.username] != nil)

        // Now change the phone number; this should take us back to phone number entry.
        #expect(
            await coordinator.requestChangeE164().awaitable() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Now it should ask for PIN entry; we are on the SVR auth credential path.
        #expect(
            await coordinator.submitE164(changedE164).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForSVRAuthCredentialPath(mode: mode)),
        )
    }

    // MARK: - Session Path

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_happyPath(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode
        let newMasterKey = Stubs.accountEntropyPoolToGenerate.getMasterKey()
        var authPassword: String!

        await createSessionAndRequestFirstCode(coordinator: coordinator, mode: mode)

        // Give back a verified session.
        sessionManager.addSubmitCodeResponseMock(.success(stubs.session(verified: true)))

        let accountIdentityResponse = Stubs.accountIdentityResponse()

        // That means it should try and register with the verified session;
        // Before registering, it should ask for push tokens to give the registration.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // It should also fetch the prekeys for account creation
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        let expectedRequest = createAccountWithSession(newMasterKey)
        mockURLSession.addResponse(
            TSRequestOWSURLSessionMock.Response(
                matcher: { request in
                    authPassword = request.authPassword
                    return request.url == expectedRequest.url
                },
                statusCode: 200,
                bodyJson: accountIdentityResponse,
            ),
        )

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: accountIdentityResponse.aci,
                pni: accountIdentityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // Once we are registered, we should finalize prekeys.
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        // Then we should try and create one time pre-keys
        // with the credentials we got in the identity response.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        // Finish the validation.
        svr.backupMasterKeyMock = { pin, masterKey, authMethod in
            #expect(pin == Stubs.pinCode)
            #expect(masterKey.rawData == newMasterKey.rawData)
            #expect(authMethod == .chatServerAuth(expectedAuthedAccount()))
            return .value(masterKey)
        }

        // Once we sync push tokens, we should restore from storage service.
        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(newMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        // Once we restore from storage service, we should attempt to reclaim
        // our username. For this test, let's fail. This should have
        // no different impact on the rest of registration.
        let mockUsernameLink: Usernames.UsernameLink = .mocked
        localUsernameManagerMock.startingUsernameState = .available(username: "boba.42", usernameLink: mockUsernameLink)
        usernameApiClientMock.confirmReservedUsernameMocks = [{ _, _, chatServiceAuth in
            #expect(chatServiceAuth == .explicit(
                aci: accountIdentityResponse.aci,
                deviceId: .primary,
                password: authPassword,
            ))
            throw OWSGenericError("Something went wrong :(")
        }]

        // And once we do the storage service restore,
        // we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(newMasterKey),
            auth: .implicit(), // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        storageServiceManagerMock.addRotateManifestMock({ _, _ in return .value(()) })

        // Submit a code.
        // Now we should ask to create a PIN.
        // No exit allowed since we've already started trying to create the account.
        #expect(
            await coordinator.submitVerificationCode(Stubs.pinCode).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForPostRegCreate(mode: mode, exitConfigOverride: .noExitAllowed)),
        )

        // Confirm the pin first.
        // No exit allowed since we've already started trying to create the account.
        #expect(
            await coordinator.setPINCodeForConfirmation(.stub()).awaitable() ==
                .pinEntry(Stubs.pinEntryStateForPostRegConfirm(mode: mode, exitConfigOverride: .noExitAllowed)),
        )

        // When we submit the pin, it should backup with SVR.
        #expect(await coordinator.submitPINCode(Stubs.pinCode).awaitable() == .done)

        // Since we set profile info, we should have scheduled a reupload.
        #expect(profileManagerMock.didScheduleReuploadLocalProfile)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_invalidE164(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode
        let badE164 = E164("+15555555555")!

        switch mode {
        case .registering, .changingNumber:
            break
        case .reRegistering:
            // no changing the number when reregistering
            return
        }

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Reject for invalid argument (the e164).
        sessionManager.addBeginSessionResponseMock(.invalidArgument)

        // Give it a phone number, which should cause it to start a session.
        // It should put us on the phone number entry screen again
        // with an error.
        #expect(
            await coordinator.submitE164(badE164).awaitable() ==
                .phoneNumberEntry(
                    stubs.phoneNumberEntryState(
                        mode: mode,
                        previouslyEnteredE164: badE164,
                        withValidationErrorFor: .invalidArgument,
                    ),
                ),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_rateLimitSessionCreation(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        let retryTimeInterval: TimeInterval = 5

        // Reject with a rate limit.
        sessionManager.addBeginSessionResponseMock(.retryAfter(retryTimeInterval))

        // Give it a phone number, which should cause it to start a session.
        // It should put us on the phone number entry screen again
        // with an error.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .phoneNumberEntry(
                    stubs.phoneNumberEntryState(
                        mode: mode,
                        previouslyEnteredE164: Stubs.e164,
                        withValidationErrorFor: .retryAfter(15),
                    ),
                ),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_cantSendFirstSMSCode(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Give back a session, but with SMS code rate limiting already.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            nextSMS: 10,
        )))

        // Give it a phone number, which should cause it to start a session.
        // It should put us on the verification code entry screen with an error.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextSMS: 10,
                    nextVerificationAttempt: nil,
                    validationError: .smsResendTimeout,
                )),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_landline(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Give back a session that's ready to go.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            nextCall: nil, /* initially calling unavailable */
        )))

        // Once we get that session, we should try and send a code.
        // Resolve with a transport error
        // and no next verification attempt on the session,
        // so it counts as transport failure with no code sent.
        sessionManager.addRequestCodeResponseMock(.transportError(stubs.session(
            nextSMS: nil, /* now sms unavailable but calling is */
        )))

        // If we resend via voice, that should put us in a happy path. Resolve with a success.
        sessionManager.didRequestCode = false
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        // Give it a phone number, which should cause it to start a session.
        // We should get back the code entry step, with a validation error for the sms transport.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextSMS: nil,
                    nextVerificationAttempt: nil,
                    validationError: .failedInitialTransport(failedTransport: .sms),
                )),
        )

        // We should get back the code entry step.
        #expect(
            await coordinator.requestVoiceCode().awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )
        #expect(sessionManager.didRequestCode)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_landline_submitCodeWithNoneSentYet(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Give back a session that's ready to go.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Once we get that session, we should try and send a code.

        // Resolve with a transport error,
        // and no next verification attempt on the session,
        // so it counts as transport failure with no code sent.
        sessionManager.addRequestCodeResponseMock(.transportError(stubs.session()))

        // If we try and submit a code, we should get an error sheet
        // because a code never got sent in the first place.
        // (If the server rejects the submission, which it obviously should).
        sessionManager.addSubmitCodeResponseMock(.disallowed(stubs.session()))

        // Give it a phone number, which should cause it to start a session.
        // We should get back the code entry step,
        // with a validation error for the sms transport.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextVerificationAttempt: nil,
                    validationError: .failedInitialTransport(failedTransport: .sms),
                )),
        )

        // The server says no code is available to submit. We know
        // we never sent a code, so show a unique error for that
        // but keep the user on the code entry screen so they can
        // retry sending a code with a transport method of their choice.

        #expect(
            await coordinator.submitVerificationCode(Stubs.verificationCode).awaitable() ==
                .showErrorSheet(.submittingVerificationCodeBeforeAnyCodeSent),
        )

        #expect(
            await coordinator.nextStep() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextVerificationAttempt: nil,
                    validationError: .failedInitialTransport(failedTransport: .sms),
                )),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_rateLimitFirstSMSCode(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // We'll ask for a push challenge, though we won't resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Give back a session that's ready to go.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            receivedDate: self.date,
        )))

        // Once we get that session, we should try and send a code.

        // Reject with a timeout.
        sessionManager.addRequestCodeResponseMock(.retryAfterTimeout(
            stubs.session(receivedDate: self.date, nextSMS: 10),
            retryAfterHeader: nil,
        ))

        // Give it a phone number, which should cause it to start a session.
        // It should put us on the phone number entry screen again
        // with an error.
        let step = await coordinator.submitE164(Stubs.e164).awaitable()
        #expect(
            step ==
                .phoneNumberEntry(
                    stubs.phoneNumberEntryState(
                        mode: mode,
                        previouslyEnteredE164: Stubs.e164,
                        withValidationErrorFor: .retryAfter(15),
                    ),
                ),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_changeE164(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        let originalE164 = E164("+17875550100")!
        let changedE164 = E164("+17875550101")!

        // We'll ask for a push challenge, though we won't resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Give back a session that's ready to go.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            e164: originalE164,
        )))

        // Once we get that session, we should try and send a code.
        // Give back a session with a sent code.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            e164: originalE164,
            nextVerificationAttempt: 0,
        )))

        // These mocks are removed after each use, so set up another
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // We'll ask for a push challenge, though we won't resolve it in this test.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Give back a session that's ready to go.
        // TODO: allow mocking multiple responses
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            e164: changedE164,
        )))

        // Once we get that session, we should try and send a code.
        // Give back a session with a sent code.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            e164: changedE164,
            nextVerificationAttempt: 0,
        )))

        // Give it a phone number, which should cause it to start a session.
        // We should be on the verification code entry screen.
        #expect(
            await coordinator.submitE164(originalE164).awaitable() ==
                .verificationCodeEntry(
                    stubs.verificationCodeEntryState(mode: mode, e164: originalE164),
                ),
        )

        // Ask to change the number; this should put us back on phone number entry.
        #expect(
            await coordinator.requestChangeE164().awaitable() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give it the new phone number, which should cause it to start a session.
        // We should be on the verification code entry screen.
        // TODO: Missing a 'requestPushToken'?
        #expect(
            await coordinator.submitE164(changedE164).awaitable() ==
                .verificationCodeEntry(
                    stubs.verificationCodeEntryState(mode: mode, e164: changedE164),
                ),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_captchaChallenge(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Give back a session with a captcha challenge.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.captcha],
        )))

        // Give back a session without the challenge.
        sessionManager.addFulfillChallengeResponseMock(.success(stubs.session()))

        // That means it should try and send a code;
        // Resolve with a session.
        // The session has a sent code, but requires a challenge to send
        // a code again. That should be ignored until we ask to send another code.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
            allowedToRequestCode: false,
            requestedInformation: [.captcha],
        )))

        // Give back a session without the challenge.
        sessionManager.addFulfillChallengeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
        )))

        // Give it a phone number, which should cause it to start a session.
        // Once we get that session, we should get a captcha step back.
        #expect(await coordinator.submitE164(Stubs.e164).awaitable() == .captchaChallenge)

        // We should get back the code entry step. Submit a captcha challenge.
        #expect(
            await coordinator.submitCaptcha(Stubs.captchaToken).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )

        // Now try and resend a code, which should hit us with the captcha challenge immediately.
        #expect(await coordinator.requestSMSCode().awaitable() == .captchaChallenge)

        // This means when we fulfill the challenge, it should
        // immediately try and send the code that couldn't be sent before because
        // of the challenge.
        stubs.date = date.addingTimeInterval(10)
        let secondCodeDate = date

        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            receivedDate: secondCodeDate,
            nextVerificationAttempt: 0,
        )))

        // Submit a captcha challenge.
        // Once all is done, we should have a new code and be back on the code
        // entry screen.
        // TODO[Registration]: test that the "next SMS code" state is properly set
        // given the new sms code date above.
        #expect(
            await coordinator.submitCaptcha(Stubs.captchaToken).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_pushChallenge(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({ "a pre-auth challenge token" })

        // Give back a session with a push challenge.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge],
        )))

        sessionManager.addFulfillChallengeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
        )))

        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge],
        )))

        // Give the push challenge token. Also prepare to handle its usage, and the
        // resulting request for another SMS code.

        // Give it a phone number, which should cause it to start a session.
        _ = await coordinator.submitE164(Stubs.e164).awaitable()

        // We should still be waiting.
        #expect(
            await coordinator.nextStep() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )
        #expect(
            sessionManager.latestChallengeFulfillment ==
                .pushChallenge("a pre-auth challenge token"),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_pushChallengeTimeoutAfterResolutionThatTakesTooLong(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Prepare to provide the challenge token.
        let (challengeTokenPromise, _) = Guarantee<String>.pending()
        let receivePreAuthChallengeTokenCount = AtomicUInt(lock: .init())

        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock {
            receivePreAuthChallengeTokenCount.increment()
            return await challengeTokenPromise.awaitable()
        }

        // Give back a session with a push challenge.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge],
        )))

        // Take too long to resolve with the challenge token.
        timeoutProviderMock.pushTokenMinWaitTime = 0.5
        timeoutProviderMock.pushTokenTimeout = 2

        // Give it a phone number, which should cause it to start a session.
        let nextStep = await coordinator.submitE164(Stubs.e164).awaitable()
        #expect(nextStep == .showErrorSheet(.sessionInvalidated))

        // One time to set up, one time for the min wait time, one time
        // for the full timeout.
        #expect(receivePreAuthChallengeTokenCount.get() == 3)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_pushChallengeTimeoutAfterNoResolution(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // We'll never provide a challenge token and will just leave it around forever.
        let (challengeTokenPromise, _) = Guarantee<String>.pending()
        let receivePreAuthChallengeTokenCount = AtomicUInt(lock: .init())
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            receivePreAuthChallengeTokenCount.increment()
            return await challengeTokenPromise.awaitable()
        })

        // Give back a session with a push challenge.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge],
        )))

        timeoutProviderMock.pushTokenMinWaitTime = 0.5
        timeoutProviderMock.pushTokenTimeout = 2

        // Give it a phone number, which should cause it to start a session.
        let nextStep = await coordinator.submitE164(Stubs.e164).awaitable()
        #expect(nextStep == .showErrorSheet(.sessionInvalidated))

        // One time to set up, one time for the min wait time, one time
        // for the full timeout.
        #expect(receivePreAuthChallengeTokenCount.get() == 3)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_pushChallengeWithoutPushNotificationsAvailable(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()
        pushRegistrationManagerMock.addRequestPushTokenMock({ .pushUnsupported(description: "") })
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Require a push challenge, which we won't be able to answer.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge],
        )))

        // Give it a phone number, which should cause it to start a session.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(
                    mode: mode,
                    previouslyEnteredE164: Stubs.e164,
                )),
        )
        #expect(sessionManager.latestChallengeFulfillment == nil)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_preferPushChallengesIfWeCanAnswerThemImmediately(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Be ready to provide the push challenge token as soon as it's needed.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({ "a pre-auth challenge token" })

        // Give back a session with multiple challenges.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.captcha, .pushChallenge],
        )))

        // Be ready to handle push challenges as soon as we can.
        sessionManager.addFulfillChallengeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
        )))

        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
        )))

        // Give it a phone number, which should cause it to start a session.
        let step = await coordinator.submitE164(Stubs.e164).awaitable()
        #expect(step == .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)))
        let fulfillment = sessionManager.latestChallengeFulfillment
        #expect(fulfillment == .pushChallenge("a pre-auth challenge token"))
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_prefersCaptchaChallengesIfWeCannotAnswerPushChallengeQuickly(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Prepare to provide the challenge token.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Give back a session with multiple challenges.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge, .captcha],
        )))

        timeoutProviderMock.pushTokenMinWaitTime = 0.5
        timeoutProviderMock.pushTokenTimeout = 2

        // Give it a phone number, which should cause it to start a session.
        let nextStep = await coordinator.submitE164(Stubs.e164).awaitable()

        // After that, we should get a captcha step back, because we haven't
        // yet received the push challenge token.
        #expect(nextStep == .captchaChallenge)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_pushChallengeFastResolution(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // Prepare to provide the challenge token.
        let (challengeTokenPromise, challengeTokenFuture) = Guarantee<String>.pending()
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({ await challengeTokenPromise.awaitable() })

        // Give back a session with multiple challenges.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge, .captcha],
        )))

        // Also prep for the token's submission.
        sessionManager.addFulfillChallengeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
        )))

        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
            allowedToRequestCode: false,
            requestedInformation: [.pushChallenge],
        )))

        timeoutProviderMock.pushTokenTimeout = 5
        Task {
            // Don't resolve the captcha token immediately, but quickly enough.
            try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
            challengeTokenFuture.resolve("challenge token")
        }

        // Give it a phone number, which should cause it to start a session.
        // Once we get that session, we should wait a short time for the
        // push challenge token and fulfill it.
        let step = await coordinator.submitE164(Stubs.e164).awaitable()
        #expect(step == .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)))
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_ignoresPushChallengesIfWeCannotEverAnswerThem(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        pushRegistrationManagerMock.addRequestPushTokenMock({ .pushUnsupported(description: "") })
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // No other setup; no auth credentials, SVR keys, etc in storage
        // so that we immediately go to the session flow.

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )

        // Give back a session with multiple challenges.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.captcha, .pushChallenge],
        )))

        // Give it a phone number, which should cause it to start a session.
        #expect(await coordinator.submitE164(Stubs.e164).awaitable() == .captchaChallenge)
        #expect(sessionManager.latestChallengeFulfillment == nil)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_unknownChallenge(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Give back a session with a captcha challenge and an unknown challenge.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            requestedInformation: [.captcha],
            hasUnknownChallengeRequiringAppUpdate: true,
        )))

        // Give back a session without the captcha but still with the unknown challenge
        sessionManager.addFulfillChallengeResponseMock(.success(stubs.session(
            allowedToRequestCode: false,
            hasUnknownChallengeRequiringAppUpdate: true,
        )))

        // Once we get that session, we should get a captcha step back.
        // We have an unknown challenge, but we should do known challenges first!
        // Give it a phone number, which should cause it to start a session.
        #expect(await coordinator.submitE164(Stubs.e164).awaitable() == .captchaChallenge)

        // This means we should get the app update banner.
        #expect(await coordinator.submitCaptcha(Stubs.captchaToken).awaitable() == .appUpdateBanner)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_wrongVerificationCode(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await createSessionAndRequestFirstCode(coordinator: coordinator, mode: mode)

        // Give back a rejected argument response, its the wrong code.
        sessionManager.addSubmitCodeResponseMock(.rejectedArgument(stubs.session(
            nextVerificationAttempt: 0,
        )))

        // Now try and send the wrong code.
        let badCode = "garbage"
        #expect(
            await coordinator.submitVerificationCode(badCode).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    validationError: .invalidVerificationCode(invalidCode: badCode),
                )),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_verificationCodeTimeouts(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await createSessionAndRequestFirstCode(coordinator: coordinator, mode: mode)

        // Give back a retry response.
        sessionManager.addSubmitCodeResponseMock(.retryAfterTimeout(
            stubs.session(nextVerificationAttempt: 10),
            retryAfterHeader: 10,
        ))

        // Resend an sms code, time that out too.
        sessionManager.addRequestCodeResponseMock(.retryAfterTimeout(
            stubs.session(nextSMS: 7, nextCall: 0, nextVerificationAttempt: 9),
            retryAfterHeader: 7,
        ))

        // Resend an voice code, time that out too
        // Make the timeout SO short that it retries
        sessionManager.didRequestCode = false
        sessionManager.addRequestCodeResponseMock(.retryAfterTimeout(
            stubs.session(nextSMS: 6, nextCall: 0.1, nextVerificationAttempt: 8),
            retryAfterHeader: 0.1,
        ))

        // Be ready for the retry. Ensure we called it the first time.
        sessionManager.addRequestCodeResponseMock(.retryAfterTimeout(
            stubs.session(nextSMS: 5, nextCall: 4, nextVerificationAttempt: 8),
            retryAfterHeader: 4,
        ))

        #expect(
            await coordinator.submitVerificationCode(Stubs.verificationCode).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextVerificationAttempt: 10,
                    validationError: .submitCodeTimeout,
                )),
        )

        #expect(
            await coordinator.requestSMSCode().awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextSMS: 7,
                    nextVerificationAttempt: 9,
                    validationError: .smsResendTimeout,
                )),
        )

        #expect(
            await coordinator.requestVoiceCode().awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextSMS: 5,
                    nextCall: 4,
                    nextVerificationAttempt: 8,
                    validationError: .voiceResendTimeout,
                )),
        )

        #expect(sessionManager.didRequestCode)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_disallowedVerificationCode(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await createSessionAndRequestFirstCode(coordinator: coordinator, mode: mode)

        // Give back a disallowed response when submitting a code.
        // Make the session unverified. Together this will be interpreted
        // as meaning no code has been sent (via sms or voice) and one
        // must be requested.
        sessionManager.addSubmitCodeResponseMock(.disallowed(stubs.session()))

        // The server says no code is available to submit. But we think we tried
        // sending a code with local state. We want to be on the verification
        // code entry screen, with an error so the user retries sending a code.
        #expect(
            await coordinator.submitVerificationCode(Stubs.verificationCode).awaitable() ==
                .showErrorSheet(.verificationCodeSubmissionUnavailable),
        )

        #expect(
            await coordinator.nextStep() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextVerificationAttempt: nil,
                )),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_timedOutVerificationCodeWithoutRetries(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await createSessionAndRequestFirstCode(coordinator: coordinator, mode: mode)

        // Give back a retry response when submitting a code,
        // but with no ability to resubmit.
        sessionManager.addSubmitCodeResponseMock(.retryAfterTimeout(stubs.session(), retryAfterHeader: nil))

        #expect(
            await coordinator.submitVerificationCode(Stubs.verificationCode).awaitable() ==
                .showErrorSheet(.verificationCodeSubmissionUnavailable),
        )

        #expect(
            await coordinator.nextStep() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(
                    mode: mode,
                    nextVerificationAttempt: nil,
                )),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_expiredSession(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode

        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Give back a session thats ready to go.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Once we get that session, we should try and send a verification code.
        // Have that ready to go.

        // Resolve with a session
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(
            nextVerificationAttempt: 0,
        )))

        // Give back an expired session.
        sessionManager.addSubmitCodeResponseMock(.invalidSession)

        // Give it a phone number, which should cause it to start a session.
        // Now we should expect to be at verification code entry since we sent the code.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )

        #expect(
            await coordinator.submitVerificationCode(Stubs.pinCode).awaitable() ==
                .showErrorSheet(.sessionInvalidated),
        )

        #expect(
            await coordinator.nextStep() ==
                .phoneNumberEntry(stubs.phoneNumberEntryState(
                    mode: mode,
                    previouslyEnteredE164: Stubs.e164,
                )),
        )
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_skipPINCode(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode
        let newMasterKey = Stubs.accountEntropyPoolToGenerate.getMasterKey()

        await createSessionAndRequestFirstCode(coordinator: coordinator, mode: mode)

        // Give back a verified session.
        sessionManager.addSubmitCodeResponseMock(.success(stubs.session(
            receivedDate: date,
            verified: true,
        )))

        let accountIdentityResponse = Stubs.accountIdentityResponse()
        var authPassword: String!

        // That means it should try and register with the verified
        // session; be ready for that.

        // Before registering, it should ask for push tokens to give the registration.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // It should also fetch the prekeys for account creation
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        let expectedRequest = createAccountWithSession(newMasterKey)
        mockURLSession.addResponse(
            TSRequestOWSURLSessionMock.Response(
                matcher: { request in
                    authPassword = request.authPassword
                    let requestAttributes = Self.attributesFromCreateAccountRequest(request)
                    // These should be empty if sessionId is sent
                    #expect((request.parameters["recoveryPassword"] as? String) == nil)
                    #expect(requestAttributes.registrationRecoveryPassword == nil)
                    return request.url == expectedRequest.url
                },
                statusCode: 200,
                bodyJson: accountIdentityResponse,
            ),
        )

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: accountIdentityResponse.aci,
                pni: accountIdentityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // Once we are registered, we should finalize prekeys.
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        // Then we should try and create one time pre-keys
        // with the credentials we got in the identity response.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        // When we skip the pin, it should skip any SVR backups.
        svr.backupMasterKeyMock = { _, masterKey, _ in
            Issue.record("Shouldn't talk to SVR with skipped PIN!")
            return .value(masterKey)
        }

        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ _, _ in
            return .value(())
        })

        // Once we skip the storage service restore,
        // we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(newMasterKey),
            auth: .implicit(), // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // Once we sync push tokens, we should restore from storage service.
        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(newMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRotateManifestMock({ _, _ in
            // TODO: Really should make this explicit credentials
            return .value(())
        })

        // Now we should ask to create a PIN.
        // No exit allowed since we've already started trying to create the account.
        #expect(
            await coordinator.submitVerificationCode(Stubs.pinCode).awaitable() ==
                .pinEntry(
                    Stubs.pinEntryStateForPostRegCreate(mode: mode, exitConfigOverride: .noExitAllowed),
                ),
        )

        // At this point we should not have set the AEP.
        #expect(db.read { accountKeyStore.getAccountEntropyPool(tx: $0) == nil })

        // Skip the PIN code.
        #expect(await coordinator.skipPINCode().awaitable() == .done)

        // We should now have set the AEP.
        #expect(db.read { accountKeyStore.getAccountEntropyPool(tx: $0) != nil })

        // Since we set profile info, we should have scheduled a reupload.
        #expect(profileManagerMock.didScheduleReuploadLocalProfile)
    }

    @MainActor @Test(arguments: Self.testCases())
    func testSessionPath_skipPINRestore_createNewPIN(testCase: TestCase) async {
        let coordinator = setupTest(testCase)
        let mode = testCase.mode
        let newMasterKey = Stubs.accountEntropyPoolToGenerate.getMasterKey()

        switch mode {
        case .registering:
            break
        case .reRegistering, .changingNumber:
            // Test only applies to registering scenarios.
            return
        }

        await createSessionAndRequestFirstCode(coordinator: coordinator, mode: mode)

        // Give back a verified session.
        sessionManager.addSubmitCodeResponseMock(.success(stubs.session(
            receivedDate: date,
            verified: true,
        )))

        // Previously used SVR so we first ask to restore.
        let accountIdentityResponse = Stubs.accountIdentityResponse(hasPreviouslyUsedSVR: true)
        var authPassword: String!

        // Try and register with the verified session
        // Before registering, it should ask for push tokens to give the registration.
        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        // It should also fetch the prekeys for account creation
        preKeyManagerMock.addCreatePreKeysMock({ Task { Stubs.prekeyBundles() } })

        let expectedRequest = createAccountWithSession(newMasterKey)
        mockURLSession.addResponse(
            TSRequestOWSURLSessionMock.Response(
                matcher: { request in
                    authPassword = request.authPassword
                    return request.url == expectedRequest.url
                },
                statusCode: 200,
                bodyJson: accountIdentityResponse,
            ),
        )

        func expectedAuthedAccount() -> AuthedAccount {
            return .explicit(
                aci: accountIdentityResponse.aci,
                pni: accountIdentityResponse.pni,
                e164: Stubs.e164,
                deviceId: .primary,
                authPassword: authPassword,
            )
        }

        // Once we are registered, we should finalize prekeys.
        preKeyManagerMock.addFinalizePreKeyMock { didSucceed in
            #expect(didSucceed)
            return Task {}
        }

        // Then we should try and create one time pre-keys
        // with the credentials we got in the identity response.
        preKeyManagerMock.addRotateOneTimePreKeyMock({ auth in
            #expect(auth == expectedAuthedAccount().chatServiceAuth)
            return Task {}
        })

        // When we skip the pin, it should skip any SVR backups.
        svr.backupMasterKeyMock = { _, masterKey, _ in
            Issue.record("Shouldn't talk to SVR with skipped PIN!")
            return .value(masterKey)

        }

        storageServiceManagerMock.addRestoreOrCreateManifestIfNecessaryMock({ auth, masterKeySource in
            #expect(auth.authedAccount == expectedAuthedAccount())
            switch masterKeySource {
            case .explicit(let explicitMasterKey):
                #expect(newMasterKey.rawData == explicitMasterKey.rawData)
            default:
                Issue.record("Unexpected master key used in storage service operation.")
            }
            return .value(())
        })

        storageServiceManagerMock.addRotateManifestMock({ _, _ in return .value(()) })

        // Once we skip the storage service restore,
        // we will sync account attributes and then we are finished!
        let expectedAttributesRequest = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
            Stubs.accountAttributes(newMasterKey),
            auth: .implicit(), // doesn't matter for url matching
            logger: .empty(),
        )
        networkManagerMock.asyncRequestHandlers.append({ request, _ in
            if request.url == expectedAttributesRequest.url {
                return HTTPResponse(requestUrl: request.url, status: 200, headers: HttpHeaders(), bodyData: nil)
            }
            throw OWSAssertionError("")
        })

        // Now we should ask to restore the PIN.
        #expect(
            await coordinator.submitVerificationCode(Stubs.pinCode).awaitable() ==
                .pinEntry(
                    Stubs.pinEntryStateForPostRegRestore(mode: mode),
                ),
        )

        // Skip the PIN code and create a new one instead.
        // When we skip, we should be asked to _create_ the PIN.
        #expect(
            await coordinator.skipAndCreateNewPINCode().awaitable() ==
                .pinEntry(
                    Stubs.pinEntryStateForPostRegCreate(mode: mode, exitConfigOverride: .noExitAllowed),
                ),
        )

        // At this point we should not have set the AEP.
        #expect(db.read { accountKeyStore.getAccountEntropyPool(tx: $0) == nil })

        // Skip this PIN code, too.
        #expect(await coordinator.skipPINCode().awaitable() == .done)

        // We should now have set the master key (i.e., the AEP).
        #expect(db.read { accountKeyStore.getAccountEntropyPool(tx: $0) != nil })

        // Since we set profile info, we should have scheduled a reupload.
        #expect(profileManagerMock.didScheduleReuploadLocalProfile)
    }

    // MARK: - Profile Setup Path

    // TODO[Registration]: test the profile setup steps.

    // MARK: - Persisted State backwards compatibility

    typealias ReglockState = RegistrationCoordinatorImpl.PersistedState.SessionState.ReglockState

    @MainActor @Test
    func testPersistedState_SVRCredentialCompat() throws {
        let reglockExpirationDate = Date(timeIntervalSince1970: 10000)
        let decoder = JSONDecoder()

        // Serialized ReglockState.none
        let reglockStateNoneData = "7b226e6f6e65223a7b7d7d"
        #expect(
            try decoder.decode(ReglockState.self, from: Data.data(fromHex: reglockStateNoneData)!) ==
                ReglockState.none,
        )

        // Serialized ReglockState.reglocked(
        //     credential: KBSAuthCredential(credential: RemoteAttestation.Auth(username: "abcd", password: "xyz"),
        //     expirationDate: reglockExpirationDate
        // )
        let reglockStateReglockedData = "7b227265676c6f636b6564223a7b2265787069726174696f6e44617465223a2d3937383239373230302c2263726564656e7469616c223a7b2263726564656e7469616c223a7b22757365726e616d65223a2261626364222c2270617373776f7264223a2278797a227d7d7d7d"
        #expect(
            try decoder.decode(ReglockState.self, from: Data.data(fromHex: reglockStateReglockedData)!) ==
                ReglockState.reglocked(credential: .testOnly(svr2: nil), expirationDate: reglockExpirationDate),
        )

        // Serialized ReglockState.reglocked(
        //     credential: ReglockState.SVRAuthCredential(
        //         kbs: KBSAuthCredential(credential: RemoteAttestation.Auth(username: "abcd", password: "xyz"),
        //         svr2: SVR2AuthCredential(credential: RemoteAttestation.Auth(username: "xxx", password: "yyy"))
        //     ),
        //     expirationDate: reglockExpirationDate
        // )
        let reglockStateReglockedSVR2Data = "7b227265676c6f636b6564223a7b2265787069726174696f6e44617465223a2d3937383239373230302c2263726564656e7469616c223a7b226b6273223a7b2263726564656e7469616c223a7b22757365726e616d65223a2261626364222c2270617373776f7264223a2278797a227d7d2c2273767232223a7b2263726564656e7469616c223a7b22757365726e616d65223a22787878222c2270617373776f7264223a22797979227d7d7d7d7d"
        #expect(
            try decoder.decode(ReglockState.self, from: Data.data(fromHex: reglockStateReglockedSVR2Data)!) ==
                ReglockState.reglocked(credential: .init(svr2: Stubs.svr2AuthCredential), expirationDate: reglockExpirationDate),
        )

        // Serialized ReglockState.waitingTimeout(expirationDate: reglockExpirationDate)
        let reglockStateWaitingTimeoutData = "7b2277616974696e6754696d656f7574223a7b2265787069726174696f6e44617465223a2d3937383239373230307d7d"
        #expect(
            try decoder.decode(ReglockState.self, from: Data.data(fromHex: reglockStateWaitingTimeoutData)!) ==
                ReglockState.waitingTimeout(expirationDate: reglockExpirationDate),
        )
    }

    // MARK: Happy Path Setups

    private func createAccountWithSession(
        _ masterKey: MasterKey,
    ) -> TSRequest {
        return RegistrationRequestFactory.createAccountRequest(
            verificationMethod: .sessionId(Stubs.sessionId),
            e164: Stubs.e164,
            authPassword: "", // Doesn't matter for request generation.
            accountAttributes: Stubs.accountAttributes(masterKey),
            skipDeviceTransfer: true,
            apnRegistrationId: Stubs.apnsRegistrationId,
            prekeyBundles: Stubs.prekeyBundles(),
            logger: .empty(),
        )
    }

    private func createAccountWithRecoveryPw(
        _ masterKey: MasterKey,
    ) -> TSRequest {
        return RegistrationRequestFactory.createAccountRequest(
            verificationMethod: .recoveryPassword(masterKey.regRecoveryPw),
            e164: Stubs.e164,
            authPassword: "", // Doesn't matter for request generation.
            accountAttributes: Stubs.accountAttributes(masterKey),
            skipDeviceTransfer: true,
            apnRegistrationId: Stubs.apnsRegistrationId,
            prekeyBundles: Stubs.prekeyBundles(),
            logger: .empty(),
        )
    }

    @MainActor
    private func goThroughOpeningHappyPath(
        coordinator: any RegistrationCoordinator,
        mode: RegistrationMode,
        expectedNextStep: RegistrationStep,
    ) async {
        contactsStore.doesNeedContactsAuthorization = true
        pushRegistrationManagerMock.doesNeedNotificationAuthorization = true

        switch mode {
        case .registering:
            // Gotta get the splash out of the way.
            #expect(await coordinator.nextStep() == .registrationSplash)
        case .reRegistering, .changingNumber:
            break
        }

        // Now we should show the permissions.
        #expect(await coordinator.continueFromSplash().awaitable() == .permissions)

        // Once the state is updated we can proceed.
        #expect(await coordinator.requestPermissions().awaitable() == expectedNextStep)
    }

    @MainActor
    private func setUpSessionPath(coordinator: any RegistrationCoordinator, mode: RegistrationMode) async {
        // Set profile info so we skip those steps.
        setupDefaultAccountAttributes()

        pushRegistrationManagerMock.addRequestPushTokenMock({ .success(Stubs.apnsRegistrationId) })

        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // No other setup; no auth credentials, SVR keys, etc in storage
        // so that we immediately go to the session flow.

        // Get past the opening.
        await goThroughOpeningHappyPath(
            coordinator: coordinator,
            mode: mode,
            expectedNextStep: .phoneNumberEntry(stubs.phoneNumberEntryState(mode: mode)),
        )
    }

    @MainActor
    private func createSessionAndRequestFirstCode(coordinator: any RegistrationCoordinator, mode: RegistrationMode) async {
        await setUpSessionPath(coordinator: coordinator, mode: mode)

        // Give it a phone number, which should cause it to start a session.

        // We'll ask for a push challenge, though we won't resolve it.
        pushRegistrationManagerMock.setReceivePreAuthChallengeTokenMock({
            try! await Task.sleep(nanoseconds: TimeInterval.infinity.clampedNanoseconds)
            fatalError()
        })

        // Give back a session that's ready to go.
        sessionManager.addBeginSessionResponseMock(.success(stubs.session()))

        // Once we get that session, we should try and send a code.
        // Resolve with a session thats ready for code submission.
        sessionManager.addRequestCodeResponseMock(.success(stubs.session(nextVerificationAttempt: 0)))

        // We should get back the code entry step.
        #expect(
            await coordinator.submitE164(Stubs.e164).awaitable() ==
                .verificationCodeEntry(stubs.verificationCodeEntryState(mode: mode)),
        )
    }

    // MARK: - Helpers

    private func setupDefaultAccountAttributes() {
        ows2FAManagerMock.pinCodeMock = { nil }
        ows2FAManagerMock.isReglockEnabledMock = { false }

        tsAccountManagerMock.isManualMessageFetchEnabledMock = { false }

        setAllProfileInfo()
    }

    private func setAllProfileInfo() {
        phoneNumberDiscoverabilityManagerMock.phoneNumberDiscoverabilityMock = { .everybody }
        profileManagerMock.localUserProfileMock = { _ in
            return OWSUserProfile(
                id: nil,
                uniqueId: "00000000-0000-4000-8000-000000000000",
                serviceIdString: nil,
                phoneNumber: nil,
                avatarFileName: nil,
                avatarUrlPath: nil,
                profileKey: Aes256Key(data: Data(count: 32))!,
                givenName: "Johnny",
                familyName: "McJohnface",
                bio: nil,
                bioEmoji: nil,
                badges: [],
                lastFetchDate: Date(timeIntervalSince1970: 1735689600),
                lastMessagingDate: nil,
                isPhoneNumberShared: false,
            )
        }
    }

    private static func attributesFromCreateAccountRequest(
        _ request: TSRequest,
    ) -> AccountAttributes {
        let accountAttributesData = try! JSONSerialization.data(
            withJSONObject: request.parameters["accountAttributes"]!,
            options: .fragmentsAllowed,
        )
        return try! JSONDecoder().decode(
            AccountAttributes.self,
            from: accountAttributesData,
        )
    }

    // MARK: - Helpers

    func buildKeyDataMocks(_ testCase: TestCase) -> (MasterKey, MasterKey) {
        let oldAccountEntropyPool = AccountEntropyPool()
        let oldMasterKey = oldAccountEntropyPool.getMasterKey()
        let newMasterKey = Stubs.accountEntropyPoolToGenerate.getMasterKey()

        switch (testCase.oldKey, testCase.newKey) {
        case (.accountEntropyPool, .accountEntropyPool):
            // on re-registration, make the AEP be present
            db.write { accountKeyStore.setAccountEntropyPool(oldAccountEntropyPool, tx: $0) }
            return (oldMasterKey, oldMasterKey)
        case (.masterKey, .masterKey):
            db.write { accountKeyStore.setMasterKey(oldMasterKey, tx: $0) }
            return (oldMasterKey, oldMasterKey)
        case (.masterKey, .accountEntropyPool):
            // If this is a reregistration from an non-AEP client,
            // AEP is only available after calling getOrGenerateAEP()
            db.write { accountKeyStore.setMasterKey(oldMasterKey, tx: $0) }
            return (oldMasterKey, newMasterKey)
        case (.none, .masterKey):
            return (newMasterKey, newMasterKey)
        case (.none, .accountEntropyPool):
            return (newMasterKey, newMasterKey)
        case (.accountEntropyPool, .masterKey):
            fatalError("Migrating to masterkey from AEP not supported")
        case (_, .none):
            fatalError("Registration requires a destination key")
        }
    }

    func mockSVRCredentials(isMatch: Bool) {
        // Put some auth credentials in storage.
        let svr2CredentialCandidates: [SVR2AuthCredential] = [
            Stubs.svr2AuthCredential,
            SVR2AuthCredential(credential: RemoteAttestation.Auth(username: "aaaa", password: "abc")),
            SVR2AuthCredential(credential: RemoteAttestation.Auth(username: "zzzz", password: "xyz")),
            SVR2AuthCredential(credential: RemoteAttestation.Auth(username: "0000", password: "123")),
        ]
        svrAuthCredentialStore.svr2Dict = Dictionary(grouping: svr2CredentialCandidates, by: \.credential.username).mapValues { $0.first! }

        // Give it a phone number, which should cause it to check the auth credentials.
        // Match the main auth credential.
        let expectedSVR2CheckRequest = RegistrationRequestFactory.svr2AuthCredentialCheckRequest(
            e164: Stubs.e164,
            credentials: svr2CredentialCandidates,
            logger: .empty(),
        )
        mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
            urlSuffix: expectedSVR2CheckRequest.url.absoluteString,
            statusCode: 200,
            bodyJson: RegistrationServiceResponses.SVR2AuthCheckResponse(matches: [
                "\(Stubs.svr2AuthCredential.credential.username):\(Stubs.svr2AuthCredential.credential.password)": isMatch ? .match : .notMatch,
                "aaaa:abc": .notMatch,
                "zzzz:xyz": .invalid,
                "0000:123": .unknown,
            ]),
        ))
    }

    // MARK: - Stubs

    private struct Stubs {

        static let accountEntropyPoolToGenerate = AccountEntropyPool()
        static let e164 = E164("+17875550100")!
        static let aci = Aci.randomForTesting()
        static let pinCode = "1234"

        static let svr2AuthCredential = SVR2AuthCredential(credential: RemoteAttestation.Auth(username: "xxx", password: "yyy"))

        static let captchaToken = "captchaToken"
        static let apnsToken = "apnsToken"
        static let apnsRegistrationId = RegistrationRequestFactory.ApnRegistrationId(apnsToken: Stubs.apnsToken)

        static let authUsername = "username_jdhfsalkjfhd"
        static let authPassword = "password_dskafjasldkfjasf"

        static let sessionId = UUID().uuidString
        static let verificationCode = "8888"

        var date: Date = Date()

        static func accountAttributes(_ masterKey: MasterKey? = nil) -> AccountAttributes {
            return AccountAttributes(
                isManualMessageFetchEnabled: false,
                registrationId: 0,
                pniRegistrationId: 0,
                unidentifiedAccessKey: "",
                unrestrictedUnidentifiedAccess: false,
                reglockToken: nil,
                registrationRecoveryPassword: masterKey?.regRecoveryPw,
                encryptedDeviceName: nil,
                discoverableByPhoneNumber: .nobody,
                capabilities: AccountAttributes.Capabilities(hasSVRBackups: true),
            )
        }

        static func accountIdentityResponse(
            hasPreviouslyUsedSVR: Bool = false,
        ) -> RegistrationServiceResponses.AccountIdentityResponse {
            return RegistrationServiceResponses.AccountIdentityResponse(
                aci: Stubs.aci,
                pni: Pni.randomForTesting(),
                e164: Stubs.e164,
                username: nil,
                hasPreviouslyUsedSVR: hasPreviouslyUsedSVR,
            )
        }

        static func prekeyBundles() -> RegistrationPreKeyUploadBundles {
            return RegistrationPreKeyUploadBundles(
                aci: preKeyBundle(identity: .aci),
                pni: preKeyBundle(identity: .pni),
            )
        }

        static func preKeyBundle(identity: OWSIdentity) -> RegistrationPreKeyUploadBundle {
            let identityKeyPair = ECKeyPair.generateKeyPair()
            return RegistrationPreKeyUploadBundle(
                identity: identity,
                identityKeyPair: identityKeyPair,
                signedPreKey: SignedPreKeyStoreImpl.generateSignedPreKey(keyId: PreKeyId.random(), signedBy: identityKeyPair.keyPair.privateKey),
                lastResortPreKey: KyberPreKeyStoreImpl.generatePreKeyRecord(keyId: 0, now: Date(), signedBy: identityKeyPair.keyPair.privateKey),
            )
        }

        func session(
            e164: E164 = Stubs.e164,
            receivedDate: Date? = nil,
            nextSMS: TimeInterval? = 0,
            nextCall: TimeInterval? = 0,
            nextVerificationAttempt: TimeInterval? = nil,
            allowedToRequestCode: Bool = true,
            requestedInformation: [RegistrationSession.Challenge] = [],
            hasUnknownChallengeRequiringAppUpdate: Bool = false,
            verified: Bool = false,
        ) -> RegistrationSession {
            let receivedDate = receivedDate ?? date
            return RegistrationSession(
                id: Stubs.sessionId,
                e164: e164,
                receivedDate: receivedDate,
                nextSMS: nextSMS,
                nextCall: nextCall,
                nextVerificationAttempt: nextVerificationAttempt,
                allowedToRequestCode: allowedToRequestCode,
                requestedInformation: requestedInformation,
                hasUnknownChallengeRequiringAppUpdate: hasUnknownChallengeRequiringAppUpdate,
                verified: verified,
            )
        }

        // MARK: Step States

        static func pinEntryStateForRegRecoveryPath(
            mode: RegistrationMode,
            error: RegistrationPinValidationError? = nil,
            remainingAttempts: UInt? = nil,
        ) -> RegistrationPinState {
            return RegistrationPinState(
                operation: .enteringExistingPin(
                    skippability: .canSkip,
                    remainingAttempts: remainingAttempts,
                ),
                error: error,
                contactSupportMode: .v2WithUnknownReglockState,
                exitConfiguration: mode.pinExitConfig,
            )
        }

        static func pinEntryStateForSVRAuthCredentialPath(
            mode: RegistrationMode,
            error: RegistrationPinValidationError? = nil,
        ) -> RegistrationPinState {
            return RegistrationPinState(
                operation: .enteringExistingPin(skippability: .canSkip, remainingAttempts: nil),
                error: error,
                contactSupportMode: .v2WithUnknownReglockState,
                exitConfiguration: mode.pinExitConfig,
            )
        }

        func phoneNumberEntryState(
            mode: RegistrationMode,
            previouslyEnteredE164: E164? = nil,
            withValidationErrorFor response: Registration.BeginSessionResponse? = nil,
        ) -> RegistrationPhoneNumberViewState {
            let response = response ?? .success(session())
            let validationError: RegistrationPhoneNumberViewState.ValidationError?
            switch response {
            case .success:
                validationError = nil
            case .invalidArgument:
                validationError = .invalidE164(.init(invalidE164: previouslyEnteredE164 ?? Stubs.e164))
            case .retryAfter(let timeInterval):
                validationError = .rateLimited(.init(
                    expiration: date.addingTimeInterval(timeInterval!),
                    e164: previouslyEnteredE164 ?? Stubs.e164,
                ))
            case .networkFailure, .genericError:
                Issue.record("Should not be generating phone number state for error responses.")
                validationError = nil
            }

            switch mode {
            case .registering:
                return .registration(.initialRegistration(.init(
                    previouslyEnteredE164: previouslyEnteredE164,
                    validationError: validationError,
                    canExitRegistration: true,
                )))
            case .reRegistering(let params):
                return .registration(.reregistration(.init(
                    e164: params.e164,
                    validationError: validationError,
                    canExitRegistration: true,
                )))
            case .changingNumber(let changeNumberParams):
                switch validationError {
                case .none:
                    if let newE164 = previouslyEnteredE164 {
                        return .changingNumber(.confirmation(.init(
                            oldE164: changeNumberParams.oldE164,
                            newE164: newE164,
                            rateLimitedError: nil,
                        )))
                    } else {
                        return .changingNumber(.initialEntry(.init(
                            oldE164: changeNumberParams.oldE164,
                            newE164: nil,
                            hasConfirmed: false,
                            invalidE164Error: nil,
                        )))
                    }
                case .rateLimited(let error):
                    return .changingNumber(.confirmation(.init(
                        oldE164: changeNumberParams.oldE164,
                        newE164: previouslyEnteredE164!,
                        rateLimitedError: error,
                    )))
                case .invalidInput:
                    owsFail("Can't happen.")
                case .invalidE164(let error):
                    return .changingNumber(.initialEntry(.init(
                        oldE164: changeNumberParams.oldE164,
                        newE164: previouslyEnteredE164,
                        hasConfirmed: previouslyEnteredE164 != nil,
                        invalidE164Error: error,
                    )))
                }
            }
        }

        func verificationCodeEntryState(
            mode: RegistrationMode,
            e164: E164 = Stubs.e164,
            nextSMS: TimeInterval? = 0,
            nextCall: TimeInterval? = 0,
            showHelpText: Bool = false,
            nextVerificationAttempt: TimeInterval? = 0,
            validationError: RegistrationVerificationValidationError? = nil,
            exitConfigOverride: RegistrationVerificationState.ExitConfiguration? = nil,
        ) -> RegistrationVerificationState {

            let canChangeE164: Bool
            switch mode {
            case .reRegistering:
                canChangeE164 = false
            case .registering, .changingNumber:
                canChangeE164 = true
            }

            return RegistrationVerificationState(
                e164: e164,
                nextSMSDate: nextSMS.map { date.addingTimeInterval($0) },
                nextCallDate: nextCall.map { date.addingTimeInterval($0) },
                nextVerificationAttemptDate: nextVerificationAttempt.map { date.addingTimeInterval($0) },
                canChangeE164: canChangeE164,
                showHelpText: showHelpText,
                validationError: validationError,
                exitConfiguration: exitConfigOverride ?? mode.verificationExitConfig,
            )
        }

        static func pinEntryStateForSessionPathReglock(
            mode: RegistrationMode,
            error: RegistrationPinValidationError? = nil,
        ) -> RegistrationPinState {
            return RegistrationPinState(
                operation: .enteringExistingPin(skippability: .unskippable, remainingAttempts: nil),
                error: error,
                contactSupportMode: .v2WithReglock,
                exitConfiguration: mode.pinExitConfig,
            )
        }

        static func pinEntryStateForPostRegRestore(
            mode: RegistrationMode,
            exitConfigOverride: RegistrationPinState.ExitConfiguration? = nil,
            error: RegistrationPinValidationError? = nil,
        ) -> RegistrationPinState {
            return RegistrationPinState(
                operation: .enteringExistingPin(
                    skippability: .canSkipAndCreateNew,
                    remainingAttempts: nil,
                ),
                error: error,
                contactSupportMode: .v2NoReglock,
                exitConfiguration: exitConfigOverride ?? mode.pinExitConfig,
            )
        }

        static func pinEntryStateForPostRegCreate(
            mode: RegistrationMode,
            exitConfigOverride: RegistrationPinState.ExitConfiguration? = nil,
        ) -> RegistrationPinState {
            return RegistrationPinState(
                operation: .creatingNewPin,
                error: nil,
                contactSupportMode: .v2NoReglock,
                exitConfiguration: exitConfigOverride ?? mode.pinExitConfig,
            )
        }

        static func pinEntryStateForPostRegConfirm(
            mode: RegistrationMode,
            error: RegistrationPinValidationError? = nil,
            exitConfigOverride: RegistrationPinState.ExitConfiguration? = nil,
        ) -> RegistrationPinState {
            return RegistrationPinState(
                operation: .confirmingNewPin(.stub()),
                error: error,
                contactSupportMode: .v2NoReglock,
                exitConfiguration: exitConfigOverride ?? mode.pinExitConfig,
            )
        }
    }
}

extension RegistrationMode {

    var testDescription: String {
        switch self {
        case .registering:
            return "registering"
        case .reRegistering:
            return "re-registering"
        case .changingNumber:
            return "changing number"
        }
    }

    var pinExitConfig: RegistrationPinState.ExitConfiguration {
        switch self {
        case .registering:
            return .noExitAllowed
        case .reRegistering:
            return .exitReRegistration
        case .changingNumber:
            // TODO[Registration]: test change number properly
            return .exitChangeNumber
        }
    }

    var verificationExitConfig: RegistrationVerificationState.ExitConfiguration {
        switch self {
        case .registering:
            return .noExitAllowed
        case .reRegistering:
            return .exitReRegistration
        case .changingNumber:
            // TODO[Registration]: test change number properly
            return .exitChangeNumber
        }
    }
}

private extension MasterKey {
    var regRecoveryPw: String { data(for: .registrationRecoveryPassword).rawData.base64EncodedString() }
    var reglockToken: String { data(for: .registrationLock).rawData.hexadecimalString }
}

struct EncodableRegistrationLockFailureResponse: Codable {
    typealias ResponseType = RegistrationServiceResponses.RegistrationLockFailureResponse
    typealias CodingKeys = ResponseType.CodingKeys

    var response: ResponseType

    init(from decoder: any Decoder) throws {
        response = try ResponseType(from: decoder)
    }

    init(timeRemainingMs: Int, svr2AuthCredential: SVR2AuthCredential) {
        response = ResponseType(timeRemainingMs: timeRemainingMs, svr2AuthCredential: svr2AuthCredential)
    }

    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(response.timeRemainingMs, forKey: .timeRemainingMs)
        try container.encodeIfPresent(response.svr2AuthCredential.credential, forKey: .svr2AuthCredential)
    }
}

private extension Usernames.UsernameLink {
    static var mocked: Usernames.UsernameLink {
        return Usernames.UsernameLink(
            handle: UUID(),
            entropy: Data(repeating: 8, count: 32),
        )!
    }
}

private extension TSRequest {
    var authPassword: String {
        var httpHeaders = HttpHeaders()
        try! applyAuth(to: &httpHeaders, socketAuth: nil)
        let authHeader = httpHeaders.value(forHeader: "Authorization")!
        owsPrecondition(authHeader.hasPrefix("Basic "))
        let authValue = String(data: Data(base64Encoded: String(authHeader.dropFirst(6)))!, encoding: .utf8)!
        return String(authValue.split(separator: ":").dropFirst().first!)
    }
}