//! End-to-end tests for subagent functionality over MCP
//!
//! These tests verify the complete integration between the skrills server,
//! subagent backends, and MCP protocol for subagent execution.

use rmcp::transport::TokioChildProcess;
use rmcp::{model::CallToolRequestParam, service::serve_client};
use serde_json::json;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
use tokio::sync::Mutex;
use tokio::time::{sleep, timeout, Duration, Instant};
use wiremock::{Mock, MockServer, ResponseTemplate};

static TEST_LOCK: Mutex<()> = Mutex::const_new(());

fn workspace_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("crate dir")
        .parent()
        .expect("workspace root")
        .to_path_buf()
}

fn target_dir() -> PathBuf {
    std::env::var("CARGO_TARGET_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| workspace_root().join("target"))
}

async fn ensure_skrills_binary() -> anyhow::Result<PathBuf> {
    let binary_path = target_dir().join("debug").join(if cfg!(windows) {
        "skrills.exe"
    } else {
        "skrills"
    });
    if binary_path.exists() {
        return Ok(binary_path);
    }

    let root = workspace_root();
    let cargo_home = std::env::var("CARGO_HOME")
        .unwrap_or_else(|_| root.join(".cargo-home").to_string_lossy().into_owned());

    let status = Command::new("cargo")
        .args(["build", "-p", "skrills"])
        .env("CARGO_HOME", cargo_home)
        .status()
        .await?;
    assert!(status.success(), "cargo build failed to produce skrills");

    Ok(binary_path)
}

async fn wait_for_run_succeeded(
    peer: &rmcp::service::Peer<rmcp::RoleClient>,
    run_id: &str,
    deadline: Duration,
) -> anyhow::Result<serde_json::Value> {
    let started = Instant::now();
    let mut last_state: Option<String> = None;
    let mut last_payload: Option<serde_json::Value> = None;

    loop {
        let status = peer
            .call_tool(CallToolRequestParam {
                name: "get-run-status".into(),
                arguments: Some(
                    json!({ "run_id": run_id })
                        .as_object()
                        .cloned()
                        .expect("run_id args should be object"),
                ),
            })
            .await?;
        let content = status.structured_content.unwrap_or(serde_json::Value::Null);
        last_payload.replace(content.clone());
        last_state.replace(
            content
                .get("status")
                .and_then(|s: &serde_json::Value| s.get("state"))
                .and_then(|s: &serde_json::Value| s.as_str())
                .unwrap_or_default()
                .to_string(),
        );

        if last_state.as_deref() == Some("Succeeded") {
            return Ok(last_payload.unwrap_or(serde_json::Value::Null));
        }

        if started.elapsed() >= deadline {
            anyhow::bail!(
                "run did not succeed within {:?}; last_state={:?} last_payload={}",
                deadline,
                last_state.unwrap_or_default(),
                last_payload.unwrap_or(serde_json::Value::Null)
            );
        }

        sleep(Duration::from_millis(100)).await;
    }
}

#[tokio::test]
async fn given_skrills_server_with_subagents_when_executing_run_subagent_then_completes_successfully(
) -> anyhow::Result<()> {
    let _guard = TEST_LOCK.lock().await;

    // GIVEN a running skrills server with subagent support
    // WHEN executing the run-subagent MCP tool
    // THEN it should route to the correct backend and complete successfully
    // Arrange
    let tmp = tempfile::tempdir()?;
    std::fs::create_dir_all(tmp.path().join(".codex/skills"))?;

    // Mock Codex/Claude HTTP endpoints to verify adapters honor config.
    let server = MockServer::start().await;
    let _codex_mock = Mock::given(wiremock::matchers::method("POST"))
        .and(wiremock::matchers::path("/v1/chat/completions"))
        .respond_with(
            ResponseTemplate::new(200)
                .set_body_json(json!({"choices": [{"message": {"content": "mock codex reply"}}]})),
        )
        .mount(&server)
        .await;
    let _claude_mock = Mock::given(wiremock::matchers::method("POST"))
        .and(wiremock::matchers::path("/v1/messages"))
        .respond_with(
            ResponseTemplate::new(200)
                .set_body_json(json!({"content": [{"text": "mock claude reply"}]})),
        )
        .mount(&server)
        .await;

    let binary_path = ensure_skrills_binary().await?;

    // Act
    let mut command = Command::new(&binary_path);
    command.arg("serve");
    command.env("HOME", tmp.path());
    command.env("SKRILLS_CODEX_API_KEY", "test-codex-key");
    command.env("SKRILLS_CODEX_BASE_URL", format!("{}/v1/", server.uri()));
    command.env("SKRILLS_CLAUDE_API_KEY", "test-claude-key");
    command.env("SKRILLS_CLAUDE_BASE_URL", format!("{}/v1/", server.uri()));
    command.env("SKRILLS_SUBAGENTS_DEFAULT_BACKEND", "codex");

    let (transport, stderr) = TokioChildProcess::builder(command)
        .stderr(Stdio::piped())
        .spawn()?;
    let mut stderr = stderr;

    let client = match timeout(Duration::from_secs(5), serve_client((), transport)).await {
        Ok(Ok(c)) => c,
        Ok(Err(e)) => {
            if let Some(mut err) = stderr.take() {
                let mut buf = String::new();
                let _: std::io::Result<usize> = err.read_to_string(&mut buf).await;
                panic!("serve_client failed: {e:?}\nstderr:\n{buf}");
            }
            return Err(e.into());
        }
        Err(_) => {
            if let Some(mut err) = stderr.take() {
                let mut buf = String::new();
                let _: std::io::Result<usize> = err.read_to_string(&mut buf).await;
                panic!("serve_client timed out\nstderr:\n{buf}");
            }
            anyhow::bail!("serve_client timed out")
        }
    };
    let peer = client.peer().clone();

    // Verify subagent tools are available
    let tools: Vec<rmcp::model::Tool> = peer.list_all_tools().await?;
    assert!(
        tools.iter().any(|t| t.name == "run-subagent"),
        "run-subagent tool should be available"
    );

    // Execute subagent with codex backend
    let args = json!({
        "prompt": "ping",
        "backend": "codex",
        "stream": false,
        "execution_mode": "api"
    });
    let result = peer
        .call_tool(CallToolRequestParam {
            name: "run-subagent".into(),
            arguments: args.as_object().cloned(),
        })
        .await?;
    let run_id = result
        .structured_content
        .as_ref()
        .and_then(|v: &serde_json::Value| v.get("run_id"))
        .and_then(|v: &serde_json::Value| v.as_str())
        .expect("run_id string")
        .to_string();

    let content = wait_for_run_succeeded(&peer, &run_id, Duration::from_secs(10)).await?;
    let events = content
        .get("events")
        .and_then(|e: &serde_json::Value| e.as_array())
        .cloned()
        .unwrap_or_default();
    assert!(!events.is_empty(), "expected streaming events");

    // Verify correct backend was called - wiremock automatically verifies when mounted
    // The test would fail if the endpoint wasn't called since the response would be missing

    client.cancel().await?;

    // Assert no errors in stderr
    if let Some(mut err) = stderr {
        let mut buf = String::new();
        let _: std::io::Result<usize> = err.read_to_string(&mut buf).await;
        assert!(
            !buf.to_lowercase().contains("error"),
            "Server should not log errors"
        );
    }
    Ok(())
}

#[tokio::test]
async fn given_server_with_multiple_backends_when_switching_default_then_routes_correctly(
) -> anyhow::Result<()> {
    let _guard = TEST_LOCK.lock().await;

    // GIVEN a skrills server with multiple backends configured
    // WHEN switching the default backend
    // THEN subsequent requests should route to the new default
    // Arrange
    let tmp = tempfile::tempdir()?;
    std::fs::create_dir_all(tmp.path().join(".codex/skills"))?;

    let server = MockServer::start().await;

    // Track calls to each backend
    let _codex_mock = Mock::given(wiremock::matchers::method("POST"))
        .and(wiremock::matchers::path("/v1/chat/completions"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "choices": [{"message": {"content": "codex response"}}]
        })))
        .mount(&server)
        .await;

    let _claude_mock = Mock::given(wiremock::matchers::method("POST"))
        .and(wiremock::matchers::path("/v1/messages"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "content": [{"text": "claude response"}]
        })))
        .mount(&server)
        .await;

    // Configure both backends
    let binary_path = ensure_skrills_binary().await?;

    // Act & Assert
    // Test with Codex as default
    let mut command = Command::new(&binary_path);
    command.arg("serve");
    command.env("HOME", tmp.path());
    command.env("SKRILLS_CODEX_API_KEY", "test-codex-key");
    command.env("SKRILLS_CODEX_BASE_URL", format!("{}/v1/", server.uri()));
    command.env("SKRILLS_CLAUDE_API_KEY", "test-claude-key");
    command.env("SKRILLS_CLAUDE_BASE_URL", format!("{}/v1/", server.uri()));
    command.env("SKRILLS_SUBAGENTS_DEFAULT_BACKEND", "codex");

    let (transport, _stderr) = TokioChildProcess::builder(command).spawn()?;

    let client = timeout(Duration::from_secs(5), serve_client((), transport)).await??;
    let peer = client.peer().clone();

    // Execute request without specifying backend (should use codex)
    let args = json!({
        "prompt": "test",
        "stream": false,
        "execution_mode": "api"
    });
    let result = peer
        .call_tool(CallToolRequestParam {
            name: "run-subagent".into(),
            arguments: args.as_object().cloned(),
        })
        .await?;

    // Wait for completion
    let run_id = result
        .structured_content
        .as_ref()
        .and_then(|v: &serde_json::Value| v.get("run_id"))
        .and_then(|v: &serde_json::Value| v.as_str())
        .expect("run_id string")
        .to_string();

    let _content = wait_for_run_succeeded(&peer, &run_id, Duration::from_secs(10)).await?;

    // Verify codex was called - wiremock automatically verifies when mounted
    // The test would fail if the endpoint wasn't called since the response would be missing

    client.cancel().await?;
    Ok(())
}

