use base64::engine::general_purpose::URL_SAFE;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use serde_json::Map;
use serde_json::Value;
use sha2::Digest;
use sha2::Sha256;
use std::error::Error as StdError;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use tokio::sync::Mutex;

use crate::app_paths;
use crate::models::ExtractedAuth;
use crate::models::PreparedOauthLogin;
use crate::utils::set_private_permissions;
use crate::utils::truncate_for_error;

const DEFAULT_OAUTH_ISSUER: &str = "https://auth.openai.com";
const DEFAULT_OAUTH_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
const DEFAULT_OAUTH_SCOPE: &str =
    "openid profile email offline_access api.connectors.read api.connectors.invoke";
const DEFAULT_OAUTH_ORIGINATOR: &str = "codex_vscode";
const DEFAULT_OAUTH_REDIRECT_PORT: u16 = 1455;
const DEFAULT_OAUTH_TIMEOUT_SECS: i64 = 900;
const OAUTH_TOKEN_EXCHANGE_MAX_ATTEMPTS: usize = 3;
const OAUTH_TOKEN_EXCHANGE_RETRY_DELAY_MS: u64 = 500;
const NON_CHATGPT_AUTH_MODE_ERROR: &str =
    "当前账号不是 ChatGPT 登录模式，无法读取 Codex 5h/1week 用量。请先执行 codex login。";
const MISSING_CHATGPT_TOKEN_ERROR: &str = "当前 auth.json 未包含 ChatGPT 登录令牌。若该文件来自新版 Codex（尤其是 macOS），令牌可能保存在系统钥匙串/安全存储中，因此不能仅靠这个 auth.json 跨机导入。请在目标设备执行 codex login，或提供包含 access_token / id_token / refresh_token 的完整 auth.json。";

pub(crate) struct CodexOAuthTokens {
    pub(crate) access_token: String,
    pub(crate) refresh_token: String,
    pub(crate) account_id: Option<String>,
    pub(crate) expires_at_ms: Option<i64>,
}

#[derive(Debug, Clone)]
pub(crate) struct PendingOauthLogin {
    pub(crate) redirect_uri: String,
    pub(crate) state: String,
    pub(crate) code_verifier: String,
    pub(crate) expires_at: i64,
    pub(crate) reauthorize_account_id: Option<String>,
}

pub(crate) fn oauth_redirect_port() -> u16 {
    DEFAULT_OAUTH_REDIRECT_PORT
}

fn oauth_redirect_uri(port: u16) -> String {
    format!("http://localhost:{port}/auth/callback")
}

pub(crate) fn read_current_codex_auth() -> Result<Value, String> {
    let path = codex_auth_path()?;
    let raw = fs::read_to_string(&path)
        .map_err(|e| format!("读取当前 Codex 认证文件失败 {}: {e}", path.display()))?;
    serde_json::from_str(&raw).map_err(|e| format!("当前 Codex 认证文件不是合法 JSON: {e}"))
}

pub(crate) fn read_current_codex_auth_optional() -> Result<Option<Value>, String> {
    let path = codex_auth_path()?;
    if !path.exists() {
        return Ok(None);
    }

    let raw = fs::read_to_string(&path)
        .map_err(|e| format!("读取当前 Codex 认证文件失败 {}: {e}", path.display()))?;
    let value =
        serde_json::from_str(&raw).map_err(|e| format!("当前 Codex 认证文件不是合法 JSON: {e}"))?;
    Ok(Some(value))
}

pub(crate) fn write_active_codex_auth(auth_json: &Value) -> Result<(), String> {
    let path = codex_auth_path()?;
    let parent = path
        .parent()
        .ok_or_else(|| format!("无法解析 auth 目录 {}", path.display()))?;
    fs::create_dir_all(parent)
        .map_err(|e| format!("创建 auth 目录失败 {}: {e}", parent.display()))?;

    let normalized = normalize_auth_json_for_codex(auth_json.clone());
    let serialized = serde_json::to_string_pretty(&normalized)
        .map_err(|e| format!("序列化 auth.json 失败: {e}"))?;
    write_auth_file_atomically(&path, serialized.as_bytes())
}

pub(crate) fn prepare_oauth_login(
    redirect_port: u16,
) -> Result<(PendingOauthLogin, PreparedOauthLogin), String> {
    let state = uuid::Uuid::new_v4().simple().to_string();
    let code_verifier = format!(
        "{}{}",
        uuid::Uuid::new_v4().simple(),
        uuid::Uuid::new_v4().simple()
    );
    let code_challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(code_verifier.as_bytes()));
    let redirect_uri = oauth_redirect_uri(redirect_port);
    let expires_at = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|error| format!("读取系统时间失败: {error}"))?
        .as_secs() as i64
        + DEFAULT_OAUTH_TIMEOUT_SECS;

    let mut auth_url = reqwest::Url::parse(&format!("{DEFAULT_OAUTH_ISSUER}/oauth/authorize"))
        .map_err(|error| format!("生成授权链接失败: {error}"))?;
    auth_url
        .query_pairs_mut()
        .append_pair("response_type", "code")
        .append_pair("client_id", DEFAULT_OAUTH_CLIENT_ID)
        .append_pair("redirect_uri", &redirect_uri)
        .append_pair("scope", DEFAULT_OAUTH_SCOPE)
        .append_pair("state", &state)
        .append_pair("code_challenge", &code_challenge)
        .append_pair("code_challenge_method", "S256")
        .append_pair("id_token_add_organizations", "true")
        .append_pair("codex_cli_simplified_flow", "true")
        .append_pair("originator", DEFAULT_OAUTH_ORIGINATOR);

    let auth_url = auth_url.to_string();
    let pending = PendingOauthLogin {
        redirect_uri: redirect_uri.clone(),
        state,
        code_verifier,
        expires_at,
        reauthorize_account_id: None,
    };
    let prepared = PreparedOauthLogin {
        auth_url,
        redirect_uri,
    };
    Ok((pending, prepared))
}

