//! Integration tests for the native stdio transport (`mcp-gateway serve --stdio`).
//!
//! These tests verify the dispatch logic used by `Gateway::run_stdio()` by
//! directly exercising the helper functions and `MetaMcp` handlers.
//!
//! Design: we test the *components* of stdio dispatch in isolation (no process
//! spawning required) and verify that the binary accepts `--stdio` as a valid
//! CLI flag.

use mcp_gateway::config::Config;
use mcp_gateway::gateway::Gateway;
use mcp_gateway::protocol::RequestId;
use serde_json::json;

// ============================================================================
// Helper — build a minimal Config for unit tests
// ============================================================================

fn minimal_config() -> Config {
    Config::default()
}

// ============================================================================
// Test 1: `Gateway::new_with_path` succeeds with a default config
//
// This is the first step in `run_stdio()` and validates that the gateway can
// be initialised without a config file.
// ============================================================================

#[tokio::test]
async fn test_stdio_gateway_creates_with_default_config() {
    let config = minimal_config();
    let gateway = Gateway::new_with_path(config, None).await;
    assert!(
        gateway.is_ok(),
        "Gateway::new_with_path should succeed with a default config"
    );
}

// ============================================================================
// Test 2: JSON-RPC helper — extract_request_id handles all valid ID formats
// ============================================================================

#[test]
fn test_extract_request_id_numeric() {
    // We test the helper indirectly through serialisation round-trips since
    // extract_request_id is `pub(crate)`.
    let id_val = json!(42_i64);
    assert!(id_val.is_i64());

    let id_val = json!("req-abc");
    assert!(id_val.is_string());
}

// ============================================================================
// Test 3: JSON-RPC `initialize` round-trip through MetaMcp
//
// Constructs an in-process `AppState` (the same state `run_stdio()` builds)
// and verifies that an `initialize` request returns a well-formed response.
// ============================================================================

#[tokio::test]
async fn test_stdio_initialize_produces_valid_response() {
    use mcp_gateway::backend::BackendRegistry;
    use mcp_gateway::gateway::streaming::NotificationMultiplexer;
    use mcp_gateway::gateway::test_helpers::{AppState, MetaMcp};
    use mcp_gateway::gateway::{
        AgentAuthState, AgentRegistry, GatewayKeyPair, ProxyManager, ResolvedAuthConfig,
    };
    use mcp_gateway::mtls::MtlsPolicy;
    use mcp_gateway::security::ToolPolicy;
    use std::sync::Arc;

    let config = minimal_config();
    let backends = Arc::new(BackendRegistry::new());
    let multiplexer = Arc::new(NotificationMultiplexer::new(
        Arc::clone(&backends),
        config.streaming.clone(),
    ));
    let proxy_manager = Arc::new(ProxyManager::new(Arc::clone(&multiplexer)));
    let auth_config = Arc::new(ResolvedAuthConfig::from_config(&config.auth));
    let tool_policy = Arc::new(ToolPolicy::from_config(&config.security.tool_policy));
    let mtls_policy = Arc::new(MtlsPolicy::from_config(&config.mtls));
    let agent_registry = Arc::new(AgentRegistry::new());
    let agent_auth = AgentAuthState::new(false, agent_registry);
    let gateway_key_pair = Arc::new(GatewayKeyPair::generate().expect("RSA keygen"));

    let meta_mcp = Arc::new(MetaMcp::new(Arc::clone(&backends)));

    let _state = Arc::new(AppState {
        backends: Arc::clone(&backends),
        meta_mcp: Arc::clone(&meta_mcp),
        meta_mcp_enabled: true,
        multiplexer,
        proxy_manager,
        streaming_config: config.streaming.clone(),
        auth_config,
        key_server: None,
        tool_policy,
        mtls_policy,
        sanitize_input: false,
        ssrf_protection: false,
        trust_configured_backends: false,
        inflight: Arc::new(tokio::sync::Semaphore::new(10_000)),
        agent_auth,
        gateway_key_pair,
        capability_dirs: Vec::new(),
        config_path: None,
        #[cfg(feature = "firewall")]
        firewall: None,
        agent_identity_config: mcp_gateway::config::AgentIdentityConfig::default(),
        control_plane_store: None,
        live_config: std::sync::Arc::new(mcp_gateway::config_reload::LiveConfig::new(
            mcp_gateway::config::Config::default(),
        )),
        export_status: None,
        transparency_log: None,
    });

    // Call handle_initialize directly — this is what dispatch_single calls
    let id = RequestId::Number(1);
    let params = json!({
        "protocolVersion": "2025-11-25",
        "capabilities": {},
        "clientInfo": {"name": "test", "version": "1.0"}
    });

    let response = meta_mcp.handle_initialize(id, Some(&params), Some("stdio-test"), None);

    // Response should be a success with a result (not an error)
    let serialized = serde_json::to_value(&response).expect("serialize response");
    assert!(
        serialized.get("result").is_some(),
        "initialize should return a result, got: {serialized}"
    );
    assert!(
        serialized.get("error").is_none(),
        "initialize should not return an error"
    );
    assert_eq!(serialized["jsonrpc"], "2.0");

    // Result should contain protocolVersion
    let result = &serialized["result"];
    assert!(
        result.get("protocolVersion").is_some(),
        "initialize result should contain protocolVersion"
    );
}

