//! `InputRequiredResult` elicitation flow.
//!
//! The 2026-07-28 spec replaces the experimental Tasks-shaped
//! elicitation path with a stateless multi-round-trip exchange: a
//! server that needs additional input returns an `InputRequiredResult`
//! carrying one or more [`InputRequest`]s plus an opaque
//! [`RequestState`]. The client re-issues the original method with the
//! same arguments plus an `inputResponses` field and the echoed
//! `requestState`. Any server instance can pick the retry up because
//! the state travels in the request, not in a per-session table.
//!
//! v1 ships the typed wire model plus the recognizer that detects an
//! `InputRequiredResult` response. The runner integration (auto-prompt
//! the user, build the retry request, replay through the executor)
//! and the cassette-format extension defer to v2 once
//!  lands the streamable-HTTP transport routing.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Opaque continuation token the server hands back in
/// `InputRequiredResult` and the client echoes verbatim on the retry.
///
/// Carried as a typed wrapper so the runner cannot accidentally treat
/// it as a plain string and so the cassette serializer can round-trip
/// it without colliding with another `String` field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RequestState(pub String);

impl RequestState {
    /// Borrow the wire form.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// One field the server needs the client to fill in before it can
/// finish the call.
///
/// The `kind` field carries the spec's three primitives (`string`,
/// `number`, `boolean`); a server is free to send other values, in
/// which case the runner surfaces them as a fail-with-reason rather
/// than guessing the shape. The optional `description` is what the
/// runner prompts the user with.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InputRequest {
    /// Stable identifier the client echoes back under the matching
    /// `InputResponse.id`. Must be unique within an
    /// `InputRequiredResult`.
    pub id: String,
    /// Field kind. The 2026-07-28 spec defines `string`, `number`, and
    /// `boolean`. Other values are documented as future-reserved.
    pub kind: String,
    /// Optional human-readable description the runner prompts the user
    /// with.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Whether the input is required. A non-required field may be
    /// omitted from the corresponding `InputResponse`. Defaults to
    /// `true` per the SEP.
    #[serde(default = "default_required")]
    pub required: bool,
}

fn default_required() -> bool {
    true
}

/// One filled-in field the client sends on the retry.
///
/// `id` matches an [`InputRequest::id`] from the prior
/// `InputRequiredResult`. `value` is the user's answer, typed against
/// the request's `kind`. The runner validates the value matches the
/// requested kind before constructing the retry envelope.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InputResponse {
    /// Matches the [`InputRequest::id`] this response answers.
    pub id: String,
    /// User answer, typed as a free JSON `Value` so the wire shape
    /// supports the spec's `string` / `number` / `boolean` primitives
    /// (and any future-reserved kinds the server documents).
    pub value: Value,
}

/// Server-side `result` payload that asks the client for more input.
///
/// A method call's response carries this shape inside its `result`
/// field when the server cannot finish without additional input. The
/// recognizer ([`recognize_input_required`]) reads the response value,
/// returns this struct when it matches, and returns `None` when the
/// response is a normal result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InputRequiredResult {
    /// Fields the server needs filled in.
    pub input_requests: Vec<InputRequest>,
    /// Opaque continuation token the client must echo on the retry.
    pub request_state: RequestState,
    /// Optional description the runner prompts the user with at the
    /// `InputRequiredResult` level (in addition to any per-request
    /// descriptions).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

/// Recognize an `InputRequiredResult` inside a response envelope.
///
/// The 2026-07-28 spec marks an `InputRequiredResult` by:
///
/// 1. `result.inputRequests` is a non-empty array.
/// 2. `result.requestState` is a non-empty string.
///
/// Both must be present and well-typed; missing or empty either
/// field treats the response as a normal result so the runner does
/// not loop on a half-shaped server. Returns the typed struct when
/// the recognition matches.
pub fn recognize_input_required(response: &Value) -> Option<InputRequiredResult> {
    let result = response.get("result")?;
    let parsed: InputRequiredResult = serde_json::from_value(result.clone()).ok()?;
    if parsed.input_requests.is_empty() {
        return None;
    }
    if parsed.request_state.as_str().is_empty() {
        return None;
    }
    Some(parsed)
}

