use std::sync::Arc;

use axum::body::Body;
use axum::http::{Request, StatusCode};
use rmcp::transport::{StreamableHttpServerConfig, StreamableHttpService};
use serde_json::json;

type LocalSessionManager =
    rmcp::transport::streamable_http_server::session::local::LocalSessionManager;

fn jsonrpc_call(tool: &str, arguments: &serde_json::Value) -> String {
    json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": tool,
            "arguments": arguments
        }
    })
    .to_string()
}

async fn call_tool_json(
    svc: &StreamableHttpService<lean_ctx::tools::LeanCtxServer, LocalSessionManager>,
    tool: &str,
    arguments: serde_json::Value,
) -> serde_json::Value {
    let req = Request::builder()
        .method("POST")
        .uri("/")
        .header("Host", "localhost")
        .header("Accept", "application/json, text/event-stream")
        .header("Content-Type", "application/json")
        .body(Body::from(jsonrpc_call(tool, &arguments)))
        .expect("request");

    let resp = svc.handle(req).await;
    assert_eq!(resp.status(), StatusCode::OK);

    let bytes = axum::body::to_bytes(Body::new(resp.into_body()), usize::MAX)
        .await
        .expect("read body");
    let v: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
    assert!(
        v.get("error").is_none(),
        "expected jsonrpc result, got error: {v}"
    );
    v
}

/// Poll `cond` up to ~10s. The memory stores are persisted on a background
/// thread (the consolidation pipeline), so asserting immediately after a tool
/// call returns raced the writer and made this test flaky under full-suite load
/// (#215). Polling keeps the assertion strict (it still fails if the artifact
/// never lands) while tolerating scheduling latency.
async fn wait_until(mut cond: impl FnMut() -> bool) -> bool {
    for _ in 0..200 {
        if cond() {
            return true;
        }
        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
    }
    cond()
}

/// True once `dir` exists and contains at least one entry. Tolerates the dir not
/// existing yet (the background writer may not have created it).
fn dir_has_entry(dir: &std::path::Path) -> bool {
    std::fs::read_dir(dir).is_ok_and(|mut it| it.next().is_some())
}

#[allow(clippy::await_holding_lock)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn memory_benchmark_suite_persists_core_artifacts() {
    let _lock = lean_ctx::core::data_dir::test_env_lock();
    let data_dir = tempfile::tempdir().expect("data dir");
    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::set_var("LEAN_CTX_DATA_DIR", data_dir.path()) };

    let dir = tempfile::tempdir().expect("project dir");
    // Ensure project-root detection can anchor on a marker.
    std::fs::create_dir_all(dir.path().join(".git")).expect("create .git marker");
    let file_path = dir.path().join("a.txt");
    std::fs::write(&file_path, "hello\n").expect("write file");

    let root = lean_ctx::core::pathutil::safe_canonicalize_or_self(dir.path());
    let root_str = root.to_string_lossy().to_string();
    let base = lean_ctx::tools::LeanCtxServer::new_with_project_root(Some(&root_str));

    let service_factory = move || Ok(base.clone());
    let cfg = StreamableHttpServerConfig::default()
        .with_stateful_mode(false)
        .with_json_response(true);
    let svc = StreamableHttpService::new(
        service_factory,
        Arc::new(LocalSessionManager::default()),
        cfg,
    );

    // 1) Establish project context (tool pipeline).
    let _ = call_tool_json(
        &svc,
        "ctx_read",
        json!({
            "path": file_path.to_string_lossy().to_string(),
            "mode": "full"
        }),
    )
    .await;

    // 2) Knowledge: remember + feedback + relations.
    let _ = call_tool_json(
        &svc,
        "ctx_knowledge",
        json!({
            "action": "remember",
            "category": "arch",
            "key": "db",
            "value": "MySQL",
            "confidence": 0.9
        }),
    )
    .await;
    let _ = call_tool_json(
        &svc,
        "ctx_knowledge",
        json!({
            "action": "remember",
            "category": "arch",
            "key": "cache",
            "value": "Redis",
            "confidence": 0.9
        }),
    )
    .await;
    let _ = call_tool_json(
        &svc,
        "ctx_knowledge",
        json!({
            "action": "feedback",
            "category": "arch",
            "key": "db",
            "value": "up"
        }),
    )
    .await;
    let relate_resp = call_tool_json(
        &svc,
        "ctx_knowledge",
        json!({
            "action": "relate",
            "category": "arch",
            "key": "db",
            "value": "depends_on",
            "query": "arch/cache"
        }),
    )
    .await;

    // 3) Episodic + procedural memory persistence.
    let _ = call_tool_json(
        &svc,
        "ctx_session",
        json!({
            "action": "episodes",
            "value": "record"
        }),
    )
    .await;
    let _ = call_tool_json(
        &svc,
        "ctx_session",
        json!({
            "action": "procedures",
            "value": "detect"
        }),
    )
    .await;

    // 4) Assert artifacts exist on disk. Persistence runs on a background thread,
    //    so poll with a bounded timeout rather than asserting immediately (#215).
    let knowledge = lean_ctx::core::knowledge::ProjectKnowledge::load_or_create(&root_str);
    let knowledge_dir = data_dir
        .path()
        .join("knowledge")
        .join(&knowledge.project_hash);

    assert!(
        wait_until(|| knowledge_dir.join("knowledge.json").exists()).await,
        "expected knowledge.json to exist"
    );
    assert!(
        wait_until(|| knowledge_dir.join("relations.json").exists()).await,
        "expected relations.json to exist (relate said: {relate_resp})"
    );

    let edge_present = wait_until(|| {
        lean_ctx::core::knowledge_relations::KnowledgeRelationGraph::load(&knowledge.project_hash)
            .is_some_and(|graph| {
                graph.edges.iter().any(|e| {
                    e.from.category == "arch"
                        && e.from.key == "db"
                        && e.to.category == "arch"
                        && e.to.key == "cache"
                })
            })
    })
    .await;
    assert!(edge_present, "expected at least one relation edge");

    let episodes_dir = data_dir.path().join("memory").join("episodes");
    assert!(
        wait_until(|| dir_has_entry(&episodes_dir)).await,
        "expected at least one episodic store file"
    );

    let procs_dir = data_dir.path().join("memory").join("procedures");
    assert!(
        wait_until(|| dir_has_entry(&procs_dir)).await,
        "expected at least one procedural store file"
    );

    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::remove_var("LEAN_CTX_DATA_DIR") };
}
