//! Integration tests for MCP Gateway new features
//!
//! These tests verify the following new features:
//! 1. Stats meta-tool (`gateway_get_stats`)
//! 2. Search tools with ranking (`gateway_search_tools`)
//! 3. Response caching (`gateway_invoke`)
//! 4. List servers (`gateway_list_servers`)
//! 5. CLI commands (cap registry-list)
//!
//! Note: These tests require the gateway to be running on localhost:39400
//! Run with: `cargo test --test integration_tests`
//! Or individually with: `cargo test --test integration_tests` `test_name`

use reqwest::Client;
use serde_json::{Value, json};
use std::time::{Duration, Instant};

/// Gateway URL for testing
const GATEWAY_URL: &str = "http://localhost:39400/mcp";

/// JSON-RPC request helper
#[derive(serde::Serialize)]
struct JsonRpcRequest {
    jsonrpc: String,
    id: u64,
    method: String,
    params: Value,
}

impl JsonRpcRequest {
    fn new(method: &str, params: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id: 1,
            method: method.to_string(),
            params,
        }
    }

    fn tools_call(tool_name: &str, arguments: &Value) -> Self {
        Self::new(
            "tools/call",
            json!({
                "name": tool_name,
                "arguments": arguments
            }),
        )
    }
}

/// Check if gateway is running
async fn is_gateway_running() -> bool {
    let client = Client::new();
    client
        .post(GATEWAY_URL)
        .json(&JsonRpcRequest::new("tools/list", json!({})))
        .timeout(Duration::from_secs(2))
        .send()
        .await
        .is_ok()
}

/// Parse MCP response content from tools/call
fn parse_tool_response(response: &Value) -> Result<Value, String> {
    // MCP wraps tool responses in result.content[0].text as JSON string
    let content_text = response
        .get("result")
        .and_then(|r| r.get("content"))
        .and_then(|c| c.get(0))
        .and_then(|item| item.get("text"))
        .and_then(|t| t.as_str())
        .ok_or("Missing content.text in response")?;

    serde_json::from_str(content_text).map_err(|e| format!("Failed to parse content JSON: {e}"))
}

#[tokio::test]
#[ignore = "requires gateway running on localhost:39400"]
async fn test_gateway_get_stats() {
    if !is_gateway_running().await {
        eprintln!("⚠️  Gateway not running on {GATEWAY_URL}, skipping test");
        return;
    }

    let client = Client::new();

    // Call gateway_get_stats via MCP JSON-RPC
    let request = JsonRpcRequest::tools_call("gateway_get_stats", &json!({}));

    let response = client
        .post(GATEWAY_URL)
        .json(&request)
        .send()
        .await
        .expect("Failed to send request");

    assert!(response.status().is_success(), "HTTP request failed");

    let body: Value = response.json().await.expect("Failed to parse JSON");

    // Verify no JSON-RPC error
    assert!(
        body.get("error").is_none(),
        "JSON-RPC error: {:?}",
        body.get("error")
    );

    // Parse the actual tool response
    let stats = parse_tool_response(&body).expect("Failed to parse tool response");

    // Verify required fields are present
    assert!(
        stats.get("invocations").is_some(),
        "Missing 'invocations' field"
    );
    assert!(
        stats.get("cache_hits").is_some(),
        "Missing 'cache_hits' field"
    );
    assert!(
        stats.get("tokens_saved").is_some(),
        "Missing 'tokens_saved' field"
    );
    assert!(
        stats.get("top_tools").is_some(),
        "Missing 'top_tools' field"
    );
    assert!(
        stats.get("cache_hit_rate").is_some(),
        "Missing 'cache_hit_rate' field"
    );
    assert!(
        stats.get("tools_discovered").is_some(),
        "Missing 'tools_discovered' field"
    );
    assert!(
        stats.get("tools_available").is_some(),
        "Missing 'tools_available' field"
    );
    assert!(
        stats.get("estimated_savings_usd").is_some(),
        "Missing 'estimated_savings_usd' field"
    );

    // Verify types
    assert!(
        stats["invocations"].is_number(),
        "invocations should be a number"
    );
    assert!(
        stats["cache_hits"].is_number(),
        "cache_hits should be a number"
    );
    assert!(
        stats["tokens_saved"].is_number(),
        "tokens_saved should be a number"
    );
    assert!(
        stats["top_tools"].is_array(),
        "top_tools should be an array"
    );

    println!("✅ Stats test passed: {stats:?}");
}