/// Build the `params` block the client sends on the retry.
///
/// Takes the original `params` value, the echoed [`RequestState`], and
/// the answers the user (or fixture) supplied. The new params is a
/// copy of the original with two additional fields: `inputResponses`
/// (the answer list) and `requestState` (the echoed token).
///
/// The original args stay intact so the server treats the retry as
/// the same logical call. Validation that every required
/// `InputRequest` has a matching `InputResponse` is the caller's
/// responsibility today; v2 will move it into this builder once the
/// runner integration lands.
pub fn build_retry_params(
    original_params: &Value,
    request_state: &RequestState,
    responses: &[InputResponse],
) -> Value {
    let mut params = original_params.clone();
    if !params.is_object() {
        // The MCP wire shape says params is always an object on the
        // methods that elicit (`tools/call`, `resources/read`, ...).
        // If a caller hands us a non-object value, wrap it so the
        // returned shape stays serializable.
        params = serde_json::json!({});
    }
    let map = params.as_object_mut().expect("params is an object by here");
    map.insert(
        "inputResponses".to_string(),
        serde_json::to_value(responses).unwrap_or(Value::Null),
    );
    map.insert(
        "requestState".to_string(),
        Value::String(request_state.0.clone()),
    );
    params
}

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

    fn sample_irr_envelope() -> Value {
        json!({
            "jsonrpc": "2.0",
            "id": 1,
            "result": {
                "inputRequests": [
                    {
                        "id": "city",
                        "kind": "string",
                        "description": "What city?",
                        "required": true,
                    },
                    {
                        "id": "units",
                        "kind": "string",
                        "description": "metric or imperial?",
                        "required": false,
                    }
                ],
                "requestState": "opaque-token-abc-123",
                "description": "Need the city before I can fetch the forecast."
            }
        })
    }

    #[test]
    fn recognize_returns_typed_struct_for_a_well_formed_response() {
        let envelope = sample_irr_envelope();
        let irr = recognize_input_required(&envelope).expect("recognized");
        assert_eq!(irr.input_requests.len(), 2);
        assert_eq!(irr.input_requests[0].id, "city");
        assert_eq!(irr.input_requests[0].kind, "string");
        assert!(irr.input_requests[0].required);
        assert!(!irr.input_requests[1].required);
        assert_eq!(irr.request_state.as_str(), "opaque-token-abc-123");
    }

    #[test]
    fn recognize_returns_none_for_a_plain_result_envelope() {
        let envelope = json!({
            "jsonrpc": "2.0",
            "id": 1,
            "result": { "content": [{"type": "text", "text": "ok"}] }
        });
        assert!(recognize_input_required(&envelope).is_none());
    }

    #[test]
    fn recognize_rejects_missing_request_state() {
        let envelope = json!({
            "jsonrpc": "2.0",
            "id": 1,
            "result": {
                "inputRequests": [{"id": "x", "kind": "string"}],
                "requestState": ""
            }
        });
        assert!(recognize_input_required(&envelope).is_none());
    }

    #[test]
    fn recognize_rejects_empty_input_requests_array() {
        let envelope = json!({
            "jsonrpc": "2.0",
            "id": 1,
            "result": {
                "inputRequests": [],
                "requestState": "ok"
            }
        });
        assert!(recognize_input_required(&envelope).is_none());
    }

    #[test]
    fn recognize_returns_none_when_response_has_no_result_field() {
        let envelope = json!({"jsonrpc": "2.0", "id": 1, "error": {"code": -32600}});
        assert!(recognize_input_required(&envelope).is_none());
    }

    #[test]
    fn input_request_required_defaults_to_true_when_omitted() {
        let raw = json!({"id": "x", "kind": "string"});
        let parsed: InputRequest = serde_json::from_value(raw).unwrap();
        assert!(parsed.required);
        assert_eq!(parsed.id, "x");
        assert_eq!(parsed.kind, "string");
        assert!(parsed.description.is_none());
    }

    #[test]
    fn build_retry_params_appends_input_responses_and_echoes_state() {
        let original = json!({"name": "weather", "arguments": {"loc": "SFO"}});
        let state = RequestState("token-99".to_string());
        let responses = vec![InputResponse {
            id: "city".into(),
            value: json!("San Francisco"),
        }];
        let retry = build_retry_params(&original, &state, &responses);
        assert_eq!(retry["name"], json!("weather"));
        assert_eq!(retry["arguments"], json!({"loc": "SFO"}));
        assert_eq!(retry["requestState"], json!("token-99"));
        assert_eq!(
            retry["inputResponses"],
            json!([{"id": "city", "value": "San Francisco"}])
        );
    }

    #[test]
    fn build_retry_params_wraps_a_non_object_original_so_the_retry_is_still_an_object() {
        // Defensive path: a malformed caller hands us `null` instead
        // of a params object. The retry must still serialize as a
        // valid request shape.
        let retry = build_retry_params(
            &json!(null),
            &RequestState("s".into()),
            &[InputResponse {
                id: "a".into(),
                value: json!(1),
            }],
        );
        assert!(retry.is_object());
        assert_eq!(retry["requestState"], json!("s"));
    }

    #[test]
    fn input_request_and_response_round_trip_through_serde() {
        let req = InputRequest {
            id: "city".into(),
            kind: "string".into(),
            description: Some("city?".into()),
            required: true,
        };
        let bytes = serde_json::to_vec(&req).unwrap();
        let parsed: InputRequest = serde_json::from_slice(&bytes).unwrap();
        assert_eq!(parsed, req);

        let resp = InputResponse {
            id: "city".into(),
            value: json!("Portland"),
        };
        let bytes = serde_json::to_vec(&resp).unwrap();
        let parsed: InputResponse = serde_json::from_slice(&bytes).unwrap();
        assert_eq!(parsed, resp);
    }

    #[test]
    fn input_required_result_uses_camel_case_wire_keys() {
        // The wire keys are `inputRequests`, `requestState`,
        // `description`. Pin them strictly so a future serde rename
        // does not silently break compatibility with a real server.
        let irr = InputRequiredResult {
            input_requests: vec![InputRequest {
                id: "x".into(),
                kind: "string".into(),
                description: None,
                required: true,
            }],
            request_state: RequestState("s".into()),
            description: None,
        };
        let value = serde_json::to_value(&irr).unwrap();
        let obj = value.as_object().unwrap();
        assert!(obj.contains_key("inputRequests"));
        assert!(obj.contains_key("requestState"));
        assert!(!obj.contains_key("input_requests"));
        assert!(!obj.contains_key("request_state"));
    }
}
