//! End-to-end tests: spawn the MCP server as a child process and communicate
//! via stdio JSON-RPC.

use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::time::Duration;

/// Spawn `python-exec-mcp --mcp` and return (child, stdin, stdout_reader).
fn spawn_server() -> (
    std::process::Child,
    std::process::ChildStdin,
    BufReader<std::process::ChildStdout>,
) {
    let bin = env!("CARGO_BIN_EXE_python-exec-mcp");
    let mut child = Command::new(bin)
        .arg("--mcp")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .expect("failed to spawn server");

    let stdin = child.stdin.take().unwrap();
    let stdout = BufReader::new(child.stdout.take().unwrap());
    (child, stdin, stdout)
}

fn send(stdin: &mut std::process::ChildStdin, msg: &str) {
    writeln!(stdin, "{msg}").unwrap();
    stdin.flush().unwrap();
}

fn recv(reader: &mut BufReader<std::process::ChildStdout>) -> serde_json::Value {
    let mut line = String::new();
    reader.read_line(&mut line).unwrap();
    serde_json::from_str(&line).unwrap_or_else(|e| panic!("invalid JSON: {e}\nraw: {line}"))
}

fn initialize(
    stdin: &mut std::process::ChildStdin,
    reader: &mut BufReader<std::process::ChildStdout>,
) {
    send(
        stdin,
        r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}"#,
    );
    let resp = recv(reader);
    assert_eq!(resp["id"], 1);
    assert!(resp["result"]["capabilities"]["tools"].is_object());

    send(
        stdin,
        r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
    );
    // notification — no response
    std::thread::sleep(Duration::from_millis(100));
}

fn call_tool(
    stdin: &mut std::process::ChildStdin,
    reader: &mut BufReader<std::process::ChildStdout>,
    id: u64,
    tool: &str,
    args: serde_json::Value,
) -> serde_json::Value {
    let req = serde_json::json!({
        "jsonrpc": "2.0",
        "id": id,
        "method": "tools/call",
        "params": {
            "name": tool,
            "arguments": args,
        }
    });
    send(stdin, &req.to_string());
    let resp = recv(reader);
    assert_eq!(resp["id"], id);
    resp
}

fn extract_text(resp: &serde_json::Value) -> String {
    resp["result"]["content"][0]["text"]
        .as_str()
        .unwrap_or("")
        .to_string()
}

// =============================================================================
// Tests
// =============================================================================

#[test]
fn e2e_initialize() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);
    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_session_start() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    let resp = call_tool(
        &mut stdin,
        &mut reader,
        2,
        "session_start",
        serde_json::json!({}),
    );
    let text = extract_text(&resp);
    assert!(text.contains("engine: python3") || text.contains("engine: micropython"));
    assert!(text.contains("timeout:"));

    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_execute_simple() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    let resp = call_tool(
        &mut stdin,
        &mut reader,
        2,
        "execute",
        serde_json::json!({"script": "print(7 * 6)"}),
    );
    let text = extract_text(&resp);
    assert_eq!(text.trim(), "42");

    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_execute_regex() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    let resp = call_tool(
        &mut stdin,
        &mut reader,
        2,
        "execute",
        serde_json::json!({"script": "import re; print(re.findall(r'\\d+', 'a1b2c3'))"}),
    );
    let text = extract_text(&resp);
    assert_eq!(text.trim(), "['1', '2', '3']");

    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_sandbox_os_system() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    let resp = call_tool(
        &mut stdin,
        &mut reader,
        2,
        "execute",
        serde_json::json!({"script": "import os; print(hasattr(os, 'system'))"}),
    );
    let text = extract_text(&resp);
    assert_eq!(text.trim(), "False");

    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_sandbox_subprocess_blocked() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    let script = r#"
try:
    import subprocess
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let resp = call_tool(
        &mut stdin,
        &mut reader,
        2,
        "execute",
        serde_json::json!({"script": script}),
    );
    let text = extract_text(&resp);
    assert_eq!(text.trim(), "BLOCKED");

    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_execute_error() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    let resp = call_tool(
        &mut stdin,
        &mut reader,
        2,
        "execute",
        serde_json::json!({"script": "raise ValueError('boom')"}),
    );
    let text = extract_text(&resp);
    assert!(text.contains("ValueError"));
    assert!(text.contains("boom"));
    assert!(text.contains("[exit code: 1]"));

    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_tools_list() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    let req = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#;
    send(&mut stdin, req);
    let resp = recv(&mut reader);

    let tools = resp["result"]["tools"].as_array().unwrap();
    let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
    assert!(names.contains(&"execute"));
    assert!(names.contains(&"execute_file"));
    assert!(names.contains(&"session_start"));

    drop(stdin);
    child.wait().unwrap();
}

#[test]
fn e2e_session_start_with_deny() {
    let (mut child, mut stdin, mut reader) = spawn_server();
    initialize(&mut stdin, &mut reader);

    // Add urllib to deny list via session_start
    let resp = call_tool(
        &mut stdin,
        &mut reader,
        2,
        "session_start",
        serde_json::json!({"deny": "urllib"}),
    );
    let text = extract_text(&resp);
    assert!(text.contains("urllib"));

    // Now urllib should be blocked
    let script = r#"
try:
    import urllib
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let resp = call_tool(
        &mut stdin,
        &mut reader,
        3,
        "execute",
        serde_json::json!({"script": script}),
    );
    let text = extract_text(&resp);
    assert_eq!(text.trim(), "BLOCKED");

    drop(stdin);
    child.wait().unwrap();
}
