//! MCP server integration tests.
//! Spawns the actual binary, sends JSON-RPC over stdin, validates responses.

use serde_json::{json, Value};
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;

const BINARY: &str = env!("CARGO_BIN_EXE_tentactl");

/// Helper: spawn the server, send messages, collect responses.
/// `expect_responses` is how many JSON-RPC responses we expect (including init).
async fn mcp_session(messages: Vec<Value>) -> Vec<Value> {
    // Count expected responses: 1 for init + 1 for each message that has an "id"
    let expected = 1 + messages.iter().filter(|m| m.get("id").is_some()).count();

    let mut child = Command::new(BINARY)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .expect("failed to spawn server");

    let mut stdin = child.stdin.take().unwrap();
    let stdout = child.stdout.take().unwrap();
    let mut reader = BufReader::new(stdout).lines();

    // Send initialize
    let init = json!({
        "jsonrpc": "2.0",
        "id": 0,
        "method": "initialize",
        "params": {
            "protocolVersion": "2024-11-05",
            "capabilities": {},
            "clientInfo": {"name": "test", "version": "0.1"}
        }
    });
    stdin
        .write_all(format!("{}\n", init).as_bytes())
        .await
        .unwrap();

    // Wait for init response before continuing
    let mut responses = Vec::new();
    if let Ok(Some(line)) = reader.next_line().await {
        if let Ok(val) = serde_json::from_str::<Value>(&line) {
            responses.push(val);
        }
    }

    // Send initialized notification
    let initialized_notif = json!({
        "jsonrpc": "2.0",
        "method": "notifications/initialized"
    });
    stdin
        .write_all(format!("{}\n", initialized_notif).as_bytes())
        .await
        .unwrap();

    // Send user messages
    for msg in &messages {
        stdin
            .write_all(format!("{}\n", msg).as_bytes())
            .await
            .unwrap();
        stdin.flush().await.unwrap();
    }

    // Read remaining expected responses with a timeout
    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10);
    while responses.len() < expected {
        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
        if remaining.is_zero() {
            break;
        }
        match tokio::time::timeout(remaining, reader.next_line()).await {
            Ok(Ok(Some(line))) => {
                if let Ok(val) = serde_json::from_str::<Value>(&line) {
                    responses.push(val);
                }
            }
            _ => break,
        }
    }

    // Clean up
    drop(stdin);
    let _ = child.kill().await;
    responses
}

/// Helper: extract the init response
fn find_response(responses: &[Value], id: i64) -> Option<&Value> {
    responses.iter().find(|r| r["id"] == id)
}

// === Tests ===

#[tokio::test]
async fn server_initializes() {
    let responses = mcp_session(vec![]).await;

    let init_resp = find_response(&responses, 0).expect("should have init response");
    assert_eq!(init_resp["result"]["protocolVersion"], "2024-11-05");
    assert!(init_resp["result"]["capabilities"]["tools"].is_object());
    assert!(init_resp["result"]["serverInfo"].is_object());

    let instructions = init_resp["result"]["instructions"].as_str().unwrap_or("");
    assert!(
        instructions.contains("Kraken"),
        "instructions should mention Kraken"
    );
}

#[tokio::test]
async fn tools_list_returns_all_tools() {
    let list_req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
    });

    let responses = mcp_session(vec![list_req]).await;
    let resp = find_response(&responses, 1).expect("should have tools/list response");

    let tools = resp["result"]["tools"].as_array().expect("tools array");

    let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();

    // All 8 tools must be present
    let expected = [
        "get_ticker",
        "get_orderbook",
        "get_ohlc",
        "get_balance",
        "get_trade_history",
        "get_open_orders",
        "place_order",
        "cancel_order",
    ];
    for name in &expected {
        assert!(
            tool_names.contains(name),
            "missing tool: {name}. Got: {tool_names:?}"
        );
    }
    assert_eq!(tools.len(), 8, "should have exactly 8 tools");
}

#[tokio::test]
async fn tools_have_descriptions() {
    let list_req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
    });

    let responses = mcp_session(vec![list_req]).await;
    let resp = find_response(&responses, 1).unwrap();
    let tools = resp["result"]["tools"].as_array().unwrap();

    for tool in tools {
        let name = tool["name"].as_str().unwrap();
        let desc = tool["description"].as_str().unwrap_or("");
        assert!(!desc.is_empty(), "tool {name} should have a description");
    }
}