#[tokio::test]
#[ignore = "requires gateway running on localhost:39400"]
async fn test_gateway_search_tools() {
    if !is_gateway_running().await {
        eprintln!("⚠️  Gateway not running on {GATEWAY_URL}, skipping test");
        return;
    }

    let client = Client::new();

    // Search for "weather" tools
    let request = JsonRpcRequest::tools_call(
        "gateway_search_tools",
        &json!({
            "query": "weather",
            "limit": 5
        }),
    );

    let response = client
        .post(GATEWAY_URL)
        .json(&request)
        .send()
        .await
        .expect("Failed to send request");

    assert!(response.status().is_success(), "HTTP request failed");

    let body: Value = response.json().await.expect("Failed to parse JSON");

    // Verify no JSON-RPC error
    assert!(
        body.get("error").is_none(),
        "JSON-RPC error: {:?}",
        body.get("error")
    );

    // Parse the search results
    let results = parse_tool_response(&body).expect("Failed to parse tool response");

    // Verify result structure
    assert!(results.get("query").is_some(), "Missing 'query' field");
    assert!(results.get("matches").is_some(), "Missing 'matches' field");
    assert!(results.get("total").is_some(), "Missing 'total' field");

    assert_eq!(
        results["query"].as_str().unwrap(),
        "weather",
        "Query should be 'weather'"
    );

    // Verify matches array
    let matches = results["matches"]
        .as_array()
        .expect("matches should be array");

    // Each match should have server, tool, description
    for m in matches {
        assert!(
            m.get("server").is_some(),
            "Match missing 'server' field: {m:?}"
        );
        assert!(m.get("tool").is_some(), "Match missing 'tool' field: {m:?}");
        assert!(
            m.get("description").is_some(),
            "Match missing 'description' field: {m:?}"
        );

        // If ranking is enabled, score field should be present
        // (This is optional depending on configuration)
        if m.get("score").is_some() {
            assert!(
                m["score"].is_number(),
                "score should be a number if present"
            );
        }
    }

    println!("✅ Search test passed: found {} results", matches.len());
}