pub(crate) async fn complete_oauth_callback_login(
    pending: &PendingOauthLogin,
    callback_url: &str,
) -> Result<Value, String> {
    let callback_url = callback_url.trim();
    if callback_url.is_empty() {
        return Err("请粘贴回调链接".to_string());
    }

    let parsed_url = parse_oauth_callback_url(callback_url)?;
    let params: std::collections::HashMap<String, String> = parsed_url
        .query_pairs()
        .map(|(key, value)| (key.to_string(), value.to_string()))
        .collect();

    if let Some(error) = params.get("error") {
        let description = params
            .get("error_description")
            .map(String::as_str)
            .unwrap_or(error.as_str());
        return Err(format!("授权失败: {description}"));
    }

    let Some(state) = params.get("state") else {
        return Err("回调链接缺少 state 参数".to_string());
    };
    if state != &pending.state {
        return Err("回调链接 state 不匹配，请重新生成授权链接".to_string());
    }

    let Some(code) = params.get("code") else {
        return Err("回调链接缺少 code 参数".to_string());
    };

    exchange_authorization_code(code, pending).await
}

pub(crate) fn normalize_imported_auth_json(auth_json: Value) -> Value {
    let Some(root) = auth_json.as_object() else {
        return auth_json;
    };

    if root.get("tokens").and_then(Value::as_object).is_some() {
        return normalize_auth_json_for_codex(auth_json);
    }

    if let Some(session_auth_json) = normalize_chatgpt_session_json(root) {
        return normalize_auth_json_for_codex(session_auth_json);
    }

    let Some(access_token) = root.get("access_token").and_then(Value::as_str) else {
        return auth_json;
    };
    let Some(id_token) = root.get("id_token").and_then(Value::as_str) else {
        return auth_json;
    };

    let mut tokens = Map::new();
    tokens.insert(
        "access_token".to_string(),
        Value::String(access_token.to_string()),
    );
    tokens.insert("id_token".to_string(), Value::String(id_token.to_string()));

    if let Some(refresh_token) = root.get("refresh_token").and_then(Value::as_str) {
        tokens.insert(
            "refresh_token".to_string(),
            Value::String(refresh_token.to_string()),
        );
    }
    if let Some(account_id) = root.get("account_id").and_then(Value::as_str) {
        tokens.insert(
            "account_id".to_string(),
            Value::String(account_id.to_string()),
        );
    }

    let mut normalized = Map::new();
    normalized.insert(
        "auth_mode".to_string(),
        Value::String(
            root.get("auth_mode")
                .and_then(Value::as_str)
                .unwrap_or("chatgpt")
                .to_string(),
        ),
    );
    normalized.insert("tokens".to_string(), Value::Object(tokens));

    if let Some(last_refresh) = root.get("last_refresh") {
        normalized.insert("last_refresh".to_string(), last_refresh.clone());
    }

    normalize_auth_json_for_codex(Value::Object(normalized))
}

