#![cfg(feature = "integration-real-tests")]
#![allow(
    unused_mut,
    clippy::redundant_clone,
    clippy::unwrap_used,
    clippy::expect_used
)]

use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use meerkat::{
    AgentFactory, Config, FactoryAgentBuilder, LlmClient, MemoryStore, PersistenceBundle,
    PersistentSessionService, SessionStore,
};
use meerkat_client::AnthropicClient;
use meerkat_core::MemoryConfigStore;
#[cfg(feature = "mob")]
use meerkat_mob_mcp::wire_mob_tools;
use meerkat_rest::{AppState, router};
use meerkat_store::StoreAdapter;
use serde_json::{Value, json};
use std::sync::Arc;
use tempfile::TempDir;
use tokio::time::{Duration, timeout};
use tower::ServiceExt;

fn smoke_model() -> String {
    std::env::var("SMOKE_MODEL").unwrap_or_else(|_| "claude-sonnet-4-5".to_string())
}

/// Returns the API key if available, or None to skip the test gracefully.
fn anthropic_api_key() -> Option<String> {
    std::env::var("ANTHROPIC_API_KEY")
        .ok()
        .or_else(|| std::env::var("RKAT_ANTHROPIC_API_KEY").ok())
}

fn build_app_state(client: Arc<dyn LlmClient>) -> (AppState, axum::Router) {
    let temp_dir = TempDir::new().expect("temp dir");
    // Leak to keep the dir alive for the full test.
    let temp_dir = Box::leak(Box::new(temp_dir));
    let project_root = temp_dir.path().join("project");
    std::fs::create_dir_all(project_root.join(".rkat")).expect("create .rkat");

    let mut config = Config::default();
    config.agent.max_tokens_per_turn = 256;
    config.agent.model = smoke_model();
    let config_store = MemoryConfigStore::new(config.clone());

    let store_path = temp_dir.path().join("sessions");
    let (event_tx, _) = tokio::sync::broadcast::channel(16);

    let store: Arc<dyn SessionStore> = Arc::new(MemoryStore::new());

    let factory = AgentFactory::new(store_path.clone())
        .builtins(false)
        .shell(false)
        .project_root(project_root.clone());
    let provider_registry = factory.provider_runtime_registry();
    let mut builder = FactoryAgentBuilder::new(factory, config.clone());
    builder.default_llm_client = Some(client.clone());
    let persistence =
        PersistenceBundle::new(store, None, Arc::new(meerkat_store::MemoryBlobStore::new()));
    let runtime_adapter = persistence.runtime_adapter();
    let workgraph_store = persistence.workgraph_store();
    builder.default_session_store = Some(Arc::new(StoreAdapter::new(persistence.session_store())));
    #[cfg(feature = "mob")]
    let builder_mob_tools_slot = Arc::clone(&builder.default_mob_tools);
    let (session_store_inner, runtime_store, blob_store) = persistence.into_parts();
    let mut session_service =
        PersistentSessionService::new(builder, 100, session_store_inner, runtime_store, blob_store);
    let session_service = Arc::new(session_service);
    #[cfg(feature = "mob")]
    let mob_state = wire_mob_tools(
        &builder_mob_tools_slot,
        session_service.clone(),
        Some(runtime_adapter.clone()),
        None,
    );
    let config_store_arc: Arc<dyn meerkat_core::ConfigStore> = Arc::new(config_store);
    let config_runtime = Arc::new(meerkat_core::ConfigRuntime::new(
        Arc::clone(&config_store_arc),
        store_path.join("config_state.json"),
    ));

    let state = AppState {
        store_path: store_path.clone(),
        default_model: config.agent.model.clone().into(),
        max_tokens: config.agent.max_tokens_per_turn,
        rest_host: config.rest.host.clone().into(),
        rest_port: config.rest.port,
        enable_builtins: false,
        enable_shell: false,
        project_root: Some(project_root),
        context_root: None,
        user_config_root: None,
        llm_client_override: Some(client),
        config_store: config_store_arc,
        event_tx,
        session_service,
        schedule_service: meerkat::ScheduleService::new(Arc::new(
            meerkat::MemoryScheduleStore::default(),
        )),
        workgraph_service: meerkat::WorkGraphService::with_scope(
            workgraph_store,
            "test-realm",
            meerkat::WorkNamespace::default(),
        ),
        webhook_auth: meerkat_rest::webhook::WebhookAuth::None,
        realm: meerkat_core::RealmId::parse("test-realm").expect("valid realm"),
        instance_id: None,
        backend: "sqlite".to_string(),
        resolved_paths: meerkat_core::ConfigResolvedPaths {
            root: store_path.display().to_string(),
            manifest_path: String::new(),
            config_path: String::new(),
            sessions_sqlite_path: None,
            sessions_jsonl_dir: String::new(),
        },
        expose_paths: false,
        config_runtime,
        realm_lease: Arc::new(tokio::sync::Mutex::new(None)),
        skill_runtime: None,
        runtime_adapter: runtime_adapter.clone(),
        runtime_pre_admissions: meerkat_rest::default_rest_runtime_pre_admissions(),
        runtime_registration_locks: meerkat_rest::default_rest_runtime_registration_locks(),
        schedule_host: Arc::default(),
        request_executor: std::sync::Arc::new(meerkat::surface::SurfaceRequestExecutor::new(
            std::time::Duration::from_secs(5),
        )),
        #[cfg(feature = "mob")]
        mob_state,
        #[cfg(feature = "mcp")]
        mcp_sessions: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
        token_store: Arc::new(meerkat_providers::auth_store::EphemeralTokenStore::new()),
        auth_lease: Arc::new(meerkat_runtime::RuntimeAuthLeaseHandle::new()),
        provider_registry,
    };

    let app = router(state.clone());
    (state, app)
}

