//! The `_meta` envelope the stateless transport attaches to every request.
//!
//! Split from `client` so that module stays under the module-size ceiling and
//! the trace-context injection is unit-testable in isolation. The session-based
//! path does not use this helper; it carries the same fields once in
//! `initialize`.

use serde_json::{json, Value};

use super::{ClientCapabilities, ProtocolVersion};

/// Build the `_meta` block the 2026-07-28 stateless transport attaches to
/// every request: protocol version, client info, and declared capabilities.
///
/// Returns a `Value` rather than a strongly typed struct so it can be spliced
/// into any request payload via `params["_meta"] = ...` without allocating a
/// custom envelope type per call site.
pub fn build_request_meta(version: ProtocolVersion, caps: &ClientCapabilities) -> Value {
    let mut meta = json!({
        "protocolVersion": version.as_wire(),
        "clientInfo": {
            "name": caps.name,
            "version": caps.version,
        },
        "capabilities": caps.capabilities,
    });
    // SEP-414: propagate an inbound W3C trace context (the standard TRACEPARENT
    // / TRACESTATE env vars) into the outbound `_meta` so the tested server's
    // spans link to the trace that invoked mcptest. A missing or malformed
    // traceparent is a no-op: propagation is optional per the spec.
    if let Some(obj) = meta.as_object_mut() {
        inject_trace_context(
            obj,
            std::env::var("TRACEPARENT").ok().as_deref(),
            std::env::var("TRACESTATE").ok().as_deref(),
        );
    }
    meta
}

/// Inject a W3C trace context into a request `_meta` map when a valid
/// `traceparent` is supplied. Split from the env read in [`build_request_meta`]
/// so the injection is unit-testable without touching process env; a missing or
/// malformed `traceparent` is a no-op (propagation is optional, SEP-414).
fn inject_trace_context(
    meta: &mut serde_json::Map<String, Value>,
    traceparent: Option<&str>,
    tracestate: Option<&str>,
) {
    let Some(raw) = traceparent else {
        return;
    };
    let Ok(parent) = crate::otel::TraceParent::parse(raw) else {
        return;
    };
    let state = tracestate
        .filter(|s| !s.is_empty())
        .map(|s| crate::otel::TraceState(s.to_string()));
    crate::otel::inject_into_meta(meta, &parent, state.as_ref());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn build_request_meta_carries_version_client_info_and_capabilities() {
        // The stateless transport attaches this block to every
        // request. The shape must match what `initialize` sends today so
        // a server can treat them identically.
        let caps = ClientCapabilities {
            name: "mcptest".to_owned(),
            version: "1.0.0".to_owned(),
            capabilities: json!({ "experimental": { "x": true } }),
        };
        let meta = build_request_meta(ProtocolVersion::V2026_07_28, &caps);
        assert_eq!(meta["protocolVersion"], json!("2026-07-28"));
        assert_eq!(meta["clientInfo"]["name"], json!("mcptest"));
        assert_eq!(meta["clientInfo"]["version"], json!("1.0.0"));
        assert_eq!(
            meta["capabilities"],
            json!({ "experimental": { "x": true } })
        );
    }

    #[test]
    fn inject_trace_context_propagates_a_valid_traceparent() {
        let mut meta = serde_json::Map::new();
        inject_trace_context(
            &mut meta,
            Some("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
            Some("vendor1=value"),
        );
        assert_eq!(
            meta["traceparent"],
            json!("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
        );
        assert_eq!(meta["tracestate"], json!("vendor1=value"));
    }

    #[test]
    fn inject_trace_context_is_a_no_op_for_a_missing_or_malformed_parent() {
        let mut meta = serde_json::Map::new();
        inject_trace_context(&mut meta, None, None);
        assert!(meta.is_empty());
        // A malformed parent must not be propagated.
        inject_trace_context(&mut meta, Some("not-a-traceparent"), None);
        assert!(meta.is_empty());
    }
}