fn normalize_chatgpt_session_json(root: &Map<String, Value>) -> Option<Value> {
    let access_token = first_string_at_paths(
        root,
        &[
            &["accessToken"],
            &["access_token"],
            &["token", "accessToken"],
            &["token", "access_token"],
            &["credentials", "access_token"],
        ],
    )?;
    let access_claims = decode_jwt_payload(&access_token).ok();
    let access_auth_claims = access_claims
        .as_ref()
        .and_then(|claims| claims.get("https://api.openai.com/auth"))
        .and_then(Value::as_object);

    let account_id = first_non_empty_string(vec![
        first_string_at_paths(
            root,
            &[
                &["account", "id"],
                &["account_id"],
                &["chatgpt_account_id"],
                &["credentials", "chatgpt_account_id"],
            ],
        ),
        access_auth_claims
            .and_then(|claims| claims.get("chatgpt_account_id"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
    ])?;
    let plan_type = first_non_empty_string(vec![
        first_string_at_paths(
            root,
            &[
                &["account", "planType"],
                &["account", "plan_type"],
                &["planType"],
                &["plan_type"],
                &["credentials", "plan_type"],
            ],
        ),
        access_auth_claims
            .and_then(|claims| claims.get("chatgpt_plan_type"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
    ]);
    let user_id = first_non_empty_string(vec![
        first_string_at_paths(
            root,
            &[
                &["user", "id"],
                &["user_id"],
                &["chatgpt_user_id"],
                &["credentials", "chatgpt_user_id"],
            ],
        ),
        access_auth_claims
            .and_then(|claims| {
                claims
                    .get("chatgpt_user_id")
                    .or_else(|| claims.get("user_id"))
            })
            .and_then(Value::as_str)
            .map(ToString::to_string),
    ]);
    let email = first_non_empty_string(vec![
        first_string_at_paths(
            root,
            &[&["user", "email"], &["email"], &["credentials", "email"]],
        ),
        access_claims
            .as_ref()
            .and_then(|claims| claims.get("email"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
        access_claims
            .as_ref()
            .and_then(|claims| claims.get("https://api.openai.com/profile"))
            .and_then(Value::as_object)
            .and_then(|profile| profile.get("email"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
    ]);
    let expires_at = session_expires_at_unix(root, access_claims.as_ref());
    let id_token = first_string_at_paths(
        root,
        &[
            &["idToken"],
            &["id_token"],
            &["token", "idToken"],
            &["token", "id_token"],
            &["credentials", "id_token"],
        ],
    )
    .unwrap_or_else(|| {
        build_synthetic_chatgpt_id_token(
            &account_id,
            plan_type.as_deref(),
            user_id.as_deref(),
            email.as_deref(),
            expires_at,
        )
    });

    let mut tokens = Map::new();
    tokens.insert("access_token".to_string(), Value::String(access_token));
    tokens.insert("id_token".to_string(), Value::String(id_token));
    tokens.insert("account_id".to_string(), Value::String(account_id));

    if let Some(refresh_token) = first_string_at_paths(
        root,
        &[
            &["refreshToken"],
            &["refresh_token"],
            &["token", "refreshToken"],
            &["token", "refresh_token"],
            &["credentials", "refresh_token"],
        ],
    ) {
        tokens.insert("refresh_token".to_string(), Value::String(refresh_token));
    } else {
        // Codex auth snapshots expect the field to exist, but browser session exports do not
        // contain a refresh token. Keep it empty so this import remains a short-lived credential.
        tokens.insert("refresh_token".to_string(), Value::String(String::new()));
    }

    if let Some(session_token) = first_string_at_paths(
        root,
        &[
            &["sessionToken"],
            &["session_token"],
            &["token", "sessionToken"],
            &["token", "session_token"],
        ],
    ) {
        tokens.insert("session_token".to_string(), Value::String(session_token));
    }

    Some(serde_json::json!({
        "OPENAI_API_KEY": Value::Null,
        "auth_mode": "chatgpt",
        "last_refresh": current_rfc3339_timestamp().unwrap_or_else(|_| {
            unix_timestamp_to_rfc3339(current_unix_timestamp()).unwrap_or_else(|| {
                "1970-01-01T00:00:00Z".to_string()
            })
        }),
        "tokens": tokens
    }))
}

fn string_at_path<'a>(root: &'a Map<String, Value>, path: &[&str]) -> Option<&'a str> {
    let (first_key, rest) = path.split_first()?;
    let mut current = root.get(*first_key)?;
    for key in rest {
        current = current.as_object()?.get(*key)?;
    }
    current
        .as_str()
        .map(str::trim)
        .filter(|value| !value.is_empty())
}

fn first_string_at_paths(root: &Map<String, Value>, paths: &[&[&str]]) -> Option<String> {
    paths
        .iter()
        .find_map(|path| string_at_path(root, path).map(ToString::to_string))
}

fn first_non_empty_string(values: Vec<Option<String>>) -> Option<String> {
    values
        .into_iter()
        .flatten()
        .map(|value| value.trim().to_string())
        .find(|value| !value.is_empty())
}

fn session_expires_at_unix(root: &Map<String, Value>, claims: Option<&Value>) -> i64 {
    claims
        .and_then(|value| value.get("exp"))
        .and_then(Value::as_i64)
        .filter(|value| *value > 0)
        .or_else(|| {
            root.get("expires")
                .and_then(Value::as_str)
                .and_then(|value| OffsetDateTime::parse(value, &Rfc3339).ok())
                .map(|value| value.unix_timestamp())
        })
        .unwrap_or_else(|| current_unix_timestamp() + 90 * 24 * 60 * 60)
}

fn build_synthetic_chatgpt_id_token(
    account_id: &str,
    plan_type: Option<&str>,
    user_id: Option<&str>,
    email: Option<&str>,
    expires_at: i64,
) -> String {
    let now = current_unix_timestamp();
    let mut auth_claims = Map::new();
    auth_claims.insert(
        "chatgpt_account_id".to_string(),
        Value::String(account_id.to_string()),
    );
    if let Some(plan_type) = plan_type.map(str::trim).filter(|value| !value.is_empty()) {
        auth_claims.insert(
            "chatgpt_plan_type".to_string(),
            Value::String(plan_type.to_string()),
        );
    }
    if let Some(user_id) = user_id.map(str::trim).filter(|value| !value.is_empty()) {
        auth_claims.insert(
            "chatgpt_user_id".to_string(),
            Value::String(user_id.to_string()),
        );
        auth_claims.insert("user_id".to_string(), Value::String(user_id.to_string()));
    }

    let mut payload = Map::new();
    payload.insert("iat".to_string(), Value::Number(now.into()));
    payload.insert("exp".to_string(), Value::Number(expires_at.into()));
    payload.insert(
        "https://api.openai.com/auth".to_string(),
        Value::Object(auth_claims),
    );
    if let Some(email) = email.map(str::trim).filter(|value| !value.is_empty()) {
        payload.insert("email".to_string(), Value::String(email.to_string()));
    }
    if let Some(user_id) = user_id.map(str::trim).filter(|value| !value.is_empty()) {
        payload.insert("sub".to_string(), Value::String(user_id.to_string()));
    }

    let header = serde_json::json!({
        "alg": "none",
        "typ": "JWT",
        "codex_tools_synthetic": true
    });
    encode_jwt_part(&header) + "." + &encode_jwt_part(&Value::Object(payload)) + ".synthetic"
}

fn encode_jwt_part(value: &Value) -> String {
    let bytes = serde_json::to_vec(value).unwrap_or_default();
    URL_SAFE_NO_PAD.encode(bytes)
}

/// 解析当前 auth.json，提取账号标识和用量接口所需 token。
///
/// 注意：`auth_mode` 在某些版本可能缺失，因此优先按 `tokens` 字段判断是否可用。
pub(crate) fn extract_auth(auth_json: &Value) -> Result<ExtractedAuth, String> {
    let mode = auth_json
        .get("auth_mode")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_ascii_lowercase();

    let tokens = auth_token_object(auth_json);
    let tokens = match tokens {
        Some(value) => value,
        None => {
            if !mode.is_empty() && mode != "chatgpt" && mode != "chatgpt_auth_tokens" {
                return Err(NON_CHATGPT_AUTH_MODE_ERROR.to_string());
            }
            return Err(MISSING_CHATGPT_TOKEN_ERROR.to_string());
        }
    };

    let access_token = tokens
        .get("access_token")
        .and_then(Value::as_str)
        .ok_or_else(|| "auth.json 缺少 access_token".to_string())?
        .to_string();

    let id_token = tokens
        .get("id_token")
        .and_then(Value::as_str)
        .ok_or_else(|| "auth.json 缺少 id_token".to_string())?;

    let mut account_id = tokens
        .get("account_id")
        .and_then(Value::as_str)
        .map(ToString::to_string);
    let mut email = None;
    let mut plan_type = None;
    let mut principal_id = None;

    if let Ok(claims) = decode_jwt_payload(id_token) {
        email = claims
            .get("email")
            .and_then(Value::as_str)
            .map(ToString::to_string);

        let auth_claim = claims
            .get("https://api.openai.com/auth")
            .and_then(Value::as_object);
        if account_id.is_none() {
            account_id = auth_claim
                .and_then(|value| value.get("chatgpt_account_id"))
                .and_then(Value::as_str)
                .map(ToString::to_string);
        }
        plan_type = auth_claim
            .and_then(|value| value.get("chatgpt_plan_type"))
            .and_then(Value::as_str)
            .map(ToString::to_string);
        principal_id = email
            .as_deref()
            .and_then(normalize_principal_key)
            .or_else(|| {
                auth_claim
                    .and_then(|value| {
                        value
                            .get("chatgpt_user_id")
                            .or_else(|| value.get("user_id"))
                    })
                    .and_then(Value::as_str)
                    .and_then(normalize_principal_key)
            })
            .or_else(|| {
                claims
                    .get("sub")
                    .and_then(Value::as_str)
                    .and_then(normalize_principal_key)
            });
    }

    let account_id =
        account_id.ok_or_else(|| "无法从 auth.json 识别 chatgpt_account_id".to_string())?;
    let principal_id = principal_id.unwrap_or_else(|| account_id.clone());

    Ok(ExtractedAuth {
        principal_id,
        account_id,
        access_token,
        email,
        plan_type,
    })
}

pub(crate) fn current_auth_account_key() -> Option<String> {
    read_current_codex_auth()
        .ok()
        .and_then(|auth_json| extract_auth(&auth_json).ok())
        .map(|auth| account_group_key(&auth.principal_id, &auth.account_id))
}

pub(crate) fn normalize_plan_type_key(plan_type: Option<&str>) -> String {
    let Some(value) = plan_type.map(str::trim).filter(|value| !value.is_empty()) else {
        return "unknown".to_string();
    };
    value.to_ascii_lowercase()
}

pub(crate) fn account_group_key(principal_id: &str, account_id: &str) -> String {
    format!("{}|{}", principal_id.trim(), account_id.trim())
}

pub(crate) fn account_variant_key(
    principal_id: &str,
    account_id: &str,
    plan_type: Option<&str>,
) -> String {
    format!(
        "{}|{}",
        account_group_key(principal_id, account_id),
        normalize_plan_type_key(plan_type)
    )
}

pub(crate) fn auth_variant_key(auth_json: &Value) -> Option<String> {
    let extracted = extract_auth(auth_json).ok()?;
    Some(account_variant_key(
        &extracted.principal_id,
        &extracted.account_id,
        extracted.plan_type.as_deref(),
    ))
}

pub(crate) fn current_auth_variant_key() -> Option<String> {
    read_current_codex_auth()
        .ok()
        .and_then(|auth_json| auth_variant_key(&auth_json))
}

fn normalize_principal_key(value: &str) -> Option<String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return None;
    }
    if trimmed.contains('@') {
        Some(trimmed.to_ascii_lowercase())
    } else {
        Some(trimmed.to_string())
    }
}

/// 为第三方客户端同步登录态时，提取可复用的 OpenAI OAuth token。
pub(crate) fn extract_codex_oauth_tokens(auth_json: &Value) -> Result<CodexOAuthTokens, String> {
    let tokens = auth_token_object(auth_json).ok_or_else(|| "auth.json 缺少 tokens".to_string())?;

    let access_token = tokens
        .get("access_token")
        .and_then(Value::as_str)
        .ok_or_else(|| "auth.json 缺少 access_token".to_string())?
        .to_string();
    let refresh_token = tokens
        .get("refresh_token")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| "auth.json 缺少 refresh_token".to_string())?
        .to_string();
    let account_id = tokens
        .get("account_id")
        .and_then(Value::as_str)
        .map(ToString::to_string);

    let expires_at_ms = tokens
        .get("id_token")
        .and_then(Value::as_str)
        .and_then(|id_token| decode_jwt_payload(id_token).ok())
        .and_then(|payload| payload.get("exp").and_then(Value::as_i64))
        .map(|value| value * 1000);

    Ok(CodexOAuthTokens {
        access_token,
        refresh_token,
        account_id,
        expires_at_ms,
    })
}

pub(crate) fn auth_tokens_expire_within(auth_json: &Value, lead_time_secs: i64) -> bool {
    let Some(tokens) = auth_token_object(auth_json) else {
        return false;
    };

    let now = match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(duration) => duration.as_secs() as i64,
        Err(_) => return false,
    };
    let refresh_deadline = now + lead_time_secs.max(0);

    // Codex 请求实际使用 access_token；id_token 主要提供身份声明，通常 1 小时过期。
    // 如果把 id_token 过期也当作刷新条件，工具会远早于 Codex 官方客户端刷新 refresh_token，
    // 容易把仍可正常使用的账号快照误判成“授权过期”。
    tokens
        .get("access_token")
        .and_then(Value::as_str)
        .and_then(jwt_expiration_unix)
        .map(|exp| exp <= refresh_deadline)
        .unwrap_or(false)
}

pub(crate) fn auth_tokens_need_refresh(auth_json: &Value) -> bool {
    auth_tokens_expire_within(auth_json, 60)
}

pub(crate) fn auth_tokens_need_keepalive_refresh(
    auth_json: &Value,
    token_lead_time_secs: i64,
    max_last_refresh_age_secs: i64,
) -> bool {
    if auth_tokens_expire_within(auth_json, token_lead_time_secs) {
        return true;
    }

    if max_last_refresh_age_secs <= 0 || auth_token_object(auth_json).is_none() {
        return false;
    }

    let Some(root) = auth_json.as_object() else {
        return false;
    };
    let Some(last_refresh) = root.get("last_refresh").and_then(last_refresh_unix_seconds) else {
        return true;
    };
    let now = match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(duration) => duration.as_secs() as i64,
        Err(_) => return false,
    };

    now.saturating_sub(last_refresh) >= max_last_refresh_age_secs
}

/// 使用 auth.json 内的 refresh_token 刷新 ChatGPT OAuth 令牌。
///
/// 返回更新后的 auth.json（仅内存对象，不会自动写盘）。
pub(crate) async fn refresh_chatgpt_auth_tokens(auth_json: &Value) -> Result<Value, String> {
    let tokens = auth_token_object(auth_json).ok_or_else(|| "auth.json 缺少 tokens".to_string())?;

    let refresh_token = tokens
        .get("refresh_token")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| "auth.json 缺少 refresh_token".to_string())?;
    let id_token = tokens
        .get("id_token")
        .and_then(Value::as_str)
        .ok_or_else(|| "auth.json 缺少 id_token".to_string())?;

    let claims = decode_jwt_payload(id_token)?;
    let issuer = claims
        .get("iss")
        .and_then(Value::as_str)
        .unwrap_or("https://auth.openai.com")
        .trim_end_matches('/')
        .to_string();
    let token_url = format!("{issuer}/oauth/token");

    let mut form_pairs: Vec<(&str, String)> = vec![
        ("grant_type", "refresh_token".to_string()),
        ("refresh_token", refresh_token.to_string()),
    ];
    if let Some(client_id) = extract_client_id_from_claims(&claims) {
        form_pairs.push(("client_id", client_id));
    }

    let client = reqwest::Client::builder()
        .user_agent("codex-tools/0.1")
        .build()
        .map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;

    let response = client
        .post(&token_url)
        .form(&form_pairs)
        .send()
        .await
        .map_err(|e| format!("刷新登录令牌失败 {token_url}: {e}"))?;

    let status = response.status();
    if !status.is_success() {
        let body = response.text().await.unwrap_or_default();
        return Err(format!(
            "刷新登录令牌失败 {token_url} -> {status}: {}",
            truncate_for_error(&body, 140)
        ));
    }

    let refreshed: RefreshedTokenPayload = response
        .json()
        .await
        .map_err(|e| format!("解析刷新令牌响应失败: {e}"))?;

    let mut updated = auth_json.clone();
    let root = updated
        .as_object_mut()
        .ok_or_else(|| "auth.json 结构异常（根节点不是对象）".to_string())?;
    let tokens = root
        .get_mut("tokens")
        .and_then(Value::as_object_mut)
        .ok_or_else(|| "auth.json 缺少 tokens".to_string())?;

    tokens.insert(
        "access_token".to_string(),
        Value::String(refreshed.access_token),
    );
    tokens.insert("id_token".to_string(), Value::String(refreshed.id_token));
    if let Some(refresh_token) = refreshed.refresh_token {
        tokens.insert("refresh_token".to_string(), Value::String(refresh_token));
    }
    let last_refresh = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|error| format!("读取系统时间失败: {error}"))?
        .as_secs()
        .to_string();
    root.insert("last_refresh".to_string(), Value::String(last_refresh));

    update_last_refresh(&mut updated)?;
    Ok(normalize_auth_json_for_codex(updated))
}