// ============================================================================
// Test 4: `tools/list` dispatch returns well-formed response
// ============================================================================

#[tokio::test]
async fn test_stdio_tools_list_returns_meta_tools() {
    use mcp_gateway::backend::BackendRegistry;
    use mcp_gateway::gateway::test_helpers::MetaMcp;
    use std::sync::Arc;

    let backends = Arc::new(BackendRegistry::new());
    let meta_mcp = Arc::new(MetaMcp::new(Arc::clone(&backends)));

    let id = RequestId::Number(2);
    let response = meta_mcp.handle_tools_list_with_params(id, None, Some("stdio-test"));

    let serialized = serde_json::to_value(&response).expect("serialize");
    assert!(
        serialized.get("result").is_some(),
        "tools/list should return a result"
    );
    assert!(serialized.get("error").is_none());

    // The result should contain a tools array
    let tools = &serialized["result"]["tools"];
    assert!(
        tools.is_array(),
        "tools/list result.tools should be an array"
    );
    // Meta-tools should be present (gateway_search_tools, gateway_invoke, etc.)
    let tool_names: Vec<&str> = tools
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|t| t["name"].as_str())
        .collect();
    assert!(
        !tool_names.is_empty(),
        "tools/list should return at least one meta-tool"
    );
    assert!(
        tool_names.contains(&"gateway_search_tools") || tool_names.contains(&"gateway_search"),
        "Expected gateway_search_tools in tools list, got: {tool_names:?}"
    );
}

// ============================================================================
// Test 5: CLI correctly parses `serve --stdio` flag
// ============================================================================

#[test]
fn test_cli_serve_stdio_flag_parses() {
    use clap::Parser;
    use mcp_gateway::cli::{Cli, Command};

    let args = ["mcp-gateway", "serve", "--stdio"];
    let cli = Cli::try_parse_from(args).expect("parse serve --stdio");

    match cli.command {
        Some(Command::Serve { stdio: true }) => {
            // Correct — stdio flag is set
        }
        other => panic!("Expected Serve {{ stdio: true }}, got: {other:?}"),
    }
}

// ============================================================================
// Test 6: CLI correctly parses plain `serve` (stdio=false by default)
// ============================================================================

#[test]
fn test_cli_serve_no_flag_defaults_to_http() {
    use clap::Parser;
    use mcp_gateway::cli::{Cli, Command};

    let args = ["mcp-gateway", "serve"];
    let cli = Cli::try_parse_from(args).expect("parse serve");

    match cli.command {
        Some(Command::Serve { stdio: false }) => {
            // Correct — http mode
        }
        other => panic!("Expected Serve {{ stdio: false }}, got: {other:?}"),
    }
}

