/// <summary>
        /// Get a scope description string.
        ///
        /// Group the categories together so as to generate the most minimal string. Some of this is done via
        /// the flags enum minimization step, but there are some disjoint roots that require an additional step.
        /// <param name="scopes">Scopes</param>
        /// <returns>Description string.</returns>
        /// <exception cref="Exception">Exception if an attribute is missing from the enum.</exception>
        public static string GetScopeString(this AzureDevOpsPATScopes scopes)
        {
            var minimalScopes = scopes.GetMinimizedScopeList();

            var azdoScopeType = typeof(AzureDevOpsPATScopes);
            var shortHandType = typeof(ScopeDescriptionAttribute);

            Dictionary <string, string> minimalScopesStrings = new Dictionary <string, string>();

            // Build up a dictionary of strings with common category roots.
            foreach (var scope in minimalScopes)
            {
                var memberInfo = azdoScopeType.GetMember(scope.ToString());
                var attribute  = Attribute.GetCustomAttribute(memberInfo[0], shortHandType);
                if (attribute == null)
                {
                    throw new Exception($"{scope.ToString()} should have a 'ScopeShortHand' attribute.");
                }

                var shortHandTypeAttribute = (ScopeDescriptionAttribute)attribute;

                if (minimalScopesStrings.ContainsKey(shortHandTypeAttribute.Resource))
                {
                    minimalScopesStrings[shortHandTypeAttribute.Resource] += shortHandTypeAttribute.Permissions;
                }
                else
                {
                    minimalScopesStrings.Add(shortHandTypeAttribute.Resource, shortHandTypeAttribute.Permissions);
                }
            }

            // Join their values together.
            return(string.Join('-', minimalScopesStrings.Select(kv => $"{kv.Key}-{kv.Value}")));
        }
        /// <summary>
        ///     Generate an azure devops PAT with a given name, target organization set, and scopes.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="targetScopes"></param>
        /// <param name="targetOrganizationNames"></param>
        /// <param name="validTo"></param>
        /// <returns>New PAT</returns>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="Exception"></exception>
        public async Task <SessionToken> GeneratePATAsync(
            string name,
            AzureDevOpsPATScopes targetScopes,
            IEnumerable <string> targetOrganizationNames,
            DateTime validTo)
        {
            ValidateParameters(name, targetScopes, targetOrganizationNames, validTo);

            var    minimalScopesList  = targetScopes.GetMinimizedScopeList();
            var    scopesWithPrefixes = minimalScopesList.Select(scope => $"vso.{scope}");
            string scopes             = string.Join(" ", scopesWithPrefixes);

            try
            {
                using var tokenConnection = new VssConnection(
                          new Uri("https://vssps.dev.azure.com/"),
                          credentials);
                using var tokenClient = await tokenConnection.GetClientAsync <TokenHttpClient>();

                var organizations = await GetAccountsByNameAsync(targetOrganizationNames);

                var tokenInfo = new SessionToken()
                {
                    DisplayName    = name,
                    Scope          = scopes,
                    TargetAccounts = organizations.Select(account => account.AccountId).ToArray(),
                    ValidFrom      = DateTime.UtcNow,
                    ValidTo        = validTo,
                };

                SessionToken pat = await tokenClient.CreateSessionTokenAsync(
                    tokenInfo,
                    SessionTokenType.Compact,
                    isPublic : false);

                return(pat);
            }
            catch (Exception ex)
            {
                throw new Exception($"Failed to generate a PAT named '{name}' for organizatons '{string.Join(", ", targetOrganizationNames)}' and scopes '{scopes}'", ex);
            }
        }