#[tokio::test]
#[ignore = "requires gateway running on localhost:39400"]
async fn test_invoke_caching() {
    if !is_gateway_running().await {
        eprintln!("⚠️  Gateway not running on {GATEWAY_URL}, skipping test");
        return;
    }

    let client = Client::new();

    // First, get stats to establish baseline
    let stats_request = JsonRpcRequest::tools_call("gateway_get_stats", &json!({}));

    let initial_response = client
        .post(GATEWAY_URL)
        .json(&stats_request)
        .send()
        .await
        .expect("Failed to get initial stats");

    let initial_body: Value = initial_response
        .json()
        .await
        .expect("Failed to parse initial stats");
    let initial_stats = parse_tool_response(&initial_body).expect("Failed to parse initial stats");
    let initial_cache_hits = initial_stats["cache_hits"].as_u64().unwrap_or(0);

    // First invocation - should NOT be cached
    let invoke_request = JsonRpcRequest::tools_call(
        "gateway_invoke",
        &json!({
            "server": "capabilities",
            "tool": "weather_current",
            "arguments": {
                "latitude": 52.52,
                "longitude": 13.405
            }
        }),
    );

    let start1 = Instant::now();
    let response1 = client
        .post(GATEWAY_URL)
        .json(&invoke_request)
        .send()
        .await
        .expect("Failed to send first request");
    let duration1 = start1.elapsed();

    assert!(response1.status().is_success(), "First request failed");
    let body1: Value = response1
        .json()
        .await
        .expect("Failed to parse first response");
    assert!(
        body1.get("error").is_none(),
        "First invocation error: {:?}",
        body1.get("error")
    );

    // Small delay to ensure cache is written
    tokio::time::sleep(Duration::from_millis(50)).await;

    // Second invocation - SHOULD be cached (same server, tool, arguments)
    let start2 = Instant::now();
    let response2 = client
        .post(GATEWAY_URL)
        .json(&invoke_request)
        .send()
        .await
        .expect("Failed to send second request");
    let duration2 = start2.elapsed();

    assert!(response2.status().is_success(), "Second request failed");
    let body2: Value = response2
        .json()
        .await
        .expect("Failed to parse second response");
    assert!(
        body2.get("error").is_none(),
        "Second invocation error: {:?}",
        body2.get("error")
    );

    // Get final stats
    let final_response = client
        .post(GATEWAY_URL)
        .json(&stats_request)
        .send()
        .await
        .expect("Failed to get final stats");

    let final_body: Value = final_response
        .json()
        .await
        .expect("Failed to parse final stats");
    let final_stats = parse_tool_response(&final_body).expect("Failed to parse final stats");
    let final_cache_hits = final_stats["cache_hits"].as_u64().unwrap_or(0);

    // Verify cache hit occurred
    assert!(
        final_cache_hits > initial_cache_hits,
        "Cache hits should have increased. Initial: {initial_cache_hits}, Final: {final_cache_hits}"
    );

    // Cached response should typically be faster (though not guaranteed in all environments)
    println!(
        "✅ Caching test passed. First call: {:?}, Second call: {:?}, Cache hits increased by: {}",
        duration1,
        duration2,
        final_cache_hits - initial_cache_hits
    );
}

#[tokio::test]
#[ignore = "requires gateway running on localhost:39400"]
async fn test_gateway_list_servers() {
    if !is_gateway_running().await {
        eprintln!("⚠️  Gateway not running on {GATEWAY_URL}, skipping test");
        return;
    }

    let client = Client::new();

    // Call gateway_list_servers
    let request = JsonRpcRequest::tools_call("gateway_list_servers", &json!({}));

    let response = client
        .post(GATEWAY_URL)
        .json(&request)
        .send()
        .await
        .expect("Failed to send request");

    assert!(response.status().is_success(), "HTTP request failed");

    let body: Value = response.json().await.expect("Failed to parse JSON");

    // Verify no JSON-RPC error
    assert!(
        body.get("error").is_none(),
        "JSON-RPC error: {:?}",
        body.get("error")
    );

    // Parse the servers list
    let servers_response = parse_tool_response(&body).expect("Failed to parse tool response");

    // Verify servers field exists and is an array
    assert!(
        servers_response.get("servers").is_some(),
        "Missing 'servers' field"
    );
    let servers = servers_response["servers"]
        .as_array()
        .expect("servers should be array");

    // Should have at least one server (capabilities backend always present)
    assert!(
        !servers.is_empty(),
        "Expected at least one server to be available"
    );

    // Verify each server has required fields
    for server in servers {
        assert!(
            server.get("name").is_some(),
            "Server missing 'name' field: {server:?}"
        );
        assert!(
            server.get("running").is_some(),
            "Server missing 'running' field: {server:?}"
        );
        assert!(
            server.get("transport").is_some(),
            "Server missing 'transport' field: {server:?}"
        );
        assert!(
            server.get("tools_count").is_some(),
            "Server missing 'tools_count' field: {server:?}"
        );
        assert!(
            server.get("circuit_state").is_some(),
            "Server missing 'circuit_state' field: {server:?}"
        );

        // Verify types
        assert!(server["name"].is_string(), "name should be string");
        assert!(server["running"].is_boolean(), "running should be boolean");
        assert!(
            server["transport"].is_string(),
            "transport should be string"
        );
        assert!(
            server["tools_count"].is_number(),
            "tools_count should be number"
        );
        assert!(
            server["circuit_state"].is_string(),
            "circuit_state should be string"
        );
    }

    println!(
        "✅ List servers test passed: found {} servers",
        servers.len()
    );
}