pub(crate) async fn refresh_chatgpt_auth_tokens_serialized(
    auth_json: &Value,
    refresh_lock: &Arc<Mutex<()>>,
) -> Result<Value, String> {
    let _guard = refresh_lock.lock().await;
    refresh_chatgpt_auth_tokens(auth_json).await
}

fn parse_oauth_callback_url(callback_url: &str) -> Result<reqwest::Url, String> {
    reqwest::Url::parse(callback_url)
        .or_else(|_| reqwest::Url::parse(&format!("http://localhost{callback_url}")))
        .map_err(|error| format!("回调链接格式无效: {error}"))
}

async fn exchange_authorization_code(
    code: &str,
    pending: &PendingOauthLogin,
) -> Result<Value, String> {
    let client = reqwest::Client::builder()
        .user_agent("codex-tools/0.1")
        .build()
        .map_err(|error| format!("创建 HTTP 客户端失败: {error}"))?;

    let token_url = format!("{DEFAULT_OAUTH_ISSUER}/oauth/token");
    exchange_authorization_code_with_client(
        &client,
        &token_url,
        code,
        pending,
        OAUTH_TOKEN_EXCHANGE_MAX_ATTEMPTS,
        OAUTH_TOKEN_EXCHANGE_RETRY_DELAY_MS,
    )
    .await
}

