use std::collections::BTreeSet;
use std::io::Write;
use std::sync::{LazyLock, Mutex};

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};

const KURA_CLIENT_NAME_HEADER: &str = "x-kura-client-name";
const KURA_CLIENT_VERSION_HEADER: &str = "x-kura-client-version";
const KURA_CLIENT_INSTALL_CHANNEL_HEADER: &str = "x-kura-client-install-channel";
const KURA_CLIENT_NOTICE_ACK_HEADER: &str = "x-kura-client-notice-ack";
const KURA_DRY_RUN_HEADER: &str = "x-kura-dry-run";
const KURA_CLI_CLIENT_NAME: &str = "kura-cli";
const KURA_NOTICE_ACK_MAX_IDS: usize = 16;

static PENDING_NOTICE_ACK_IDS: LazyLock<Mutex<BTreeSet<String>>> =
    LazyLock::new(|| Mutex::new(BTreeSet::new()));
static CLI_RUNTIME_OPTIONS: LazyLock<Mutex<CliRuntimeOptions>> =
    LazyLock::new(|| Mutex::new(CliRuntimeOptions::default()));

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CliOutputMode {
    Json,
    JsonCompact,
}

#[derive(Clone, Copy, Debug)]
pub struct CliRuntimeOptions {
    pub output_mode: CliOutputMode,
    pub quiet_stderr: bool,
    pub dry_run: bool,
}

impl Default for CliRuntimeOptions {
    fn default() -> Self {
        Self {
            output_mode: CliOutputMode::Json,
            quiet_stderr: false,
            dry_run: false,
        }
    }
}

pub fn set_cli_runtime_options(options: CliRuntimeOptions) {
    let mut current = CLI_RUNTIME_OPTIONS
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    *current = options;
}

pub fn cli_runtime_options() -> CliRuntimeOptions {
    *CLI_RUNTIME_OPTIONS
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

pub fn dry_run_enabled() -> bool {
    cli_runtime_options().dry_run
}

pub fn stderr_is_quiet() -> bool {
    cli_runtime_options().quiet_stderr
}

pub fn emit_stderr_line(line: &str) {
    if !stderr_is_quiet() {
        eprintln!("{line}");
    }
}

fn should_compact_output(raw_override: bool) -> bool {
    raw_override
        || matches!(
            cli_runtime_options().output_mode,
            CliOutputMode::JsonCompact
        )
}

pub fn format_json_output(value: &Value, raw_override: bool) -> String {
    if should_compact_output(raw_override) {
        serde_json::to_string(value).unwrap_or_else(|_| value.to_string())
    } else {
        serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
    }
}

pub fn print_json_stdout(value: &Value) {
    println!("{}", format_json_output(value, false));
}

pub fn print_json_stderr(value: &Value) {
    eprintln!("{}", format_json_output(value, false));
}

pub fn print_json_stdout_with_raw(value: &Value, raw_override: bool) {
    println!("{}", format_json_output(value, raw_override));
}

#[derive(Debug)]
pub struct RawApiResponse {
    pub status: u16,
    pub body: Value,
}

pub fn is_mutating_method(method: &reqwest::Method) -> bool {
    matches!(
        *method,
        reqwest::Method::POST
            | reqwest::Method::PUT
            | reqwest::Method::PATCH
            | reqwest::Method::DELETE
    )
}

pub fn emit_dry_run_request(
    method: &reqwest::Method,
    api_url: &str,
    path: &str,
    token_present: bool,
    body: Option<&Value>,
    query: &[(String, String)],
    headers: &[(String, String)],
    raw_output: bool,
    note: Option<&str>,
) -> i32 {
    let query_entries: Vec<Value> = query
        .iter()
        .map(|(key, value)| json!({ "key": key, "value": value }))
        .collect();
    let header_entries: Vec<Value> = headers
        .iter()
        .map(|(key, value)| json!({ "key": key, "value": value }))
        .collect();

    let mut preview = json!({
        "dry_run": true,
        "status": "not_executed",
        "method": method.as_str(),
        "path": path,
        "url": format!("{api_url}{path}"),
        "auth": {
            "authorization_header_present": token_present
        },
        "query": query_entries,
        "headers": header_entries,
        "body": body.cloned().unwrap_or(Value::Null)
    });

    if let Some(note) = note {
        preview["note"] = json!(note);
    }

    print_json_stdout_with_raw(&preview, raw_output);
    0
}

fn append_server_dry_run_header(
    headers: &[(String, String)],
    enable_server_dry_run: bool,
    method: &reqwest::Method,
) -> Vec<(String, String)> {
    let mut merged = headers.to_vec();
    if enable_server_dry_run && dry_run_enabled() && is_mutating_method(method) {
        merged.push((KURA_DRY_RUN_HEADER.to_string(), "validate".to_string()));
    }
    merged
}

/// Stored credentials for the CLI
#[derive(Debug, Serialize, Deserialize)]
pub struct StoredCredentials {
    pub api_url: String,
    pub access_token: String,
    pub refresh_token: String,
    pub expires_at: DateTime<Utc>,
}

#[derive(Deserialize)]
pub struct TokenResponse {
    pub access_token: String,
    pub refresh_token: String,
    pub expires_in: i64,
}

pub fn client() -> reqwest::Client {
    reqwest::Client::new()
}

fn build_api_url(
    api_url: &str,
    path: &str,
    query: &[(String, String)],
) -> Result<reqwest::Url, String> {
    let mut url = reqwest::Url::parse(&format!("{api_url}{path}"))
        .map_err(|e| format!("Invalid URL: {api_url}{path}: {e}"))?;
    if !query.is_empty() {
        let mut params = url.query_pairs_mut();
        for (key, value) in query {
            params.append_pair(key, value);
        }
    }
    Ok(url)
}

fn cli_install_channel() -> String {
    std::env::var("KURA_CLI_INSTALL_CHANNEL")
        .ok()
        .map(|value| value.trim().to_ascii_lowercase())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "cargo".to_string())
}

