//! Live end-to-end routing smoke tests against a real backend.
//!
//! These tests are intentionally `#[ignore]` because they require:
//! - a reachable backend URL
//! - a valid user session JWT
//! - real network I/O and side effects
//!
//! Run manually:
//! OPENHUMAN_LIVE_API_URL="https://<your-backend>" \
//! OPENHUMAN_LIVE_TOKEN="<jwt>" \
//! OPENHUMAN_LIVE_USER_ID="<user-id>" \
//! cargo test --test live_routing_e2e -- --ignored --nocapture

use std::path::Path;
use std::sync::{Mutex, OnceLock};
use std::time::Duration;

use futures_util::StreamExt;
use serde_json::{json, Value};
use tempfile::tempdir;
use tokio::time::timeout;

use openhuman_core::core::auth::{init_rpc_token, CORE_TOKEN_ENV_VAR};
use openhuman_core::core::jsonrpc::build_core_http_router;

static LIVE_E2E_ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
static LIVE_RPC_AUTH_INIT: OnceLock<()> = OnceLock::new();
const TEST_RPC_TOKEN: &str = "live-routing-e2e-local-token";

struct EnvVarGuard {
    key: &'static str,
    old: Option<String>,
}

impl EnvVarGuard {
    fn set_to_path(key: &'static str, path: &Path) -> Self {
        let old = std::env::var(key).ok();
        // SAFETY: EnvVarGuard is only used in tests that first acquire
        // live_e2e_env_lock(), which serializes process-global env mutations.
        unsafe { std::env::set_var(key, path.as_os_str()) };
        Self { key, old }
    }
}

impl Drop for EnvVarGuard {
    fn drop(&mut self) {
        match &self.old {
            // SAFETY: See EnvVarGuard::set_to_path; teardown runs under the same
            // live_e2e_env_lock() critical section as setup.
            Some(v) => unsafe { std::env::set_var(self.key, v) },
            // SAFETY: Guarded by live_e2e_env_lock(), preventing concurrent env access.
            None => unsafe { std::env::remove_var(self.key) },
        }
    }
}

fn live_e2e_env_lock() -> std::sync::MutexGuard<'static, ()> {
    let mutex = LIVE_E2E_ENV_LOCK.get_or_init(|| Mutex::new(()));
    match mutex.lock() {
        Ok(guard) => guard,
        Err(poisoned) => poisoned.into_inner(),
    }
}

fn required_env(name: &str) -> String {
    std::env::var(name).unwrap_or_else(|_| panic!("missing required env var: {name}"))
}

fn write_live_config(openhuman_dir: &Path, api_origin: &str) {
    let cfg = format!(
        r#"api_url = "{api_origin}"
default_model = "reasoning-v1"
default_temperature = 0.7
chat_onboarding_completed = true

[secrets]
encrypt = false
"#
    );

    fn write_config_file(config_dir: &Path, cfg: &str) {
        std::fs::create_dir_all(config_dir).expect("mkdir openhuman");
        let path = config_dir.join("config.toml");
        std::fs::write(&path, cfg).expect("write config");
    }

    write_config_file(openhuman_dir, &cfg);
    // Match runtime config resolution order used during pre-login auth flows.
    // If we seed ~/.openhuman, also seed ~/.openhuman/users/local.
    if openhuman_dir
        .file_name()
        .is_some_and(|name| name == std::ffi::OsStr::new(".openhuman"))
    {
        write_config_file(&openhuman_dir.join("users").join("local"), &cfg);
    }
}

async fn post_json_rpc(rpc_base: &str, id: i64, method: &str, params: Value) -> Value {
    let client = reqwest::Client::new();
    let resp = client
        .post(format!("{rpc_base}/rpc"))
        .header("Authorization", format!("Bearer {TEST_RPC_TOKEN}"))
        .json(&json!({
            "jsonrpc": "2.0",
            "id": id,
            "method": method,
            "params": params
        }))
        .send()
        .await
        .expect("rpc request");

    resp.json::<Value>().await.expect("rpc json body")
}

async fn read_sse_event_by_types(events_url: &str, target_events: &[&str]) -> Value {
    const CHUNK_TIMEOUT_SECS: u64 = 15;

    let client = reqwest::Client::new();
    let resp = client
        .get(events_url)
        .header("Authorization", format!("Bearer {TEST_RPC_TOKEN}"))
        .send()
        .await
        .unwrap_or_else(|e| panic!("open SSE stream failed: {e}"));
    let mut stream = resp.bytes_stream();

    let mut buffer = String::new();
    loop {
        let chunk = match timeout(Duration::from_secs(CHUNK_TIMEOUT_SECS), stream.next()).await {
            Ok(Some(Ok(bytes))) => bytes,
            Ok(Some(Err(e))) => panic!("SSE stream chunk error: {e}"),
            Ok(None) => break,
            Err(_) => continue,
        };
        let text = String::from_utf8_lossy(&chunk);
        buffer.push_str(&text);

        while let Some(split_idx) = buffer.find("\n\n") {
            let raw_event = buffer[..split_idx].to_string();
            buffer = buffer[split_idx + 2..].to_string();

            let mut data_lines = Vec::new();
            for line in raw_event.lines() {
                if let Some(data) = line.strip_prefix("data:") {
                    data_lines.push(data.trim_start());
                }
            }
            if !data_lines.is_empty() {
                let payload = data_lines.join("\n");
                let value: Value = serde_json::from_str(&payload)
                    .unwrap_or_else(|e| panic!("invalid sse data json: {e}"));
                if let Some(event_type) = value.get("event").and_then(Value::as_str) {
                    if target_events.iter().any(|t| *t == event_type) {
                        return value;
                    }
                }
            }
        }
    }
    panic!("SSE stream ended before receiving any target event: {target_events:?}");
}