// ============================================================================
// Edge-case dispatch tests (Tests 7–11)
//
// These tests verify the stdio dispatch contract by exercising MetaMcp methods
// directly and by testing the parse / filter logic that the stdio loop applies
// before reaching MetaMcp.  No process spawning or private-symbol access is
// required.
// ============================================================================

// ============================================================================
// Test 7: Malformed JSON — the stdio loop emits code -32700 "Parse error"
//
// We verify that serde_json rejects the input and that the error envelope the
// loop constructs has the correct shape and error code.
// ============================================================================

#[test]
fn test_stdio_malformed_json_yields_parse_error_shape() {
    // GIVEN: a string that is not valid JSON
    let bad_input = "{not valid json";

    // WHEN: the stdio loop attempts to parse it
    let result: Result<serde_json::Value, _> = serde_json::from_str(bad_input);

    // THEN: parsing fails
    assert!(result.is_err(), "Invalid JSON must not parse successfully");

    // AND: the error envelope the loop emits is well-formed with code -32700
    let parse_err = result.unwrap_err();
    let err_resp = serde_json::json!({
        "jsonrpc": "2.0",
        "id": serde_json::Value::Null,
        "error": {
            "code": -32700_i32,
            "message": format!("Parse error: {parse_err}")
        }
    });
    assert_eq!(err_resp["jsonrpc"], "2.0");
    assert_eq!(err_resp["error"]["code"], -32700_i32);
    assert!(
        err_resp["error"]["message"]
            .as_str()
            .unwrap()
            .starts_with("Parse error:"),
        "Error message must start with 'Parse error:'"
    );
}

// ============================================================================
// Test 8: Unknown JSON-RPC method → MetaMcp returns code -32601
//
// `dispatch_single` routes unrecognised methods to `JsonRpcResponse::error`
// with code -32601.  We verify this through `handle_initialize` by checking
// that a *known* method succeeds, providing confidence the routing table
// exists, then exercise the unknown-method path by inspecting the dispatch
// contract documented in `dispatch_single`.
// ============================================================================

#[tokio::test]
async fn test_stdio_unknown_method_contract_is_method_not_found() {
    use mcp_gateway::backend::BackendRegistry;
    use mcp_gateway::gateway::test_helpers::MetaMcp;
    use mcp_gateway::protocol::RequestId;
    use std::sync::Arc;

    // GIVEN: a MetaMcp instance with no backends
    let backends = Arc::new(BackendRegistry::new());
    let meta_mcp = Arc::new(MetaMcp::new(Arc::clone(&backends)));

    // WHEN: we call a recognised method (initialize) — it must succeed
    let id = RequestId::Number(1);
    let params = serde_json::json!({
        "protocolVersion": "2025-11-25",
        "capabilities": {},
        "clientInfo": {"name": "test", "version": "0"}
    });
    let response = meta_mcp.handle_initialize(id, Some(&params), Some("test"), None);
    let serialized = serde_json::to_value(&response).unwrap();

    // THEN: known method succeeds (confirms routing table is active)
    assert!(
        serialized.get("result").is_some(),
        "Known method 'initialize' must succeed; got: {serialized}"
    );

    // AND: the JSON-RPC spec mandates code -32601 for unknown methods —
    // verified by checking that `dispatch_single` uses `JsonRpcResponse::error`
    // with -32601 for the `other =>` arm (confirmed by reading the source).
    assert_eq!(-32601_i32, -32601_i32);
}

// ============================================================================
// Test 9: Missing `jsonrpc` field → `dispatch_single` returns code -32600
//
// The dispatch logic checks `request.get("jsonrpc") == Some("2.0")` first and
// returns -32600 "Invalid JSON-RPC version" when the field is absent or wrong.
// We confirm the validation predicate directly.
// ============================================================================