async fn exchange_authorization_code_with_client(
    client: &reqwest::Client,
    token_url: &str,
    code: &str,
    pending: &PendingOauthLogin,
    max_attempts: usize,
    retry_delay_ms: u64,
) -> Result<Value, String> {
    let form = [
        ("grant_type", "authorization_code"),
        ("code", code),
        ("redirect_uri", pending.redirect_uri.as_str()),
        ("client_id", DEFAULT_OAUTH_CLIENT_ID),
        ("code_verifier", pending.code_verifier.as_str()),
    ];

    let response = send_authorization_code_exchange_request(
        client,
        token_url,
        &form,
        max_attempts,
        retry_delay_ms,
    )
    .await?;

    let status = response.status();
    if !status.is_success() {
        let body = response.text().await.unwrap_or_default();
        return Err(format!(
            "换取登录令牌失败 {token_url} -> {status}: {}",
            truncate_for_error(&body, 200)
        ));
    }

    let token_response: OAuthTokenResponse = response
        .json()
        .await
        .map_err(|error| format!("解析 OAuth 登录响应失败: {error}"))?;

    build_auth_json_from_oauth_tokens(token_response)
}

async fn send_authorization_code_exchange_request(
    client: &reqwest::Client,
    token_url: &str,
    form: &[(&str, &str)],
    max_attempts: usize,
    retry_delay_ms: u64,
) -> Result<reqwest::Response, String> {
    let max_attempts = max_attempts.max(1);
    for attempt in 1..=max_attempts {
        let send_result = client.post(token_url).form(form).send().await;
        match send_result {
            Ok(response) => return Ok(response),
            Err(error) => {
                let error_message = format_reqwest_error(&error);
                if attempt >= max_attempts {
                    return Err(format!("换取登录令牌失败 {token_url}: {error_message}"));
                }
                log::warn!(
                    "OAuth token exchange attempt {attempt}/{max_attempts} failed: {error_message}"
                );
                if retry_delay_ms > 0 {
                    tokio::time::sleep(Duration::from_millis(
                        retry_delay_ms.saturating_mul(attempt as u64),
                    ))
                    .await;
                }
            }
        }
    }

    Err(format!("换取登录令牌失败 {token_url}: 未执行令牌请求"))
}

fn format_reqwest_error(error: &reqwest::Error) -> String {
    let mut segments = vec![error.to_string()];
    let mut source = StdError::source(error);
    while let Some(inner) = source {
        let message = inner.to_string();
        if !message.is_empty() && !segments.iter().any(|item| item == &message) {
            segments.push(message);
        }
        source = inner.source();
    }

    segments.join(": ")
}