fn assert_no_jsonrpc_error<'a>(v: &'a Value, context: &str) -> &'a Value {
    if let Some(err) = v.get("error") {
        panic!("{context}: JSON-RPC error: {err}");
    }
    v.get("result")
        .unwrap_or_else(|| panic!("{context}: missing result: {v}"))
}

async fn serve_rpc() -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) {
    ensure_test_rpc_auth();
    let app = build_core_http_router(false);
    let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
        .await
        .expect("bind ephemeral listener");
    let addr = listener.local_addr().expect("listener addr");
    let join = tokio::spawn(async move {
        axum::serve(listener, app.into_make_service())
            .await
            .expect("rpc server should run");
    });
    (addr, join)
}

fn ensure_test_rpc_auth() {
    LIVE_RPC_AUTH_INIT.get_or_init(|| {
        // SAFETY: set_var is inside get_or_init so it runs exactly once across
        // all test threads. Rust 1.81+ requires unsafe for set_var in
        // multi-threaded contexts; the OnceLock guard limits the mutation to a
        // single call at init time, before any concurrent env reads occur.
        unsafe { std::env::set_var(CORE_TOKEN_ENV_VAR, TEST_RPC_TOKEN) };
        let token_dir = std::env::temp_dir().join("openhuman-live-routing-e2e-auth");
        init_rpc_token(&token_dir).expect("init rpc auth token for live_routing_e2e");
    });
}

#[tokio::test]
#[ignore = "requires live backend URL + valid token"]
async fn live_channel_web_chat_routing_cases_trigger_real_backend() {
    let _env_lock = live_e2e_env_lock();

    let api_url = required_env("OPENHUMAN_LIVE_API_URL");
    let token = required_env("OPENHUMAN_LIVE_TOKEN");
    let user_id = required_env("OPENHUMAN_LIVE_USER_ID");

    let tmp = tempdir().expect("tempdir");
    let home = tmp.path();
    let openhuman_home = home.join(".openhuman");
    let _home_guard = EnvVarGuard::set_to_path("HOME", home);

    write_live_config(&openhuman_home, &api_url);
    write_live_config(&openhuman_home.join("users").join(&user_id), &api_url);

    let (rpc_addr, rpc_join) = serve_rpc().await;
    let rpc_base = format!("http://{}", rpc_addr);
    tokio::time::sleep(Duration::from_millis(100)).await;

    let store = post_json_rpc(
        &rpc_base,
        1,
        "openhuman.auth_store_session",
        json!({
            "token": token,
            "user_id": user_id
        }),
    )
    .await;
    assert_no_jsonrpc_error(&store, "store_session");

    let routing_cases = [
        "hint:reasoning",
        "hint:agentic",
        "hint:coding",
        "reasoning-v1",
    ];

    for (idx, model_override) in routing_cases.iter().enumerate() {
        let client_id = format!("live-routing-client-{idx}");
        let thread_id = format!("live-routing-thread-{idx}");
        let events_url = format!("{}/events?client_id={}", rpc_base, client_id);
        let sse_task = tokio::spawn(async move {
            read_sse_event_by_types(&events_url, &["chat_done", "chat_error"]).await
        });

        let web_chat = post_json_rpc(
            &rpc_base,
            10 + idx as i64,
            "openhuman.channel_web_chat",
            json!({
                "client_id": client_id,
                "thread_id": thread_id,
                "message": format!("live routing case: {model_override}"),
                "model_override": model_override,
            }),
        )
        .await;
        let web_chat_result = assert_no_jsonrpc_error(&web_chat, "channel_web_chat");
        assert_eq!(
            web_chat_result
                .get("result")
                .and_then(|v| v.get("accepted")),
            Some(&json!(true)),
            "request not accepted for case {model_override}"
        );

        let sse_event = timeout(Duration::from_secs(120), sse_task)
            .await
            .unwrap_or_else(|_| {
                panic!("timed out waiting for terminal SSE event for case {model_override}")
            })
            .expect("sse task join should succeed");
        let event_type = sse_event
            .get("event")
            .and_then(Value::as_str)
            .unwrap_or("unknown");
        assert_eq!(
            event_type, "chat_done",
            "received terminal SSE event '{event_type}' for case {model_override}: {sse_event}"
        );
        println!("live case '{model_override}' completed with chat_done");
    }

    rpc_join.abort();
}