/// Scenario 25: Full REST session lifecycle with a real Anthropic LLM.
///
/// 1. POST /sessions — create session with identity prompt
/// 2. POST /sessions/{id}/messages — ask recall question
/// 3. GET /sessions/{id} — verify message count
/// 4. GET /sessions — verify session appears in list
/// 5. DELETE /sessions/{id} — archive the session
/// 6. GET /sessions/{id} — confirm 404
#[tokio::test]
#[ignore = "lane:e2e-live"]
async fn e2e_scenario_25_rest_full_lifecycle() {
    let api_key = match anthropic_api_key() {
        Some(k) => k,
        None => {
            eprintln!("SKIP: ANTHROPIC_API_KEY not set");
            return;
        }
    };

    let client: Arc<dyn LlmClient> =
        Arc::new(AnthropicClient::new(api_key).expect("build AnthropicClient"));

    let (_state, app) = build_app_state(client);

    // ── Step 1: Create session ──────────────────────────────────────────
    let create_payload = json!({
        "prompt": "I am RestBot. My favorite number is 42. Reply briefly.",
        "model": smoke_model(),
        "max_tokens": 256
    });
    let req = Request::builder()
        .method("POST")
        .uri("/sessions")
        .header("content-type", "application/json")
        .body(Body::from(serde_json::to_vec(&create_payload).unwrap()))
        .unwrap();

    let resp = timeout(Duration::from_secs(120), app.clone().oneshot(req))
        .await
        .expect("create session timed out")
        .expect("create session request failed");
    assert_eq!(
        resp.status(),
        StatusCode::OK,
        "POST /sessions should return 200"
    );

    let body = resp.into_body().collect().await.unwrap().to_bytes();
    let create_json: Value = serde_json::from_slice(&body).expect("parse create response");
    let session_id = create_json["session_id"]
        .as_str()
        .expect("response must contain session_id")
        .to_string();
    assert!(
        !session_id.is_empty(),
        "session_id must be a non-empty string"
    );

    // ── Step 2: Continue session with recall question ───────────────────
    let continue_payload = json!({
        "session_id": session_id,
        "prompt": "What is my name and favorite number? Reply in one sentence."
    });
    let req = Request::builder()
        .method("POST")
        .uri(format!("/sessions/{session_id}/messages"))
        .header("content-type", "application/json")
        .body(Body::from(serde_json::to_vec(&continue_payload).unwrap()))
        .unwrap();

    let resp = timeout(Duration::from_secs(120), app.clone().oneshot(req))
        .await
        .expect("continue session timed out")
        .expect("continue session request failed");
    assert_eq!(
        resp.status(),
        StatusCode::OK,
        "POST /sessions/{{id}}/messages should return 200"
    );

    let body = resp.into_body().collect().await.unwrap().to_bytes();
    let continue_json: Value = serde_json::from_slice(&body).expect("parse continue response");
    let text = continue_json["text"]
        .as_str()
        .expect("continue response must have text field");
    let text_lower = text.to_lowercase();
    assert!(
        text_lower.contains("restbot") || text_lower.contains("rest bot"),
        "LLM response should mention 'RestBot', got: {text}"
    );
    assert!(
        text_lower.contains("42"),
        "LLM response should mention '42', got: {text}"
    );

    // ── Step 3: GET session details ─────────────────────────────────────
    let req = Request::builder()
        .method("GET")
        .uri(format!("/sessions/{session_id}"))
        .body(Body::empty())
        .unwrap();

    let resp = timeout(Duration::from_secs(120), app.clone().oneshot(req))
        .await
        .expect("get session timed out")
        .expect("get session request failed");
    assert_eq!(
        resp.status(),
        StatusCode::OK,
        "GET /sessions/{{id}} should return 200"
    );

    let body = resp.into_body().collect().await.unwrap().to_bytes();
    let details_json: Value = serde_json::from_slice(&body).expect("parse details response");
    let message_count = details_json["message_count"]
        .as_u64()
        .expect("details must have message_count");
    // 2 user messages + 2 assistant responses = at least 4
    assert!(
        message_count >= 4,
        "expected at least 4 messages, got {message_count}"
    );

    // ── Step 4: GET sessions list ───────────────────────────────────────
    let req = Request::builder()
        .method("GET")
        .uri("/sessions")
        .body(Body::empty())
        .unwrap();

    let resp = timeout(Duration::from_secs(120), app.clone().oneshot(req))
        .await
        .expect("list sessions timed out")
        .expect("list sessions request failed");
    assert_eq!(
        resp.status(),
        StatusCode::OK,
        "GET /sessions should return 200"
    );

    let body = resp.into_body().collect().await.unwrap().to_bytes();
    let list_json: Value = serde_json::from_slice(&body).expect("parse list response");
    let sessions = list_json["sessions"]
        .as_array()
        .expect("list response must have sessions array");
    let found = sessions
        .iter()
        .any(|s| s["session_id"].as_str() == Some(&session_id));
    assert!(found, "session list must contain our session {session_id}");

    // ── Step 5: DELETE (archive) session ────────────────────────────────
    let req = Request::builder()
        .method("DELETE")
        .uri(format!("/sessions/{session_id}"))
        .body(Body::empty())
        .unwrap();

    let resp = timeout(Duration::from_secs(120), app.clone().oneshot(req))
        .await
        .expect("delete session timed out")
        .expect("delete session request failed");
    assert_eq!(
        resp.status(),
        StatusCode::OK,
        "DELETE /sessions/{{id}} should return 200"
    );

    // ── Step 6: GET archived session — PersistentSessionService keeps archived
    // sessions readable, so GET may return 200 (archived view) or 404.
    // Both are valid — the test pins whichever the current implementation does.
    let req = Request::builder()
        .method("GET")
        .uri(format!("/sessions/{session_id}"))
        .body(Body::empty())
        .unwrap();

    let resp = timeout(Duration::from_secs(120), app.clone().oneshot(req))
        .await
        .expect("get archived session timed out")
        .expect("get archived session request failed");
    let status = resp.status();
    assert!(
        status == StatusCode::OK || status == StatusCode::NOT_FOUND || status.is_client_error(),
        "GET archived session should return 200 (archived view) or 404, got {status}"
    );
}