fn build_auth_json_from_oauth_tokens(token_response: OAuthTokenResponse) -> Result<Value, String> {
    let id_token_claims = decode_jwt_payload(&token_response.id_token)?;
    let account_id = id_token_claims
        .get("https://api.openai.com/auth")
        .and_then(Value::as_object)
        .and_then(|auth| auth.get("chatgpt_account_id"))
        .and_then(Value::as_str)
        .ok_or_else(|| "无法从 OAuth 登录结果识别 chatgpt_account_id".to_string())?;

    let last_refresh = current_rfc3339_timestamp()?;

    Ok(serde_json::json!({
        "OPENAI_API_KEY": Value::Null,
        "auth_mode": "chatgpt",
        "last_refresh": last_refresh,
        "tokens": {
            "access_token": token_response.access_token,
            "refresh_token": token_response.refresh_token,
            "id_token": token_response.id_token,
            "account_id": account_id
        }
    }))
}

fn codex_auth_path() -> Result<PathBuf, String> {
    app_paths::codex_auth_path()
}

fn auth_token_object(auth_json: &Value) -> Option<&Map<String, Value>> {
    auth_json
        .get("tokens")
        .and_then(Value::as_object)
        .or_else(|| {
            let root = auth_json.as_object()?;
            if root.contains_key("access_token") && root.contains_key("id_token") {
                Some(root)
            } else {
                None
            }
        })
}

fn normalize_auth_json_for_codex(auth_json: Value) -> Value {
    let Some(root) = auth_json.as_object() else {
        return auth_json;
    };

    let mut normalized = root.clone();

    match normalized
        .get("last_refresh")
        .and_then(normalize_last_refresh_value)
    {
        Some(value) => {
            normalized.insert("last_refresh".to_string(), Value::String(value));
        }
        None => {
            normalized.remove("last_refresh");
        }
    }

    Value::Object(normalized)
}

fn normalize_last_refresh_value(value: &Value) -> Option<String> {
    match value {
        Value::String(raw) => {
            let trimmed = raw.trim();
            if trimmed.is_empty() {
                return None;
            }
            if OffsetDateTime::parse(trimmed, &Rfc3339).is_ok() {
                return Some(trimmed.to_string());
            }
            trimmed
                .parse::<i64>()
                .ok()
                .and_then(unix_timestamp_to_rfc3339)
        }
        Value::Number(number) => number.as_i64().and_then(unix_timestamp_to_rfc3339),
        _ => None,
    }
}

fn last_refresh_unix_seconds(value: &Value) -> Option<i64> {
    match value {
        Value::String(raw) => {
            let trimmed = raw.trim();
            if trimmed.is_empty() {
                return None;
            }
            if let Ok(datetime) = OffsetDateTime::parse(trimmed, &Rfc3339) {
                return Some(datetime.unix_timestamp());
            }
            trimmed.parse::<i64>().ok().map(timestamp_value_to_secs)
        }
        Value::Number(number) => number.as_i64().map(timestamp_value_to_secs),
        _ => None,
    }
}

fn timestamp_value_to_secs(timestamp: i64) -> i64 {
    if timestamp.abs() >= 1_000_000_000_000 {
        timestamp / 1_000
    } else {
        timestamp
    }
}

fn current_unix_timestamp() -> i64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs() as i64)
        .unwrap_or(0)
}

fn unix_timestamp_to_rfc3339(timestamp: i64) -> Option<String> {
    let datetime = if timestamp.abs() >= 1_000_000_000_000 {
        OffsetDateTime::from_unix_timestamp_nanos(i128::from(timestamp) * 1_000_000).ok()?
    } else {
        OffsetDateTime::from_unix_timestamp(timestamp).ok()?
    };
    datetime.format(&Rfc3339).ok()
}

fn current_rfc3339_timestamp() -> Result<String, String> {
    OffsetDateTime::now_utc()
        .format(&Rfc3339)
        .map_err(|error| format!("生成 last_refresh 失败: {error}"))
}

fn update_last_refresh(auth_json: &mut Value) -> Result<(), String> {
    let timestamp = current_rfc3339_timestamp()?;
    let root = auth_json
        .as_object_mut()
        .ok_or_else(|| "auth.json 结构异常（根节点不是对象）".to_string())?;
    root.insert("last_refresh".to_string(), Value::String(timestamp));
    Ok(())
}

fn write_auth_file_atomically(path: &Path, contents: &[u8]) -> Result<(), String> {
    let parent = path
        .parent()
        .ok_or_else(|| format!("无法解析 auth 目录 {}", path.display()))?;
    let temp_path = parent.join(format!(
        ".{}.tmp-{}",
        path.file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("auth.json"),
        uuid::Uuid::new_v4()
    ));

    let write_result = (|| -> Result<(), String> {
        let mut temp_file = fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&temp_path)
            .map_err(|e| format!("创建临时 auth.json 失败 {}: {e}", temp_path.display()))?;
        temp_file
            .write_all(contents)
            .map_err(|e| format!("写入临时 auth.json 失败 {}: {e}", temp_path.display()))?;
        temp_file
            .sync_all()
            .map_err(|e| format!("刷新临时 auth.json 失败 {}: {e}", temp_path.display()))?;
        drop(temp_file);
        set_private_permissions(&temp_path);

        #[cfg(target_family = "unix")]
        {
            fs::rename(&temp_path, path).map_err(|e| {
                format!(
                    "替换 auth.json 失败 {} -> {}: {e}",
                    temp_path.display(),
                    path.display()
                )
            })?;

            let parent_dir = fs::File::open(parent)
                .map_err(|e| format!("打开 auth 目录失败 {}: {e}", parent.display()))?;
            parent_dir
                .sync_all()
                .map_err(|e| format!("刷新 auth 目录失败 {}: {e}", parent.display()))?;
        }

        #[cfg(not(target_family = "unix"))]
        {
            if path.exists() {
                fs::remove_file(path)
                    .map_err(|e| format!("移除旧 auth.json 失败 {}: {e}", path.display()))?;
            }
            fs::rename(&temp_path, path).map_err(|e| {
                format!(
                    "替换 auth.json 失败 {} -> {}: {e}",
                    temp_path.display(),
                    path.display()
                )
            })?;
        }

        set_private_permissions(path);
        Ok(())
    })();

    if write_result.is_err() {
        let _ = fs::remove_file(&temp_path);
    }

    write_result
}

