pub mod attached_clients;
use crate::{
    error::*,
    http_client::{AuthorizationRequestParameters, OAuthTokenResponse},
    scoped_keys::{ScopedKey, ScopedKeysFlow},
    util, FirefoxAccount,
};
use jwcrypto::{EncryptionAlgorithm, EncryptionParameters};
use rc_crypto::digest;
use serde_derive::*;
use std::convert::TryFrom;
use std::{
    collections::{HashMap, HashSet},
    iter::FromIterator,
    time::{SystemTime, UNIX_EPOCH},
};
use url::Url;
const OAUTH_MIN_TIME_LEFT: u64 = 60;
pub const OAUTH_WEBCHANNEL_REDIRECT: &str = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel";
impl FirefoxAccount {
    
    
    
    
    
    
    
    
    
    pub fn get_access_token(&mut self, scope: &str, ttl: Option<u64>) -> Result<AccessTokenInfo> {
        if scope.contains(' ') {
            return Err(ErrorKind::MultipleScopesRequested.into());
        }
        if let Some(oauth_info) = self.state.access_token_cache.get(scope) {
            if oauth_info.expires_at > util::now_secs() + OAUTH_MIN_TIME_LEFT {
                return Ok(oauth_info.clone());
            }
        }
        let resp = match self.state.refresh_token {
            Some(ref refresh_token) => {
                if refresh_token.scopes.contains(scope) {
                    self.client.access_token_with_refresh_token(
                        &self.state.config,
                        &refresh_token.token,
                        ttl,
                        &[scope],
                    )?
                } else {
                    return Err(ErrorKind::NoCachedToken(scope.to_string()).into());
                }
            }
            None => match self.state.session_token {
                Some(ref session_token) => self.client.access_token_with_session_token(
                    &self.state.config,
                    &session_token,
                    &[scope],
                )?,
                None => return Err(ErrorKind::NoCachedToken(scope.to_string()).into()),
            },
        };
        let since_epoch = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|_| ErrorKind::IllegalState("Current date before Unix Epoch."))?;
        let expires_at = since_epoch.as_secs() + resp.expires_in;
        let token_info = AccessTokenInfo {
            scope: resp.scope,
            token: resp.access_token,
            key: self.state.scoped_keys.get(scope).cloned(),
            expires_at,
        };
        self.state
            .access_token_cache
            .insert(scope.to_string(), token_info.clone());
        Ok(token_info)
    }
    
    pub fn get_session_token(&self) -> Result<String> {
        match self.state.session_token {
            Some(ref session_token) => Ok(session_token.to_string()),
            None => Err(ErrorKind::NoSessionToken.into()),
        }
    }
    
    pub fn check_authorization_status(&mut self) -> Result<IntrospectInfo> {
        let resp = match self.state.refresh_token {
            Some(ref refresh_token) => {
                self.auth_circuit_breaker.check()?;
                self.client
                    .oauth_introspect_refresh_token(&self.state.config, &refresh_token.token)?
            }
            None => return Err(ErrorKind::NoRefreshToken.into()),
        };
        Ok(IntrospectInfo {
            active: resp.active,
        })
    }
    
    
    
    
    
    
    
    pub fn begin_pairing_flow(
        &mut self,
        pairing_url: &str,
        scopes: &[&str],
        entrypoint: &str,
        metrics: Option<MetricsParams>,
    ) -> Result<String> {
        let mut url = self.state.config.pair_supp_url()?;
        url.query_pairs_mut().append_pair("entrypoint", entrypoint);
        if let Some(metrics) = metrics {
            metrics.append_params_to_url(&mut url);
        }
        let pairing_url = Url::parse(pairing_url)?;
        if url.host_str() != pairing_url.host_str() {
            return Err(ErrorKind::OriginMismatch.into());
        }
        url.set_fragment(pairing_url.fragment());
        self.oauth_flow(url, scopes)
    }
    
    
    
    
    
    pub fn begin_oauth_flow(
        &mut self,
        scopes: &[&str],
        entrypoint: &str,
        metrics: Option<MetricsParams>,
    ) -> Result<String> {
        let mut url = if self.state.last_seen_profile.is_some() {
            self.state.config.oauth_force_auth_url()?
        } else {
            self.state.config.authorization_endpoint()?
        };
        url.query_pairs_mut()
            .append_pair("action", "email")
            .append_pair("response_type", "code")
            .append_pair("entrypoint", entrypoint);
        if let Some(metrics) = metrics {
            metrics.append_params_to_url(&mut url);
        }
        if let Some(ref cached_profile) = self.state.last_seen_profile {
            url.query_pairs_mut()
                .append_pair("email", &cached_profile.response.email);
        }
        let scopes: Vec<String> = match self.state.refresh_token {
            Some(ref refresh_token) => {
                
                let mut all_scopes: Vec<String> = vec![];
                all_scopes.extend(scopes.iter().map(ToString::to_string));
                let existing_scopes = refresh_token.scopes.clone();
                all_scopes.extend(existing_scopes);
                HashSet::<String>::from_iter(all_scopes)
                    .into_iter()
                    .collect()
            }
            None => scopes.iter().map(ToString::to_string).collect(),
        };
        let scopes: Vec<&str> = scopes.iter().map(<_>::as_ref).collect();
        self.oauth_flow(url, &scopes)
    }
    
    
    
    
    
    
    
    
    
    pub fn authorize_code_using_session_token(
        &self,
        auth_params: AuthorizationParameters,
    ) -> Result<String> {
        let session_token = self.get_session_token()?;
        
        
        let allowed_scopes = self.client.scoped_key_data(
            &self.state.config,
            &session_token,
            &auth_params.client_id,
            &auth_params.scope.join(" "),
        )?;
        if let Some(not_allowed_scope) = auth_params
            .scope
            .iter()
            .find(|scope| !allowed_scopes.contains_key(*scope))
        {
            return Err(ErrorKind::ScopeNotAllowed(
                auth_params.client_id.clone(),
                not_allowed_scope.clone(),
            )
            .into());
        }
        let keys_jwe = if let Some(keys_jwk) = auth_params.keys_jwk {
            let mut scoped_keys = HashMap::new();
            allowed_scopes
                .iter()
                .try_for_each(|(scope, _)| -> Result<()> {
                    scoped_keys.insert(
                        scope,
                        self.state
                            .scoped_keys
                            .get(scope)
                            .ok_or_else(|| ErrorKind::NoScopedKey(scope.clone()))?,
                    );
                    Ok(())
                })?;
            let scoped_keys = serde_json::to_string(&scoped_keys)?;
            let keys_jwk = base64::decode_config(keys_jwk, base64::URL_SAFE_NO_PAD)?;
            let jwk = serde_json::from_slice(&keys_jwk)?;
            Some(jwcrypto::encrypt_to_jwe(
                scoped_keys.as_bytes(),
                EncryptionParameters::ECDH_ES {
                    enc: EncryptionAlgorithm::A256GCM,
                    peer_jwk: &jwk,
                },
            )?)
        } else {
            None
        };
        let auth_request_params = AuthorizationRequestParameters {
            client_id: auth_params.client_id,
            scope: auth_params.scope.join(" "),
            state: auth_params.state,
            access_type: auth_params.access_type,
            code_challenge: auth_params
                .pkce_params
                .as_ref()
                .map(|param| param.code_challenge.clone()),
            code_challenge_method: auth_params
                .pkce_params
                .map(|param| param.code_challenge_method),
            keys_jwe,
        };
        let resp = self.client.authorization_code_using_session_token(
            &self.state.config,
            &session_token,
            auth_request_params,
        )?;
        Ok(resp.code)
    }
    fn oauth_flow(&mut self, mut url: Url, scopes: &[&str]) -> Result<String> {
        self.clear_access_token_cache();
        let state = util::random_base64_url_string(16)?;
        let code_verifier = util::random_base64_url_string(43)?;
        let code_challenge = digest::digest(&digest::SHA256, &code_verifier.as_bytes())?;
        let code_challenge = base64::encode_config(&code_challenge, base64::URL_SAFE_NO_PAD);
        let scoped_keys_flow = ScopedKeysFlow::with_random_key()?;
        let jwk = scoped_keys_flow.get_public_key_jwk()?;
        let jwk_json = serde_json::to_string(&jwk)?;
        let keys_jwk = base64::encode_config(&jwk_json, base64::URL_SAFE_NO_PAD);
        url.query_pairs_mut()
            .append_pair("client_id", &self.state.config.client_id)
            .append_pair("scope", &scopes.join(" "))
            .append_pair("state", &state)
            .append_pair("code_challenge_method", "S256")
            .append_pair("code_challenge", &code_challenge)
            .append_pair("access_type", "offline")
            .append_pair("keys_jwk", &keys_jwk);
        if self.state.config.redirect_uri == OAUTH_WEBCHANNEL_REDIRECT {
            url.query_pairs_mut()
                .append_pair("context", "oauth_webchannel_v1");
        } else {
            url.query_pairs_mut()
                .append_pair("redirect_uri", &self.state.config.redirect_uri);
        }
        self.flow_store.insert(
            state, 
            OAuthFlow {
                scoped_keys_flow: Some(scoped_keys_flow),
                code_verifier,
            },
        );
        Ok(url.to_string())
    }
    
    
    
    
    
    pub fn complete_oauth_flow(&mut self, code: &str, state: &str) -> Result<()> {
        self.clear_access_token_cache();
        let oauth_flow = match self.flow_store.remove(state) {
            Some(oauth_flow) => oauth_flow,
            None => return Err(ErrorKind::UnknownOAuthState.into()),
        };
        let resp = self.client.refresh_token_with_code(
            &self.state.config,
            &code,
            &oauth_flow.code_verifier,
        )?;
        self.handle_oauth_response(resp, oauth_flow.scoped_keys_flow)
    }
    pub(crate) fn handle_oauth_response(
        &mut self,
        resp: OAuthTokenResponse,
        scoped_keys_flow: Option<ScopedKeysFlow>,
    ) -> Result<()> {
        if let Some(ref jwe) = resp.keys_jwe {
            let scoped_keys_flow = scoped_keys_flow.ok_or_else(|| {
                ErrorKind::UnrecoverableServerError("Got a JWE but have no JWK to decrypt it.")
            })?;
            let decrypted_keys = scoped_keys_flow.decrypt_keys_jwe(jwe)?;
            let scoped_keys: serde_json::Map<String, serde_json::Value> =
                serde_json::from_str(&decrypted_keys)?;
            for (scope, key) in scoped_keys {
                let scoped_key: ScopedKey = serde_json::from_value(key)?;
                self.state.scoped_keys.insert(scope, scoped_key);
            }
        }
        
        
        if resp.session_token.is_some() {
            self.state.session_token = resp.session_token;
        }
        
        
        
        if let Err(err) = self
            .client
            .destroy_access_token(&self.state.config, &resp.access_token)
        {
            log::warn!("Access token destruction failure: {:?}", err);
        }
        let old_refresh_token = self.state.refresh_token.clone();
        let new_refresh_token = resp
            .refresh_token
            .ok_or_else(|| ErrorKind::UnrecoverableServerError("No refresh token in response"))?;
        
        
        let old_device_info = match old_refresh_token {
            Some(_) => match self.get_current_device() {
                Ok(maybe_device) => maybe_device,
                Err(err) => {
                    log::warn!("Error while getting previous device information: {:?}", err);
                    None
                }
            },
            None => None,
        };
        self.state.refresh_token = Some(RefreshToken {
            token: new_refresh_token,
            scopes: HashSet::from_iter(resp.scope.split(' ').map(ToString::to_string)),
        });
        
        
        if let Some(ref refresh_token) = old_refresh_token {
            if let Err(err) = self
                .client
                .destroy_refresh_token(&self.state.config, &refresh_token.token)
            {
                log::warn!("Refresh token destruction failure: {:?}", err);
            }
        }
        if let Some(ref device_info) = old_device_info {
            if let Err(err) = self.replace_device(
                &device_info.display_name,
                &device_info.device_type,
                &device_info.push_subscription,
                &device_info.available_commands,
            ) {
                log::warn!("Device information restoration failed: {:?}", err);
            }
        }
        
        
        self.state.device_capabilities.clear();
        Ok(())
    }
    
    
    
    
    
    
    
    pub fn handle_session_token_change(&mut self, session_token: &str) -> Result<()> {
        let old_refresh_token = self
            .state
            .refresh_token
            .as_ref()
            .ok_or_else(|| ErrorKind::NoRefreshToken)?;
        let scopes: Vec<&str> = old_refresh_token.scopes.iter().map(AsRef::as_ref).collect();
        let resp = self.client.refresh_token_with_session_token(
            &self.state.config,
            &session_token,
            &scopes,
        )?;
        let new_refresh_token = resp
            .refresh_token
            .ok_or_else(|| ErrorKind::UnrecoverableServerError("No refresh token in response"))?;
        self.state.refresh_token = Some(RefreshToken {
            token: new_refresh_token,
            scopes: HashSet::from_iter(resp.scope.split(' ').map(ToString::to_string)),
        });
        self.state.session_token = Some(session_token.to_owned());
        self.clear_access_token_cache();
        self.clear_devices_and_attached_clients_cache();
        
        
        self.state.device_capabilities.clear();
        Ok(())
    }
    
    pub fn clear_access_token_cache(&mut self) {
        self.state.access_token_cache.clear();
    }
    #[cfg(feature = "integration_test")]
    pub fn new_logged_in(
        config: crate::Config,
        session_token: &str,
        scoped_keys: HashMap<String, ScopedKey>,
    ) -> Self {
        let mut fxa = FirefoxAccount::with_config(config);
        fxa.state.session_token = Some(session_token.to_owned());
        scoped_keys.iter().for_each(|(key, val)| {
            fxa.state.scoped_keys.insert(key.to_string(), val.clone());
        });
        fxa
    }
}
const AUTH_CIRCUIT_BREAKER_CAPACITY: u8 = 5;
const AUTH_CIRCUIT_BREAKER_RENEWAL_RATE: f32 = 3.0 / 60.0 / 1000.0; 
#[derive(Clone, Copy)]
pub(crate) struct AuthCircuitBreaker {
    tokens: u8,
    last_refill: u64, 
}
impl Default for AuthCircuitBreaker {
    fn default() -> Self {
        AuthCircuitBreaker {
            tokens: AUTH_CIRCUIT_BREAKER_CAPACITY,
            last_refill: Self::now(),
        }
    }
}
impl AuthCircuitBreaker {
    pub(crate) fn check(&mut self) -> Result<()> {
        self.refill();
        if self.tokens == 0 {
            return Err(ErrorKind::AuthCircuitBreakerError.into());
        }
        self.tokens -= 1;
        Ok(())
    }
    fn refill(&mut self) {
        let now = Self::now();
        let new_tokens =
            ((now - self.last_refill) as f64 * AUTH_CIRCUIT_BREAKER_RENEWAL_RATE as f64) as u8; 
        if new_tokens > 0 {
            self.last_refill = now;
            self.tokens = std::cmp::min(
                AUTH_CIRCUIT_BREAKER_CAPACITY,
                self.tokens.saturating_add(new_tokens),
            );
        }
    }
    #[cfg(not(test))]
    #[inline]
    fn now() -> u64 {
        util::now()
    }
    #[cfg(test)]
    fn now() -> u64 {
        1600000000000
    }
}
#[derive(Clone)]
pub struct AuthorizationPKCEParams {
    pub code_challenge: String,
    pub code_challenge_method: String,
}
#[derive(Clone)]
pub struct AuthorizationParameters {
    pub client_id: String,
    pub scope: Vec<String>,
    pub state: String,
    pub access_type: String,
    pub pkce_params: Option<AuthorizationPKCEParams>,
    pub keys_jwk: Option<String>,
}
impl TryFrom<Url> for AuthorizationParameters {
    type Error = Error;
    fn try_from(url: Url) -> Result<Self> {
        let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
        let scope = query_map
            .get("scope")
            .cloned()
            .ok_or_else(|| ErrorKind::MissingUrlParameter("scope"))?;
        let client_id = query_map
            .get("client_id")
            .cloned()
            .ok_or_else(|| ErrorKind::MissingUrlParameter("client_id"))?;
        let state = query_map
            .get("state")
            .cloned()
            .ok_or_else(|| ErrorKind::MissingUrlParameter("state"))?;
        let access_type = query_map
            .get("access_type")
            .cloned()
            .ok_or_else(|| ErrorKind::MissingUrlParameter("access_type"))?;
        let code_challenge = query_map.get("code_challenge").cloned();
        let code_challenge_method = query_map.get("code_challenge_method").cloned();
        let pkce_params = match (code_challenge, code_challenge_method) {
            (Some(code_challenge), Some(code_challenge_method)) => Some(AuthorizationPKCEParams {
                code_challenge,
                code_challenge_method,
            }),
            _ => None,
        };
        let keys_jwk = query_map.get("keys_jwk").cloned();
        Ok(Self {
            client_id,
            scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
            state,
            access_type,
            pkce_params,
            keys_jwk,
        })
    }
}
pub struct MetricsParams {
    pub parameters: std::collections::HashMap<String, String>,
}
impl MetricsParams {
    fn append_params_to_url(&self, url: &mut Url) {
        self.parameters
            .iter()
            .for_each(|(parameter_name, parameter_value)| {
                url.query_pairs_mut()
                    .append_pair(parameter_name, parameter_value);
            });
    }
}
#[derive(Clone, Serialize, Deserialize)]
pub struct RefreshToken {
    pub token: String,
    pub scopes: HashSet<String>,
}
impl std::fmt::Debug for RefreshToken {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RefreshToken")
            .field("scopes", &self.scopes)
            .finish()
    }
}
pub struct OAuthFlow {
    pub scoped_keys_flow: Option<ScopedKeysFlow>,
    pub code_verifier: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AccessTokenInfo {
    pub scope: String,
    pub token: String,
    pub key: Option<ScopedKey>,
    pub expires_at: u64, 
}
impl std::fmt::Debug for AccessTokenInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AccessTokenInfo")
            .field("scope", &self.scope)
            .field("key", &self.key)
            .field("expires_at", &self.expires_at)
            .finish()
    }
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IntrospectInfo {
    pub active: bool,
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::{http_client::*, Config};
    use std::borrow::Cow;
    use std::collections::HashMap;
    use std::sync::Arc;
    impl FirefoxAccount {
        pub fn add_cached_token(&mut self, scope: &str, token_info: AccessTokenInfo) {
            self.state
                .access_token_cache
                .insert(scope.to_string(), token_info);
        }
        pub fn set_session_token(&mut self, session_token: &str) {
            self.state.session_token = Some(session_token.to_owned());
        }
    }
    #[test]
    fn test_oauth_flow_url() {
        
        viaduct_reqwest::use_reqwest_backend();
        let config = Config::new(
            "https://accounts.firefox.com",
            "12345678",
            "https://foo.bar",
        );
        let mut params = HashMap::new();
        params.insert("flow_id".to_string(), "87654321".to_string());
        let metrics_params = MetricsParams { parameters: params };
        let mut fxa = FirefoxAccount::with_config(config);
        let url = fxa
            .begin_oauth_flow(&["profile"], "test_oauth_flow_url", Some(metrics_params))
            .unwrap();
        let flow_url = Url::parse(&url).unwrap();
        assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
        assert_eq!(flow_url.path(), "/authorization");
        let mut pairs = flow_url.query_pairs();
        assert_eq!(pairs.count(), 12);
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("action"), Cow::Borrowed("email")))
        );
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("response_type"), Cow::Borrowed("code")))
        );
        assert_eq!(
            pairs.next(),
            Some((
                Cow::Borrowed("entrypoint"),
                Cow::Borrowed("test_oauth_flow_url")
            ))
        );
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("flow_id"), Cow::Borrowed("87654321")))
        );
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
        );
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("scope"), Cow::Borrowed("profile")))
        );
        let state_param = pairs.next().unwrap();
        assert_eq!(state_param.0, Cow::Borrowed("state"));
        assert_eq!(state_param.1.len(), 22);
        assert_eq!(
            pairs.next(),
            Some((
                Cow::Borrowed("code_challenge_method"),
                Cow::Borrowed("S256")
            ))
        );
        let code_challenge_param = pairs.next().unwrap();
        assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
        assert_eq!(code_challenge_param.1.len(), 43);
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
        );
        let keys_jwk = pairs.next().unwrap();
        assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
        assert_eq!(keys_jwk.1.len(), 168);
        assert_eq!(
            pairs.next(),
            Some((
                Cow::Borrowed("redirect_uri"),
                Cow::Borrowed("https://foo.bar")
            ))
        );
    }
    #[test]
    fn test_force_auth_url() {
        let config = Config::stable_dev("12345678", "https://foo.bar");
        let mut fxa = FirefoxAccount::with_config(config);
        let email = "test@example.com";
        fxa.add_cached_profile("123", email);
        let url = fxa
            .begin_oauth_flow(&["profile"], "test_force_auth_url", None)
            .unwrap();
        let url = Url::parse(&url).unwrap();
        assert_eq!(url.path(), "/oauth/force_auth");
        let mut pairs = url.query_pairs();
        assert_eq!(
            pairs.find(|e| e.0 == "email"),
            Some((Cow::Borrowed("email"), Cow::Borrowed(email),))
        );
    }
    #[test]
    fn test_webchannel_context_url() {
        
        viaduct_reqwest::use_reqwest_backend();
        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
        let config = Config::new(
            "https://accounts.firefox.com",
            "12345678",
            "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
        );
        let mut fxa = FirefoxAccount::with_config(config);
        let url = fxa
            .begin_oauth_flow(&SCOPES, "test_webchannel_context_url", None)
            .unwrap();
        let url = Url::parse(&url).unwrap();
        let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
        let context = &query_params["context"];
        assert_eq!(context, "oauth_webchannel_v1");
        assert_eq!(query_params.get("redirect_uri"), None);
    }
    #[test]
    fn test_webchannel_pairing_context_url() {
        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
        const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
        let config = Config::new(
            "https://accounts.firefox.com",
            "12345678",
            "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
        );
        let mut fxa = FirefoxAccount::with_config(config);
        let url = fxa
            .begin_pairing_flow(
                &PAIRING_URL,
                &SCOPES,
                "test_webchannel_pairing_context_url",
                None,
            )
            .unwrap();
        let url = Url::parse(&url).unwrap();
        let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
        let context = &query_params["context"];
        assert_eq!(context, "oauth_webchannel_v1");
        assert_eq!(query_params.get("redirect_uri"), None);
    }
    #[test]
    fn test_pairing_flow_url() {
        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
        const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
        const EXPECTED_URL: &str = "https://accounts.firefox.com/pair/supp?client_id=12345678&redirect_uri=https%3A%2F%2Ffoo.bar&scope=https%3A%2F%2Fidentity.mozilla.com%2Fapps%2Foldsync&state=SmbAA_9EA5v1R2bgIPeWWw&code_challenge_method=S256&code_challenge=ZgHLPPJ8XYbXpo7VIb7wFw0yXlTa6MUOVfGiADt0JSM&access_type=offline&keys_jwk=eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6Ing5LUltQjJveDM0LTV6c1VmbW5sNEp0Ti14elV2eFZlZXJHTFRXRV9BT0kiLCJ5IjoiNXBKbTB3WGQ4YXdHcm0zREl4T1pWMl9qdl9tZEx1TWlMb1RkZ1RucWJDZyJ9#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
        let config = Config::new(
            "https://accounts.firefox.com",
            "12345678",
            "https://foo.bar",
        );
        let mut params = HashMap::new();
        params.insert("flow_id".to_string(), "87654321".to_string());
        let metrics_params = MetricsParams { parameters: params };
        let mut fxa = FirefoxAccount::with_config(config);
        let url = fxa
            .begin_pairing_flow(
                &PAIRING_URL,
                &SCOPES,
                "test_pairing_flow_url",
                Some(metrics_params),
            )
            .unwrap();
        let flow_url = Url::parse(&url).unwrap();
        let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap();
        assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
        assert_eq!(flow_url.path(), "/pair/supp");
        assert_eq!(flow_url.fragment(), expected_parsed_url.fragment());
        let mut pairs = flow_url.query_pairs();
        assert_eq!(pairs.count(), 10);
        assert_eq!(
            pairs.next(),
            Some((
                Cow::Borrowed("entrypoint"),
                Cow::Borrowed("test_pairing_flow_url")
            ))
        );
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("flow_id"), Cow::Borrowed("87654321")))
        );
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
        );
        assert_eq!(
            pairs.next(),
            Some((
                Cow::Borrowed("scope"),
                Cow::Borrowed("https://identity.mozilla.com/apps/oldsync")
            ))
        );
        let state_param = pairs.next().unwrap();
        assert_eq!(state_param.0, Cow::Borrowed("state"));
        assert_eq!(state_param.1.len(), 22);
        assert_eq!(
            pairs.next(),
            Some((
                Cow::Borrowed("code_challenge_method"),
                Cow::Borrowed("S256")
            ))
        );
        let code_challenge_param = pairs.next().unwrap();
        assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
        assert_eq!(code_challenge_param.1.len(), 43);
        assert_eq!(
            pairs.next(),
            Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
        );
        let keys_jwk = pairs.next().unwrap();
        assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
        assert_eq!(keys_jwk.1.len(), 168);
        assert_eq!(
            pairs.next(),
            Some((
                Cow::Borrowed("redirect_uri"),
                Cow::Borrowed("https://foo.bar")
            ))
        );
    }
    #[test]
    fn test_pairing_flow_origin_mismatch() {
        static PAIRING_URL: &str = "https://bad.origin.com/pair#channel_id=foo&channel_key=bar";
        let config = Config::stable_dev("12345678", "https://foo.bar");
        let mut fxa = FirefoxAccount::with_config(config);
        let url = fxa.begin_pairing_flow(
            &PAIRING_URL,
            &["https://identity.mozilla.com/apps/oldsync"],
            "test_pairiong_flow_origin_mismatch",
            None,
        );
        assert!(url.is_err());
        match url {
            Ok(_) => {
                panic!("should have error");
            }
            Err(err) => match err.kind() {
                ErrorKind::OriginMismatch { .. } => {}
                _ => panic!("error not OriginMismatch"),
            },
        }
    }
    #[test]
    fn test_check_authorization_status() {
        let config = Config::stable_dev("12345678", "https://foo.bar");
        let mut fxa = FirefoxAccount::with_config(config);
        let refresh_token_scopes = std::collections::HashSet::new();
        fxa.state.refresh_token = Some(RefreshToken {
            token: "refresh_token".to_owned(),
            scopes: refresh_token_scopes,
        });
        let mut client = FxAClientMock::new();
        client
            .expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
                token.partial_eq("refresh_token")
            })
            .times(1)
            .returns_once(Ok(IntrospectResponse { active: true }));
        fxa.set_client(Arc::new(client));
        let auth_status = fxa.check_authorization_status().unwrap();
        assert_eq!(auth_status.active, true);
    }
    #[test]
    fn test_check_authorization_status_circuit_breaker() {
        let config = Config::stable_dev("12345678", "https://foo.bar");
        let mut fxa = FirefoxAccount::with_config(config);
        let refresh_token_scopes = std::collections::HashSet::new();
        fxa.state.refresh_token = Some(RefreshToken {
            token: "refresh_token".to_owned(),
            scopes: refresh_token_scopes,
        });
        let mut client = FxAClientMock::new();
        
        
        client
            .expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
                token.partial_eq("refresh_token")
            })
            .returns_once(Ok(IntrospectResponse { active: true }));
        client
            .expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
                token.partial_eq("refresh_token")
            })
            .returns_once(Ok(IntrospectResponse { active: true }));
        client
            .expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
                token.partial_eq("refresh_token")
            })
            .returns_once(Ok(IntrospectResponse { active: true }));
        client
            .expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
                token.partial_eq("refresh_token")
            })
            .returns_once(Ok(IntrospectResponse { active: true }));
        client
            .expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
                token.partial_eq("refresh_token")
            })
            .returns_once(Ok(IntrospectResponse { active: true }));
        client.expect_oauth_introspect_refresh_token_calls_in_order();
        fxa.set_client(Arc::new(client));
        for _ in 0..5 {
            assert!(fxa.check_authorization_status().is_ok());
        }
        match fxa.check_authorization_status() {
            Ok(_) => unreachable!("should not happen"),
            Err(err) => assert!(matches!(err.kind(), ErrorKind::AuthCircuitBreakerError)),
        }
    }
    #[test]
    fn test_auth_circuit_breaker_unit_recovery() {
        let mut breaker = AuthCircuitBreaker::default();
        
        assert_eq!(AuthCircuitBreaker::now(), 1600000000000);
        for _ in 0..AUTH_CIRCUIT_BREAKER_CAPACITY {
            assert!(breaker.check().is_ok());
        }
        assert!(breaker.check().is_err());
        
        breaker.last_refill -= 60 * 1000;
        let expected_tokens_before_check: u8 =
            (AUTH_CIRCUIT_BREAKER_RENEWAL_RATE * 60.0 * 1000.0) as u8;
        assert!(breaker.check().is_ok());
        assert_eq!(breaker.tokens, expected_tokens_before_check - 1);
    }
    use crate::scopes;
    #[test]
    fn test_auth_code_pair_valid_not_allowed_scope() {
        let config = Config::stable_dev("12345678", "https://foo.bar");
        let mut fxa = FirefoxAccount::with_config(config);
        fxa.set_session_token("session");
        let mut client = FxAClientMock::new();
        let not_allowed_scope = "https://identity.mozilla.com/apps/lockbox";
        let expected_scopes = scopes::OLD_SYNC
            .chars()
            .chain(std::iter::once(' '))
            .chain(not_allowed_scope.chars())
            .collect::<String>();
        client
            .expect_scoped_key_data(
                mockiato::Argument::any,
                |arg| arg.partial_eq("session"),
                |arg| arg.partial_eq("12345678"),
                |arg| arg.partial_eq(expected_scopes),
            )
            .returns_once(Err(ErrorKind::RemoteError {
                code: 400,
                errno: 163,
                error: "Invalid Scopes".to_string(),
                message: "Not allowed to request scopes".to_string(),
                info: "fyi, there was a server error".to_string(),
            }
            .into()));
        fxa.set_client(Arc::new(client));
        let auth_params = AuthorizationParameters {
            client_id: "12345678".to_string(),
            scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
            state: "somestate".to_string(),
            access_type: "offline".to_string(),
            pkce_params: None,
            keys_jwk: None,
        };
        let res = fxa.authorize_code_using_session_token(auth_params);
        assert!(res.is_err());
        let err = res.unwrap_err();
        if let ErrorKind::RemoteError {
            code,
            errno,
            error: _,
            message: _,
            info: _,
        } = err.kind()
        {
            assert_eq!(*code, 400);
            assert_eq!(*errno, 163); 
        } else {
            panic!("Should return an error from the server specifying that the requested scopes are not allowed");
        }
    }
    #[test]
    fn test_auth_code_pair_invalid_scope_not_allowed() {
        let config = Config::stable_dev("12345678", "https://foo.bar");
        let mut fxa = FirefoxAccount::with_config(config);
        fxa.set_session_token("session");
        let mut client = FxAClientMock::new();
        let invalid_scope = "IamAnInvalidScope";
        let expected_scopes = scopes::OLD_SYNC
            .chars()
            .chain(std::iter::once(' '))
            .chain(invalid_scope.chars())
            .collect::<String>();
        let mut server_ret = HashMap::new();
        server_ret.insert(
            scopes::OLD_SYNC.to_string(),
            ScopedKeyDataResponse {
                key_rotation_secret: "IamASecret".to_string(),
                key_rotation_timestamp: 100,
                identifier: "".to_string(),
            },
        );
        client
            .expect_scoped_key_data(
                mockiato::Argument::any,
                |arg| arg.partial_eq("session"),
                |arg| arg.partial_eq("12345678"),
                |arg| arg.partial_eq(expected_scopes),
            )
            .returns_once(Ok(server_ret));
        fxa.set_client(Arc::new(client));
        let auth_params = AuthorizationParameters {
            client_id: "12345678".to_string(),
            scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
            state: "somestate".to_string(),
            access_type: "offline".to_string(),
            pkce_params: None,
            keys_jwk: None,
        };
        let res = fxa.authorize_code_using_session_token(auth_params);
        assert!(res.is_err());
        let err = res.unwrap_err();
        if let ErrorKind::ScopeNotAllowed(client_id, scope) = err.kind() {
            assert_eq!(client_id.clone(), "12345678");
            assert_eq!(scope.clone(), "IamAnInvalidScope");
        } else {
            panic!("Should return an error that specifies the scope that is not allowed");
        }
    }
    #[test]
    fn test_auth_code_pair_scope_not_in_state() {
        let config = Config::stable_dev("12345678", "https://foo.bar");
        let mut fxa = FirefoxAccount::with_config(config);
        fxa.set_session_token("session");
        let mut client = FxAClientMock::new();
        let mut server_ret = HashMap::new();
        server_ret.insert(
            scopes::OLD_SYNC.to_string(),
            ScopedKeyDataResponse {
                key_rotation_secret: "IamASecret".to_string(),
                key_rotation_timestamp: 100,
                identifier: "".to_string(),
            },
        );
        client
            .expect_scoped_key_data(
                mockiato::Argument::any,
                |arg| arg.partial_eq("session"),
                |arg| arg.partial_eq("12345678"),
                |arg| arg.partial_eq(scopes::OLD_SYNC),
            )
            .returns_once(Ok(server_ret));
        fxa.set_client(Arc::new(client));
        let auth_params = AuthorizationParameters {
            client_id: "12345678".to_string(),
            scope: vec![scopes::OLD_SYNC.to_string()],
            state: "somestate".to_string(),
            access_type: "offline".to_string(),
            pkce_params: None,
            keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
        };
        let res = fxa.authorize_code_using_session_token(auth_params);
        assert!(res.is_err());
        let err = res.unwrap_err();
        if let ErrorKind::NoScopedKey(scope) = err.kind() {
            assert_eq!(scope.clone(), scopes::OLD_SYNC.to_string());
        } else {
            panic!("Should return an error that specifies the scope that is not in the state");
        }
    }
}