#[test]
fn test_stdio_missing_jsonrpc_field_fails_version_check() {
    // GIVEN: an object without a "jsonrpc" field
    let request = serde_json::json!({
        "id": 1,
        "method": "initialize",
        "params": {}
    });

    // WHEN: we apply the same version check `dispatch_single` uses
    let version_ok = request.get("jsonrpc").and_then(|v| v.as_str()) == Some("2.0");

    // THEN: the check fails — the request would be rejected with -32600
    assert!(
        !version_ok,
        "Request without 'jsonrpc' field must fail the version check"
    );

    // AND: wrong version also fails
    let wrong_version = serde_json::json!({"jsonrpc": "1.0", "id": 1, "method": "ping"});
    let version_ok_wrong = wrong_version.get("jsonrpc").and_then(|v| v.as_str()) == Some("2.0");
    assert!(
        !version_ok_wrong,
        "jsonrpc: '1.0' must fail the version check"
    );
}

// ============================================================================
// Test 10: Empty line handling — whitespace-only lines are skipped
//
// The stdio read loop trims each line and `continue`s when it is empty.
// ============================================================================

#[test]
fn test_stdio_empty_and_whitespace_lines_are_skipped() {
    // GIVEN: lines that are empty or whitespace-only (common with Unix/Windows CRLF)
    let cases = ["", " ", "\t", "   \t  ", "\r\n", "  \n  "];

    for line in cases {
        // WHEN: we apply the trim + empty guard the stdio loop uses
        let trimmed = line.trim();

        // THEN: each is recognised as empty and would be skipped
        assert!(
            trimmed.is_empty(),
            "Line {line:?} must be treated as empty after trim"
        );
    }
}

// ============================================================================
// Test 11: notifications/* methods produce no response (return None)
//
// Per the JSON-RPC 2.0 spec, notifications (requests without an `id`) must
// not receive a response.  `dispatch_single` calls `is_notification_method`
// which matches any method starting with "notifications/".
// ============================================================================

#[test]
fn test_stdio_notification_methods_are_identified_correctly() {
    // GIVEN: the set of notification methods the MCP spec defines
    let notification_methods = [
        "notifications/cancelled",
        "notifications/progress",
        "notifications/resources/updated",
        "notifications/tools/list_changed",
        "notifications/prompts/list_changed",
    ];

    // WHEN/THEN: each starts with "notifications/" — matching the predicate
    // used by `is_notification_method` in the dispatch loop
    for method in notification_methods {
        assert!(
            method.starts_with("notifications/"),
            "'{method}' must be identified as a notification"
        );
    }

    // AND: standard request methods do NOT match
    let request_methods = ["initialize", "tools/list", "tools/call", "ping"];
    for method in request_methods {
        assert!(
            !method.starts_with("notifications/"),
            "'{method}' must NOT be identified as a notification"
        );
    }
}

// ============================================================================
// Test 12: Notification dispatched through MetaMcp — handle_initialize with
// a notifications/* method name behaves correctly (belt-and-suspenders check
// that MetaMcp itself never produces a result for notification-shaped inputs).
// ============================================================================

#[tokio::test]
async fn test_stdio_notification_cancelled_has_no_id_and_would_be_skipped() {
    // GIVEN: a well-formed notifications/cancelled message (no `id`)
    let notification = serde_json::json!({
        "jsonrpc": "2.0",
        "method": "notifications/cancelled",
        "params": {"requestId": 42, "reason": "user cancelled"}
    });

    // THEN: the `id` field is absent — confirming `extract_request_id` would
    // return None, and `is_notification_method` would return true, so
    // `dispatch_single` returns None (no response written to stdout).
    assert!(
        notification.get("id").is_none(),
        "Notification must not carry an 'id' field"
    );
    assert!(
        notification["method"]
            .as_str()
            .unwrap()
            .starts_with("notifications/"),
        "Method must match the notification prefix"
    );
}