fn with_cli_client_headers(mut req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
    req = req.header(KURA_CLIENT_NAME_HEADER, KURA_CLI_CLIENT_NAME);
    req = req.header(KURA_CLIENT_VERSION_HEADER, env!("CARGO_PKG_VERSION"));
    req = req.header(KURA_CLIENT_INSTALL_CHANNEL_HEADER, cli_install_channel());
    if let Some(ack_header_value) = pending_notice_ack_header_value() {
        req = req.header(KURA_CLIENT_NOTICE_ACK_HEADER, ack_header_value);
    }
    req
}

fn parse_user_notice_ack_ids(body: &serde_json::Value) -> Vec<String> {
    body.get("user_notices")
        .and_then(|value| value.as_array())
        .map(|items| {
            items
                .iter()
                .filter_map(|item| item.as_object())
                .filter_map(|item| item.get("notice_id").and_then(|value| value.as_str()))
                .map(str::trim)
                .filter(|value| is_valid_notice_ack_id(value))
                .map(ToString::to_string)
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn queue_user_notice_acks(body: &serde_json::Value) {
    let notice_ids = parse_user_notice_ack_ids(body);
    if notice_ids.is_empty() {
        return;
    }
    let mut pending = PENDING_NOTICE_ACK_IDS
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    for notice_id in notice_ids {
        if pending.len() >= KURA_NOTICE_ACK_MAX_IDS {
            break;
        }
        pending.insert(notice_id);
    }
}

fn pending_notice_ack_header_value() -> Option<String> {
    let pending = PENDING_NOTICE_ACK_IDS
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    if pending.is_empty() {
        return None;
    }
    let value = pending
        .iter()
        .take(KURA_NOTICE_ACK_MAX_IDS)
        .cloned()
        .collect::<Vec<_>>()
        .join(",");
    if value.is_empty() { None } else { Some(value) }
}

fn is_valid_notice_ack_id(raw: &str) -> bool {
    let trimmed = raw.trim();
    if trimmed.is_empty() || trimmed.len() > 200 {
        return false;
    }
    trimmed
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_' | '-' | '.'))
}

fn extract_user_notice_lines(body: &serde_json::Value) -> Vec<String> {
    let notices = body
        .get("user_notices")
        .and_then(|value| value.as_array())
        .cloned()
        .unwrap_or_default();

    let mut lines = Vec::new();
    for notice in notices {
        let Some(obj) = notice.as_object() else {
            continue;
        };
        let message = obj
            .get("message_short")
            .and_then(|value| value.as_str())
            .map(str::trim)
            .filter(|value| !value.is_empty());
        let cmd = obj
            .get("upgrade_command")
            .and_then(|value| value.as_str())
            .map(str::trim)
            .filter(|value| !value.is_empty());
        let docs_hint = obj
            .get("docs_hint")
            .and_then(|value| value.as_str())
            .map(str::trim)
            .filter(|value| !value.is_empty());

        let mut line = String::from("[kura notice]");
        if let Some(message) = message {
            line.push(' ');
            line.push_str(message);
        }
        if let Some(cmd) = cmd {
            line.push_str(" Update: ");
            line.push_str(cmd);
        } else if let Some(docs_hint) = docs_hint {
            line.push(' ');
            line.push_str(docs_hint);
        }
        if line != "[kura notice]" {
            lines.push(line);
        }
    }
    lines
}

pub fn env_flag_enabled(name: &str) -> bool {
    std::env::var(name)
        .ok()
        .map(|value| {
            let normalized = value.trim().to_ascii_lowercase();
            matches!(normalized.as_str(), "1" | "true" | "yes" | "on")
        })
        .unwrap_or(false)
}

pub fn admin_surface_enabled() -> bool {
    env_flag_enabled("KURA_ENABLE_ADMIN_SURFACE")
}

pub fn is_admin_api_path(path: &str) -> bool {
    let trimmed = path.trim();
    if trimmed.is_empty() {
        return false;
    }

    let normalized = if trimmed.starts_with('/') {
        trimmed.to_ascii_lowercase()
    } else {
        format!("/{}", trimmed.to_ascii_lowercase())
    };

    normalized == "/v1/admin" || normalized.starts_with("/v1/admin/")
}

pub fn exit_error(message: &str, docs_hint: Option<&str>) -> ! {
    let mut err = json!({
        "error": "cli_error",
        "message": message
    });
    if let Some(hint) = docs_hint {
        err["docs_hint"] = json!(hint);
    }
    print_json_stderr(&err);
    std::process::exit(1);
}

pub fn config_path() -> std::path::PathBuf {
    let config_dir = dirs::config_dir()
        .unwrap_or_else(|| std::path::PathBuf::from("."))
        .join("kura");
    config_dir.join("config.json")
}

pub fn load_credentials() -> Option<StoredCredentials> {
    let path = config_path();
    let data = std::fs::read_to_string(&path).ok()?;
    serde_json::from_str(&data).ok()
}

pub fn save_credentials(creds: &StoredCredentials) -> Result<(), Box<dyn std::error::Error>> {
    let path = config_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    let data = serde_json::to_string_pretty(creds)?;

    // Write with restricted permissions (0o600)
    let mut file = std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .mode(0o600)
        .open(&path)?;
    file.write_all(data.as_bytes())?;

    Ok(())
}

/// Resolve a Bearer token for API requests (priority order):
/// 1. KURA_API_KEY env var
/// 2. ~/.config/kura/config.json (with auto-refresh)
/// 3. Error
pub async fn resolve_token(api_url: &str) -> Result<String, Box<dyn std::error::Error>> {
    // 1. Environment variable
    if let Ok(key) = std::env::var("KURA_API_KEY") {
        return Ok(key);
    }

    // 2. Stored credentials
    if let Some(creds) = load_credentials() {
        // Check if access token needs refresh (5-min buffer)
        let buffer = chrono::Duration::minutes(5);
        if Utc::now() + buffer >= creds.expires_at {
            // Try to refresh
            match refresh_stored_token(api_url, &creds).await {
                Ok(new_creds) => {
                    save_credentials(&new_creds)?;
                    return Ok(new_creds.access_token);
                }
                Err(_) => {
                    return Err(
                        "Access token expired and refresh failed. Run `kura login` again.".into(),
                    );
                }
            }
        }
        return Ok(creds.access_token);
    }

    Err("No credentials found. Run `kura login` or set KURA_API_KEY.".into())
}

async fn refresh_stored_token(
    api_url: &str,
    creds: &StoredCredentials,
) -> Result<StoredCredentials, Box<dyn std::error::Error>> {
    let resp = client()
        .post(format!("{api_url}/v1/auth/token"))
        .json(&json!({
            "grant_type": "refresh_token",
            "refresh_token": creds.refresh_token,
            "client_id": "kura-cli"
        }))
        .send()
        .await?;

    if !resp.status().is_success() {
        let body: serde_json::Value = resp.json().await?;
        return Err(format!("Token refresh failed: {}", body).into());
    }

    let token_resp: TokenResponse = resp.json().await?;
    Ok(StoredCredentials {
        api_url: creds.api_url.clone(),
        access_token: token_resp.access_token,
        refresh_token: token_resp.refresh_token,
        expires_at: Utc::now() + chrono::Duration::seconds(token_resp.expires_in),
    })
}

/// Execute an authenticated API request, print response, exit with structured code.
///
/// Exit codes: 0=success (2xx), 1=client error (4xx), 2=server error (5xx),
///             3=connection error, 4=usage error
pub async fn api_request(
    api_url: &str,
    method: reqwest::Method,
    path: &str,
    token: Option<&str>,
    body: Option<serde_json::Value>,
    query: &[(String, String)],
    extra_headers: &[(String, String)],
    raw: bool,
    include: bool,
) -> i32 {
    let url = match build_api_url(api_url, path, query) {
        Ok(url) => url,
        Err(message) => {
            let err = json!({
                "error": "cli_error",
                "message": message
            });
            print_json_stderr(&err);
            return 4;
        }
    };

    if dry_run_enabled() && is_mutating_method(&method) {
        return emit_dry_run_request(
            &method,
            api_url,
            path,
            token.is_some(),
            body.as_ref(),
            query,
            extra_headers,
            raw,
            None,
        );
    }

    let mut req = with_cli_client_headers(client().request(method, url));

    if let Some(t) = token {
        req = req.header("Authorization", format!("Bearer {t}"));
    }

    for (k, v) in extra_headers {
        req = req.header(k.as_str(), v.as_str());
    }

    if let Some(b) = body {
        req = req.json(&b);
    }

    let resp = match req.send().await {
        Ok(r) => r,
        Err(e) => {
            let err = json!({
                "error": "connection_error",
                "message": format!("{e}"),
                "docs_hint": "Is the API server running? Check KURA_API_URL."
            });
            print_json_stderr(&err);
            return 3;
        }
    };

    let status = resp.status().as_u16();
    let exit_code = match status {
        200..=299 => 0,
        400..=499 => 1,
        _ => 2,
    };
    // Collect headers before consuming response
    let headers: serde_json::Map<String, serde_json::Value> = if include {
        resp.headers()
            .iter()
            .map(|(k, v)| (k.to_string(), json!(v.to_str().unwrap_or("<binary>"))))
            .collect()
    } else {
        serde_json::Map::new()
    };

    let resp_body: serde_json::Value = match resp.bytes().await {
        Ok(bytes) => {
            if bytes.is_empty() {
                serde_json::Value::Null
            } else {
                serde_json::from_slice(&bytes).unwrap_or_else(|_| {
                    serde_json::Value::String(String::from_utf8_lossy(&bytes).to_string())
                })
            }
        }
        Err(e) => json!({"raw_error": format!("Failed to read response body: {e}")}),
    };

    let user_notice_lines = if exit_code == 0 {
        queue_user_notice_acks(&resp_body);
        extract_user_notice_lines(&resp_body)
    } else {
        Vec::new()
    };

    let output = if include {
        json!({
            "status": status,
            "headers": headers,
            "body": resp_body
        })
    } else {
        resp_body
    };

    let formatted = format_json_output(&output, raw);

    for line in user_notice_lines {
        emit_stderr_line(&line);
    }

    if exit_code == 0 {
        println!("{formatted}");
    } else {
        eprintln!("{formatted}");
    }

    exit_code
}

/// Execute a raw API request and return the response (no printing).
/// Used by doctor and other commands that need to inspect the response.
pub async fn raw_api_request(
    api_url: &str,
    method: reqwest::Method,
    path: &str,
    token: Option<&str>,
) -> Result<(u16, serde_json::Value), String> {
    raw_api_request_with_query(api_url, method, path, token, &[]).await
}

/// Execute a raw API request with query parameters and return the response (no printing).
pub async fn raw_api_request_with_query(
    api_url: &str,
    method: reqwest::Method,
    path: &str,
    token: Option<&str>,
    query: &[(String, String)],
) -> Result<(u16, serde_json::Value), String> {
    let url = build_api_url(api_url, path, query)?;

    let mut req = with_cli_client_headers(client().request(method, url));
    if let Some(t) = token {
        req = req.header("Authorization", format!("Bearer {t}"));
    }

    let resp = req.send().await.map_err(|e| format!("{e}"))?;
    let status = resp.status().as_u16();
    let body: serde_json::Value = resp
        .json()
        .await
        .unwrap_or(json!({"error": "non-json response"}));
    if (200..=299).contains(&status) {
        queue_user_notice_acks(&body);
    }

    Ok((status, body))
}

pub async fn raw_api_request_json(
    api_url: &str,
    method: reqwest::Method,
    path: &str,
    token: Option<&str>,
    body: Option<Value>,
    query: &[(String, String)],
    extra_headers: &[(String, String)],
) -> Result<RawApiResponse, String> {
    raw_api_request_json_with_options(
        api_url,
        method,
        path,
        token,
        body,
        query,
        extra_headers,
        false,
    )
    .await
}

pub async fn raw_api_request_json_with_options(
    api_url: &str,
    method: reqwest::Method,
    path: &str,
    token: Option<&str>,
    body: Option<Value>,
    query: &[(String, String)],
    extra_headers: &[(String, String)],
    enable_server_dry_run: bool,
) -> Result<RawApiResponse, String> {
    let url = build_api_url(api_url, path, query)?;
    let merged_headers =
        append_server_dry_run_header(extra_headers, enable_server_dry_run, &method);

    let mut req = with_cli_client_headers(client().request(method, url));
    if let Some(t) = token {
        req = req.header("Authorization", format!("Bearer {t}"));
    }

    for (k, v) in &merged_headers {
        req = req.header(k.as_str(), v.as_str());
    }

    if let Some(body) = body {
        req = req.json(&body);
    }

    let resp = req.send().await.map_err(|e| format!("{e}"))?;
    let status = resp.status().as_u16();
    let body: Value = resp
        .json()
        .await
        .unwrap_or_else(|_| json!({"error": "non-json response"}));
    if (200..=299).contains(&status) {
        queue_user_notice_acks(&body);
    }

    Ok(RawApiResponse { status, body })
}

/// Check if auth is configured (without making a request).
/// Returns (method_name, detail) or None.
pub fn check_auth_configured() -> Option<(&'static str, String)> {
    if let Ok(key) = std::env::var("KURA_API_KEY") {
        let prefix = if key.len() > 12 { &key[..12] } else { &key };
        return Some(("api_key (env)", format!("{prefix}...")));
    }

    if let Some(creds) = load_credentials() {
        let expired = chrono::Utc::now() >= creds.expires_at;
        let detail = if expired {
            format!("expired at {}", creds.expires_at)
        } else {
            format!("valid until {}", creds.expires_at)
        };
        return Some(("oauth_token (stored)", detail));
    }

    None
}

/// Read JSON from a file path or stdin (when path is "-").
pub fn read_json_from_file(path: &str) -> Result<serde_json::Value, String> {
    let raw = if path == "-" {
        let mut buf = String::new();
        std::io::stdin()
            .read_line(&mut buf)
            .map_err(|e| format!("Failed to read stdin: {e}"))?;
        // Read remaining lines too
        let mut rest = String::new();
        while std::io::stdin()
            .read_line(&mut rest)
            .map_err(|e| format!("Failed to read stdin: {e}"))?
            > 0
        {
            buf.push_str(&rest);
            rest.clear();
        }
        buf
    } else {
        std::fs::read_to_string(path).map_err(|e| format!("Failed to read file '{path}': {e}"))?
    };
    serde_json::from_str(&raw).map_err(|e| format!("Invalid JSON in '{path}': {e}"))
}

// Unix-specific imports for file permissions
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;

// No-op on non-unix (won't compile for Windows without this)
#[cfg(not(unix))]
trait OpenOptionsExt {
    fn mode(&mut self, _mode: u32) -> &mut Self;
}

#[cfg(not(unix))]
impl OpenOptionsExt for std::fs::OpenOptions {
    fn mode(&mut self, _mode: u32) -> &mut Self {
        self
    }
}

#[cfg(test)]
mod tests {
    use super::{
        CliOutputMode, CliRuntimeOptions, append_server_dry_run_header, extract_user_notice_lines,
        is_admin_api_path, parse_user_notice_ack_ids, pending_notice_ack_header_value,
        queue_user_notice_acks, set_cli_runtime_options,
    };
    use serde_json::json;

    #[test]
    fn admin_path_detection_matches_v1_admin_namespace_only() {
        assert!(is_admin_api_path("/v1/admin"));
        assert!(is_admin_api_path("/v1/admin/invites"));
        assert!(is_admin_api_path("v1/admin/security/kill-switch"));
        assert!(!is_admin_api_path("/v1/agent/context"));
        assert!(!is_admin_api_path("/health"));
    }

    #[test]
    fn extract_user_notice_lines_reads_message_and_upgrade_command() {
        let current_version = env!("CARGO_PKG_VERSION");
        let body = json!({
            "user_notices": [{
                "kind": "client_update",
                "message_short": format!("Kura CLI update available ({}).", current_version),
                "upgrade_command": "cargo install kura-cli --locked --force"
            }]
        });
        let lines = extract_user_notice_lines(&body);
        assert_eq!(lines.len(), 1);
        assert!(lines[0].contains("[kura notice]"));
        assert!(lines[0].contains("Kura CLI update available"));
        assert!(lines[0].contains("cargo install kura-cli --locked --force"));
    }

    #[test]
    fn extract_user_notice_lines_returns_empty_when_absent() {
        let lines = extract_user_notice_lines(&json!({"ok": true}));
        assert!(lines.is_empty());
    }

    #[test]
    fn parse_user_notice_ack_ids_collects_notice_ids() {
        let body = json!({
            "user_notices": [
                {"notice_id": "client_update:kura-cli:0.1.5"},
                {"notice_id": "client_update:kura-mcp:0.1.5"}
            ]
        });
        let ids = parse_user_notice_ack_ids(&body);
        assert_eq!(
            ids,
            vec![
                "client_update:kura-cli:0.1.5".to_string(),
                "client_update:kura-mcp:0.1.5".to_string()
            ]
        );
    }

    #[test]
    fn queue_user_notice_acks_makes_ack_header_available() {
        super::PENDING_NOTICE_ACK_IDS
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
            .clear();
        queue_user_notice_acks(&json!({
            "user_notices": [{"notice_id": "client_update:kura-cli:0.1.5"}]
        }));
        let ack_header = pending_notice_ack_header_value();
        assert_eq!(ack_header.as_deref(), Some("client_update:kura-cli:0.1.5"));
    }

    #[test]
    fn append_server_dry_run_header_only_marks_enabled_mutating_requests() {
        set_cli_runtime_options(CliRuntimeOptions {
            output_mode: CliOutputMode::Json,
            quiet_stderr: false,
            dry_run: true,
        });
        let post_headers = append_server_dry_run_header(&[], true, &reqwest::Method::POST);
        assert!(
            post_headers
                .iter()
                .any(|(key, value)| key == "x-kura-dry-run" && value == "validate")
        );

        let get_headers = append_server_dry_run_header(&[], true, &reqwest::Method::GET);
        assert!(!get_headers.iter().any(|(key, _)| key == "x-kura-dry-run"));

        let disabled_headers = append_server_dry_run_header(&[], false, &reqwest::Method::POST);
        assert!(
            !disabled_headers
                .iter()
                .any(|(key, _)| key == "x-kura-dry-run")
        );
        set_cli_runtime_options(CliRuntimeOptions::default());
    }
}