#[tokio::test]
#[ignore = "requires mcp-gateway binary built in target/"]
async fn test_cap_registry_list() {
    // Check if mcp-gateway binary exists in target/debug or target/release
    let binary_path = if std::path::Path::new("target/release/mcp-gateway").exists() {
        "target/release/mcp-gateway"
    } else if std::path::Path::new("target/debug/mcp-gateway").exists() {
        "target/debug/mcp-gateway"
    } else {
        eprintln!("⚠️  mcp-gateway binary not found, skipping CLI test");
        eprintln!("   Build with: cargo build --release");
        return;
    };

    // Run: mcp-gateway cap registry-list
    let output = std::process::Command::new(binary_path)
        .args(["cap", "registry-list"])
        .output()
        .expect("Failed to execute mcp-gateway cap registry-list");

    // Should succeed
    assert!(
        output.status.success(),
        "cap registry-list command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);

    // Output should contain capability names from registry/index.json
    // Expected capabilities: stripe_list_charges, yahoo_stock_quote, ecb_exchange_rates,
    // gmail_send_email, slack_post_message, weather_current, wikipedia_search, github_create_issue

    let expected_capabilities = [
        "stripe_list_charges",
        "yahoo_stock_quote",
        "ecb_exchange_rates",
        "gmail_send_email",
        "slack_post_message",
        "weather_current",
        "wikipedia_search",
        "github_create_issue",
    ];

    for cap in &expected_capabilities {
        assert!(
            stdout.contains(cap),
            "Output should contain capability '{cap}', but got: {stdout}"
        );
    }

    println!("✅ CLI registry-list test passed");
}

#[tokio::test]
#[ignore = "requires gateway running on localhost:39400"]
async fn test_gateway_integration_flow() {
    if !is_gateway_running().await {
        eprintln!("⚠️  Gateway not running on {GATEWAY_URL}, skipping test");
        return;
    }

    let client = Client::new();

    // 1. List servers
    let list_servers_req = JsonRpcRequest::tools_call("gateway_list_servers", &json!({}));
    let servers_response = client
        .post(GATEWAY_URL)
        .json(&list_servers_req)
        .send()
        .await
        .expect("Failed to list servers");
    let servers_body: Value = servers_response.json().await.expect("Failed to parse");
    let servers_data = parse_tool_response(&servers_body).expect("Failed to parse servers");
    let servers = servers_data["servers"].as_array().expect("servers array");

    assert!(!servers.is_empty(), "Should have at least one server");

    // 2. Search for tools
    let search_req = JsonRpcRequest::tools_call(
        "gateway_search_tools",
        &json!({
            "query": "weather",
            "limit": 10
        }),
    );
    let search_response = client
        .post(GATEWAY_URL)
        .json(&search_req)
        .send()
        .await
        .expect("Failed to search");
    let search_body: Value = search_response.json().await.expect("Failed to parse");
    let search_data = parse_tool_response(&search_body).expect("Failed to parse search");

    // 3. Get stats
    let stats_req = JsonRpcRequest::tools_call("gateway_get_stats", &json!({}));
    let stats_response = client
        .post(GATEWAY_URL)
        .json(&stats_req)
        .send()
        .await
        .expect("Failed to get stats");
    let stats_body: Value = stats_response.json().await.expect("Failed to parse");
    let stats_data = parse_tool_response(&stats_body).expect("Failed to parse stats");

    // Stats should show the search we just performed
    let searches_found = search_data["total"].as_u64().unwrap_or(0);
    assert!(
        stats_data["tools_discovered"].as_u64().unwrap_or(0) >= searches_found,
        "Stats should reflect discovered tools"
    );

    println!("✅ Integration flow test passed");
    println!("   Servers: {}", servers.len());
    println!("   Search results: {searches_found}");
    println!("   Total invocations: {}", stats_data["invocations"]);
}
