//
        // Internal (accessed by the tests)
        //

        internal static (byte[] EncryptedToken, string Passcode) GetEncryptedTokenAndPasscode(string username,
                                                                                              string deviceId,
                                                                                              IUi ui,
                                                                                              RestClient rest)
        {
            byte[] GetToken(string code) => GetEncryptedToken(username, deviceId, code, DateTime.Now, rest);

            // First request without any passcode. This should succeed for known devices.
            var encryptedToken = GetToken(NoPasscode);

            if (encryptedToken != EmailPasscodeRequired)
            {
                return(encryptedToken, NoPasscode);
            }

            // It's a new device, the email PIN is required
            for (;;)
            {
                var passcode = ui.ProvideEmailPasscode();
                if (passcode == Passcode.Cancel)
                {
                    throw new CanceledMultiFactorException("Second factor step is canceled by the user");
                }

                if (passcode == Passcode.Resend)
                {
                    GetToken(NoPasscode);
                    continue;
                }

                // Now try with the PIN from the email
                encryptedToken = GetToken(passcode.Code);
                if (encryptedToken == EmailPasscodeRequired)
                {
                    throw new InternalErrorException("Unexpected response");
                }

                return(encryptedToken, passcode.Code);
            }
        }
        internal static string Login(string username,
                                     byte[] passwordHash,
                                     string deviceId,
                                     IUi ui,
                                     ISecureStorage storage,
                                     RestClient rest)
        {
            // Try simple password login, potentially with a stored second factor token if
            // "remember me" was used before.
            var rememberMeOptions = GetRememberMeOptions(storage);
            var response          = RequestAuthToken(username, passwordHash, deviceId, rememberMeOptions, rest);

            // Simple password login (no 2FA) succeeded
            if (response.AuthToken != null)
            {
                return(response.AuthToken);
            }

            var secondFactor = response.SecondFactor;

            if (secondFactor.Methods == null || secondFactor.Methods.Count == 0)
            {
                throw new InternalErrorException("Expected a non empty list of available 2FA methods");
            }

            // We had a "remember me" token saved, but the login failed anyway. This token is not valid anymore.
            if (rememberMeOptions != null)
            {
                EraseRememberMeToken(storage);
            }

            var      method   = ChooseSecondFactorMethod(secondFactor, ui);
            var      extra    = secondFactor.Methods[method];
            Passcode passcode = null;

            switch (method)
            {
            case Response.SecondFactorMethod.GoogleAuth:
                passcode = ui.ProvideGoogleAuthPasscode();
                break;

            case Response.SecondFactorMethod.Email:
                // When only the email 2FA present, the email is sent by the server right away.
                // Trigger only when other methods are present.
                if (secondFactor.Methods.Count != 1)
                {
                    TriggerEmailMfaPasscode(username, passwordHash, rest);
                }

                passcode = ui.ProvideEmailPasscode((string)extra["Email"] ?? "");
                break;

            case Response.SecondFactorMethod.Duo:
            case Response.SecondFactorMethod.DuoOrg:
            {
                var duo = Duo.Authenticate((string)extra["Host"] ?? "",
                                           (string)extra["Signature"] ?? "",
                                           ui,
                                           rest.Transport);

                if (duo != null)
                {
                    passcode = new Passcode(duo.Passcode, duo.RememberMe);
                }

                break;
            }

            case Response.SecondFactorMethod.YubiKey:
                passcode = ui.ProvideYubiKeyPasscode();
                break;

            case Response.SecondFactorMethod.U2f:
                passcode = AskU2fPasscode(JObject.Parse((string)extra["Challenge"]), ui);
                break;

            default:
                throw new UnsupportedFeatureException($"2FA method {method} is not supported");
            }

            // We're done interacting with the UI
            ui.Close();

            if (passcode == null)
            {
                throw MakeCancelledMfaError();
            }

            var secondFactorResponse = RequestAuthToken(username,
                                                        passwordHash,
                                                        deviceId,
                                                        new SecondFactorOptions(method,
                                                                                passcode.Code,
                                                                                passcode.RememberMe),
                                                        rest);

            // Password + 2FA is successful
            if (secondFactorResponse.AuthToken != null)
            {
                SaveRememberMeToken(secondFactorResponse, storage);
                return(secondFactorResponse.AuthToken);
            }

            throw new BadMultiFactorException("Second factor code is not correct");
        }