#[tokio::test]
async fn place_order_has_warning_in_description() {
    let list_req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
    });

    let responses = mcp_session(vec![list_req]).await;
    let resp = find_response(&responses, 1).unwrap();
    let tools = resp["result"]["tools"].as_array().unwrap();

    let place_order = tools
        .iter()
        .find(|t| t["name"] == "place_order")
        .expect("place_order tool");

    let desc = place_order["description"].as_str().unwrap();
    assert!(
        desc.contains("REAL MONEY"),
        "place_order should warn about real money. Got: {desc}"
    );
}

#[tokio::test]
async fn tools_have_input_schemas() {
    let list_req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
    });

    let responses = mcp_session(vec![list_req]).await;
    let resp = find_response(&responses, 1).unwrap();
    let tools = resp["result"]["tools"].as_array().unwrap();

    for tool in tools {
        let name = tool["name"].as_str().unwrap();
        let schema = &tool["inputSchema"];
        assert!(schema.is_object(), "tool {name} should have an inputSchema");
        assert_eq!(
            schema["type"], "object",
            "tool {name} inputSchema should be type object"
        );
    }
}

#[tokio::test]
async fn get_ticker_schema_requires_pair() {
    let list_req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
    });

    let responses = mcp_session(vec![list_req]).await;
    let resp = find_response(&responses, 1).unwrap();
    let tools = resp["result"]["tools"].as_array().unwrap();

    let ticker = tools.iter().find(|t| t["name"] == "get_ticker").unwrap();

    let required = ticker["inputSchema"]["required"]
        .as_array()
        .expect("required array");
    let required_names: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
    assert!(
        required_names.contains(&"pair"),
        "get_ticker should require 'pair'"
    );
}

#[tokio::test]
async fn place_order_schema_requires_fields() {
    let list_req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
    });

    let responses = mcp_session(vec![list_req]).await;
    let resp = find_response(&responses, 1).unwrap();
    let tools = resp["result"]["tools"].as_array().unwrap();

    let place_order = tools.iter().find(|t| t["name"] == "place_order").unwrap();

    let required = place_order["inputSchema"]["required"]
        .as_array()
        .expect("required array");
    let required_names: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();

    for field in &["pair", "direction", "order_type", "volume"] {
        assert!(
            required_names.contains(field),
            "place_order should require '{field}'. Got: {required_names:?}"
        );
    }
}

#[tokio::test]
async fn get_balance_without_keys_returns_auth_error() {
    let call = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "get_balance",
            "arguments": {}
        }
    });

    let responses = mcp_session(vec![call]).await;
    let resp = find_response(&responses, 1).expect("should have response");

    // Should return an error content (not a JSON-RPC error, but isError in result)
    let content = &resp["result"]["content"][0]["text"];
    let text = content.as_str().unwrap_or("");
    assert!(
        text.contains("Authentication required"),
        "should mention auth. Got: {text}"
    );
    assert_eq!(resp["result"]["isError"], true);
}

#[tokio::test]
async fn call_unknown_tool_returns_error() {
    let call = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "nonexistent_tool",
            "arguments": {}
        }
    });

    let responses = mcp_session(vec![call]).await;
    let resp = find_response(&responses, 1).expect("should have response");

    // Should be a JSON-RPC error (method not found or similar)
    assert!(
        resp.get("error").is_some(),
        "unknown tool should return error. Got: {resp}"
    );
}

#[tokio::test]
async fn multiple_tool_calls_in_sequence() {
    let call1 = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
    });
    let call2 = json!({
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {
            "name": "get_balance",
            "arguments": {}
        }
    });

    let responses = mcp_session(vec![call1, call2]).await;

    let resp1 = find_response(&responses, 1).expect("should have tools/list response");
    assert!(resp1["result"]["tools"].is_array());

    let resp2 = find_response(&responses, 2).expect("should have get_balance response");
    assert!(resp2["result"]["content"].is_array());
}

#[tokio::test]
async fn get_ticker_missing_pair_returns_error() {
    let call = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "get_ticker",
            "arguments": {}
        }
    });

    let responses = mcp_session(vec![call]).await;
    let resp = find_response(&responses, 1).expect("should have response");

    // Should get a parameter validation error
    assert!(
        resp.get("error").is_some() || resp["result"]["isError"] == true,
        "missing required param should error. Got: {resp}"
    );
}