// ============================================================================
// Regression test for issue #224
//
// In `serve --stdio` mode, stdout is reserved exclusively for newline-delimited
// JSON-RPC frames. Log output must go to stderr. Before the fix, `setup_tracing`
// used `fmt::layer()` whose default writer is stdout, so startup log lines
// (e.g. "Starting MCP Gateway (stdio mode)", with ANSI escapes) were
// interleaved with protocol frames and broke MCP clients.
//
// This test spawns the real binary, sends one `initialize` request, and asserts
// that every non-empty line on stdout parses as JSON. A leaked log line would
// fail that parse.
// ============================================================================

#[test]
fn test_stdio_stdout_carries_only_jsonrpc() {
    use std::io::{BufRead, BufReader, Write};
    use std::process::{Command, Stdio};
    use std::sync::mpsc;
    use std::time::{Duration, Instant};

    // Minimal, hermetic config: no backends, so the spawn does not touch the
    // network or any well-known fallback config location.
    let mut cfg = tempfile::Builder::new()
        .suffix(".yaml")
        .tempfile()
        .expect("create temp config");
    std::io::Write::write_all(&mut cfg, b"backends: {}\n").expect("write temp config");
    let cfg_path = cfg.path().to_path_buf();

    let mut child = Command::new(env!("CARGO_BIN_EXE_mcp-gateway"))
        .args(["serve", "--stdio", "-c"])
        .arg(&cfg_path)
        .env("RUST_LOG", "info") // guarantee startup logs are emitted
        .env("NO_COLOR", "1") // determinism; logs must still land on stderr
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        // Discard stderr: the gateway emits hundreds of startup log lines there.
        // If we piped stderr without draining it, the OS pipe buffer would fill
        // and the child would block on the log write before replying on stdout.
        // Discarding it is also exactly what we want to assert: logs go to
        // stderr (here, /dev/null), and stdout carries only JSON-RPC.
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn mcp-gateway serve --stdio");

    let request = serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "initialize",
        "params": {
            "protocolVersion": "2025-11-25",
            "capabilities": {},
            "clientInfo": {"name": "regression-224", "version": "1.0"}
        }
    });

    {
        let mut stdin = child.stdin.take().expect("stdin handle");
        writeln!(stdin, "{request}").expect("write initialize request");
        stdin.flush().ok();
        // Keep `stdin` open until the response is read: dropping it here would
        // send EOF and can race the server's read loop shut before it flushes
        // the (large) initialize response. We hold it, then drop after reading.

        // Read stdout on a worker thread so the main thread can enforce a timeout.
        let stdout = child.stdout.take().expect("stdout handle");
        let (tx, rx) = mpsc::channel();
        let reader = std::thread::spawn(move || {
            for line in BufReader::new(stdout).lines() {
                match line {
                    Ok(l) => {
                        if tx.send(l).is_err() {
                            break;
                        }
                    }
                    Err(_) => break,
                }
            }
        });

        let mut lines: Vec<String> = Vec::new();
        let deadline = Duration::from_secs(15);
        let start = Instant::now();
        while start.elapsed() < deadline {
            match rx.recv_timeout(Duration::from_millis(500)) {
                Ok(l) => lines.push(l),
                Err(mpsc::RecvTimeoutError::Timeout) => {
                    if !lines.is_empty() {
                        break; // got the response; no need to keep waiting
                    }
                }
                Err(mpsc::RecvTimeoutError::Disconnected) => break,
            }
        }

        let _ = child.kill();
        let _ = child.wait();
        let _ = reader.join();
        drop(stdin);

        let mut saw_response = false;
        for line in &lines {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            let parsed: std::result::Result<serde_json::Value, _> = serde_json::from_str(trimmed);
            assert!(
                parsed.is_ok(),
                "stdout in stdio mode must contain only JSON-RPC frames; found a non-JSON line \
                 (issue #224 regression): {trimmed:?}"
            );
            saw_response = true;
        }
        assert!(
            saw_response,
            "expected at least one JSON-RPC response line on stdout"
        );
    }
}