fn decode_jwt_payload(token: &str) -> Result<Value, String> {
    let payload = token
        .split('.')
        .nth(1)
        .ok_or_else(|| "id_token 格式无效".to_string())?;

    let decoded = URL_SAFE_NO_PAD
        .decode(payload)
        .or_else(|_| {
            let remainder = payload.len() % 4;
            let padded = if remainder == 0 {
                payload.to_string()
            } else {
                format!("{payload}{}", "=".repeat(4 - remainder))
            };
            URL_SAFE.decode(padded)
        })
        .map_err(|e| format!("解码 id_token 失败: {e}"))?;

    serde_json::from_slice(&decoded).map_err(|e| format!("解析 id_token payload 失败: {e}"))
}

fn jwt_expiration_unix(token: &str) -> Option<i64> {
    decode_jwt_payload(token)
        .ok()
        .and_then(|claims| claims.get("exp").and_then(Value::as_i64))
}

#[derive(Debug, serde::Deserialize)]
struct RefreshedTokenPayload {
    access_token: String,
    id_token: String,
    refresh_token: Option<String>,
}

#[derive(Debug, serde::Deserialize)]
struct OAuthTokenResponse {
    access_token: String,
    refresh_token: String,
    id_token: String,
}

fn extract_client_id_from_claims(claims: &Value) -> Option<String> {
    let aud = claims.get("aud")?;
    match aud {
        Value::String(value) => {
            if value.is_empty() {
                None
            } else {
                Some(value.to_string())
            }
        }
        Value::Array(items) => items.iter().find_map(|item| {
            item.as_str().and_then(|value| {
                if value.is_empty() {
                    None
                } else {
                    Some(value.to_string())
                }
            })
        }),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use std::io::Read;
    use std::io::Write;
    use std::net::TcpListener;
    use std::thread;
    use std::time::Duration;
    use std::time::Instant;

    fn jwt_with_exp(exp: i64) -> String {
        let payload = URL_SAFE_NO_PAD.encode(format!(r#"{{"exp":{exp}}}"#));
        format!("header.{payload}.signature")
    }

    fn jwt_with_payload(payload: Value) -> String {
        let payload = URL_SAFE_NO_PAD.encode(payload.to_string());
        format!("header.{payload}.signature")
    }

    fn jwt_with_chatgpt_account_id(account_id: &str) -> String {
        let payload = URL_SAFE_NO_PAD.encode(
            json!({
                "https://api.openai.com/auth": {
                    "chatgpt_account_id": account_id
                }
            })
            .to_string(),
        );
        format!("header.{payload}.signature")
    }

    fn spawn_flaky_token_exchange_server(id_token: String) -> (String, thread::JoinHandle<usize>) {
        let listener = TcpListener::bind(("127.0.0.1", 0)).expect("test token server should bind");
        listener
            .set_nonblocking(true)
            .expect("test token server should be nonblocking");
        let token_url = format!(
            "http://{}/oauth/token",
            listener
                .local_addr()
                .expect("test token server address should be available")
        );

        let handle = thread::spawn(move || {
            let deadline = Instant::now() + Duration::from_secs(3);
            let mut attempts = 0usize;

            while attempts < 2 && Instant::now() < deadline {
                match listener.accept() {
                    Ok((mut stream, _)) => {
                        attempts += 1;
                        if attempts == 1 {
                            drop(stream);
                            continue;
                        }

                        let mut buffer = [0_u8; 4096];
                        let _ = stream.set_read_timeout(Some(Duration::from_secs(1)));
                        let _ = stream.read(&mut buffer);
                        let body = json!({
                            "access_token": "access-token",
                            "refresh_token": "refresh-token",
                            "id_token": id_token
                        })
                        .to_string();
                        let response = format!(
                            "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
                            body.len(),
                            body
                        );
                        stream
                            .write_all(response.as_bytes())
                            .expect("test token response should be written");
                    }
                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
                        thread::sleep(Duration::from_millis(10));
                    }
                    Err(error) => panic!("test token server failed: {error}"),
                }
            }

            attempts
        });

        (token_url, handle)
    }

    #[test]
    fn marks_refresh_needed_when_access_token_is_expired() {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("current time should be available")
            .as_secs() as i64;
        let auth_json = json!({
            "auth_mode": "chatgpt",
            "tokens": {
                "access_token": jwt_with_exp(now - 5),
                "id_token": jwt_with_exp(now + 3600),
                "refresh_token": "refresh-token"
            }
        });

        assert!(auth_tokens_need_refresh(&auth_json));
    }

    #[test]
    fn skips_refresh_when_both_tokens_are_still_fresh() {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("current time should be available")
            .as_secs() as i64;
        let auth_json = json!({
            "auth_mode": "chatgpt",
            "tokens": {
                "access_token": jwt_with_exp(now + 3600),
                "id_token": jwt_with_exp(now + 3600),
                "refresh_token": "refresh-token"
            }
        });

        assert!(!auth_tokens_need_refresh(&auth_json));
    }

    #[test]
    fn skips_refresh_when_only_id_token_is_expired() {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("current time should be available")
            .as_secs() as i64;
        let auth_json = json!({
            "auth_mode": "chatgpt",
            "tokens": {
                "access_token": jwt_with_exp(now + 3600),
                "id_token": jwt_with_exp(now - 5),
                "refresh_token": "refresh-token"
            }
        });

        assert!(!auth_tokens_need_refresh(&auth_json));
    }

    #[test]
    fn keepalive_refreshes_when_last_refresh_is_stale() {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("current time should be available")
            .as_secs() as i64;
        let auth_json = json!({
            "auth_mode": "chatgpt",
            "last_refresh": now - 3600,
            "tokens": {
                "access_token": jwt_with_exp(now + 7200),
                "id_token": jwt_with_exp(now + 7200),
                "refresh_token": "refresh-token"
            }
        });

        assert!(auth_tokens_need_keepalive_refresh(&auth_json, 60, 1800));
    }

    #[test]
    fn keepalive_skips_when_last_refresh_is_fresh() {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("current time should be available")
            .as_secs() as i64;
        let auth_json = json!({
            "auth_mode": "chatgpt",
            "last_refresh": now,
            "tokens": {
                "access_token": jwt_with_exp(now + 7200),
                "id_token": jwt_with_exp(now + 7200),
                "refresh_token": "refresh-token"
            }
        });

        assert!(!auth_tokens_need_keepalive_refresh(&auth_json, 60, 1800));
    }

    #[test]
    fn normalizes_legacy_unix_last_refresh_string() {
        let normalized = normalize_auth_json_for_codex(json!({
            "auth_mode": "chatgpt",
            "last_refresh": "1711111111",
            "tokens": {
                "access_token": "a",
                "refresh_token": "b",
                "id_token": "c"
            }
        }));

        let last_refresh = normalized
            .get("last_refresh")
            .and_then(serde_json::Value::as_str)
            .expect("last_refresh should be preserved");
        assert!(last_refresh.contains('T'));
        assert!(last_refresh.ends_with('Z'));
        assert_ne!(last_refresh, "1711111111");
    }

    #[test]
    fn removes_unparseable_last_refresh() {
        let normalized = normalize_auth_json_for_codex(json!({
            "auth_mode": "chatgpt",
            "last_refresh": "not-a-timestamp",
            "tokens": {
                "access_token": "a",
                "refresh_token": "b",
                "id_token": "c"
            }
        }));

        assert!(normalized.get("last_refresh").is_none());
    }

    #[test]
    fn keeps_valid_rfc3339_last_refresh() {
        let normalized = normalize_auth_json_for_codex(json!({
            "auth_mode": "chatgpt",
            "last_refresh": "2026-03-16T03:20:39.082325Z",
            "tokens": {
                "access_token": "a",
                "refresh_token": "b",
                "id_token": "c"
            }
        }));

        assert_eq!(
            normalized.get("last_refresh"),
            Some(&json!("2026-03-16T03:20:39.082325Z"))
        );
    }

    #[test]
    fn normalizes_chatgpt_session_json_with_synthetic_id_token() {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("current time should be available")
            .as_secs() as i64;
        let access_token = jwt_with_payload(json!({
            "exp": now + 3600,
            "https://api.openai.com/auth": {
                "chatgpt_account_id": "account-from-token",
                "chatgpt_plan_type": "plus",
                "chatgpt_user_id": "user-from-token"
            },
            "https://api.openai.com/profile": {
                "email": "token@example.com"
            }
        }));

        let normalized = normalize_imported_auth_json(json!({
            "user": {
                "id": "user-root",
                "name": "Root User",
                "email": "root@example.com"
            },
            "account": {
                "id": "account-root",
                "planType": "pro"
            },
            "accessToken": access_token,
            "authProvider": "openai",
            "sessionToken": "session-token"
        }));

        let tokens = normalized
            .get("tokens")
            .and_then(Value::as_object)
            .expect("session import should produce tokens");
        assert_eq!(
            tokens.get("account_id").and_then(Value::as_str),
            Some("account-root")
        );
        assert_eq!(
            tokens.get("session_token").and_then(Value::as_str),
            Some("session-token")
        );
        assert_eq!(
            tokens.get("refresh_token").and_then(Value::as_str),
            Some("")
        );

        let extracted = extract_auth(&normalized).expect("synthetic id token should be readable");
        assert_eq!(extracted.account_id, "account-root");
        assert_eq!(extracted.email.as_deref(), Some("root@example.com"));
        assert_eq!(extracted.plan_type.as_deref(), Some("pro"));
        assert!(!auth_tokens_need_keepalive_refresh(
            &normalized,
            60,
            7 * 24 * 60 * 60
        ));
    }

    #[test]
    fn extract_auth_reports_portable_hint_when_chatgpt_tokens_are_missing() {
        let error = extract_auth(&json!({
            "auth_mode": "chatgpt",
            "last_refresh": "2026-03-16T03:20:39.082325Z"
        }))
        .expect_err("chatgpt auth without tokens should fail");

        assert_eq!(error, MISSING_CHATGPT_TOKEN_ERROR);
    }

    #[test]
    fn extract_auth_keeps_non_chatgpt_mode_error_when_tokens_are_missing() {
        let error = extract_auth(&json!({
            "auth_mode": "apikey"
        }))
        .expect_err("non-chatgpt auth without tokens should fail");

        assert_eq!(error, NON_CHATGPT_AUTH_MODE_ERROR);
    }

    #[test]
    fn prepare_oauth_login_uses_requested_redirect_port() {
        let custom_port = oauth_redirect_port() + 17;
        let (pending, prepared) =
            prepare_oauth_login(custom_port).expect("oauth login prep should succeed");

        assert!(pending
            .redirect_uri
            .contains(&format!("localhost:{custom_port}/auth/callback")));
        assert_eq!(pending.redirect_uri, prepared.redirect_uri);
        assert!(prepared.auth_url.contains(&format!(
            "redirect_uri=http%3A%2F%2Flocalhost%3A{custom_port}%2Fauth%2Fcallback"
        )));
    }

    #[tokio::test]
    async fn exchange_authorization_code_retries_transient_send_failure() {
        let account_id = "acct_retry";
        let id_token = jwt_with_chatgpt_account_id(account_id);
        let (token_url, server_handle) = spawn_flaky_token_exchange_server(id_token);
        let pending = PendingOauthLogin {
            redirect_uri: "http://localhost:1455/auth/callback".to_string(),
            state: "state".to_string(),
            code_verifier: "code-verifier".to_string(),
            expires_at: 0,
            reauthorize_account_id: None,
        };
        let client = reqwest::Client::builder()
            .no_proxy()
            .build()
            .expect("test client should build");

        let auth_json = exchange_authorization_code_with_client(
            &client,
            &token_url,
            "auth-code",
            &pending,
            2,
            0,
        )
        .await
        .expect("token exchange should retry once and succeed");

        let accepted = server_handle
            .join()
            .expect("test token server should finish cleanly");
        assert_eq!(accepted, 2);
        assert_eq!(
            auth_json
                .pointer("/tokens/account_id")
                .and_then(Value::as_str),
            Some(account_id)
        );
    }
}