#[tokio::test]
async fn given_server_when_streaming_enabled_then_emits_events() -> anyhow::Result<()> {
    // GIVEN a running skrills server
    // WHEN executing a subagent with streaming enabled
    // THEN it should emit streaming events during execution
    let _guard = TEST_LOCK.lock().await;

    let tmp = tempfile::tempdir()?;
    std::fs::create_dir_all(tmp.path().join(".codex/skills"))?;

    let server = MockServer::start().await;
    let _codex_mock = Mock::given(wiremock::matchers::method("POST"))
        .and(wiremock::matchers::path("/v1/chat/completions"))
        .respond_with(
            ResponseTemplate::new(200)
                .set_body_json(json!({"choices": [{"message": {"content": "streaming reply"}}]})),
        )
        .mount(&server)
        .await;

    let binary_path = ensure_skrills_binary().await?;

    let mut command = Command::new(&binary_path);
    command.arg("serve");
    command.env("HOME", tmp.path());
    command.env("SKRILLS_CODEX_API_KEY", "test-codex-key");
    command.env("SKRILLS_CODEX_BASE_URL", format!("{}/v1/", server.uri()));
    command.env("SKRILLS_SUBAGENTS_DEFAULT_BACKEND", "codex");

    let (transport, _stderr) = TokioChildProcess::builder(command).spawn()?;
    let client = timeout(Duration::from_secs(5), serve_client((), transport)).await??;
    let peer = client.peer().clone();

    let args = json!({
        "prompt": "stream",
        "backend": "codex",
        "stream": true,
        "execution_mode": "api"
    });
    let result = peer
        .call_tool(CallToolRequestParam {
            name: "run-subagent".into(),
            arguments: args.as_object().cloned(),
        })
        .await?;

    let run_id = result
        .structured_content
        .as_ref()
        .and_then(|v: &serde_json::Value| v.get("run_id"))
        .and_then(|v: &serde_json::Value| v.as_str())
        .expect("run_id string")
        .to_string();

    let _content = wait_for_run_succeeded(&peer, &run_id, Duration::from_secs(10)).await?;

    let events_result = peer
        .call_tool(CallToolRequestParam {
            name: "get-run-events".into(),
            arguments: Some(
                json!({ "run_id": run_id })
                    .as_object()
                    .cloned()
                    .expect("get-run-events args should be object"),
            ),
        })
        .await?;

    let events = events_result
        .structured_content
        .as_ref()
        .and_then(|v: &serde_json::Value| v.get("events"))
        .and_then(|v: &serde_json::Value| v.as_array())
        .cloned()
        .unwrap_or_default();

    assert!(
        events
            .iter()
            .any(|e| e.get("kind").and_then(|v| v.as_str()) == Some("stream")),
        "expected at least one stream event"
    );

    client.cancel().await?;
    Ok(())
}
