//! Parse the elicitation answer-source config (WOR-1383).
//!
//! A 2026-07-28 server can answer a `tools/call` with an
//! `InputRequiredResult` (SEP-2322); the runner satisfies it from a
//! configured answer source and retries. Two shapes are parsed here:
//!
//! - a test-level `input_responses:` static map (`id` to value) or
//!   `input_responder:` provider (mutually exclusive), and
//! - a suite-level `input_responder:` default.
//!
//! Pulled out of [`super::test_parse`] so that module stays under the
//! AGENTS module-size ceiling. The runtime responder (which holds a
//! `reqwest::Client`) is built from these plain specs at plan-build time,
//! not here.

use std::collections::BTreeMap;

use serde_yaml::Value as YamlValue;

use super::test_parse::yaml_to_json;
use super::types::{InputResponderSpec, InputResponseSpec, RestInputResponderSpec};
use super::ParseError;

/// Parse a tool test's `input_responses` (static map) and
/// `input_responder` (provider). The two are mutually exclusive: a test
/// declares at most one answer source.
pub(super) fn parse_test_input_config(
    entry: &YamlValue,
    path: &str,
) -> Result<(Option<InputResponseSpec>, Option<InputResponderSpec>), ParseError> {
    let responses = entry
        .get("input_responses")
        .map(|node| parse_input_responses(node, path))
        .transpose()?;
    let responder = entry
        .get("input_responder")
        .map(|node| parse_input_responder(node, path))
        .transpose()?;
    if responses.is_some() && responder.is_some() {
        return Err(ParseError::BadShape {
            path: path.to_string(),
            field: "input_responder",
            expected: "absent when `input_responses` is set (declare one elicitation answer source, not both)",
        });
    }
    Ok((responses, responder))
}

/// Parse the optional suite-level `input_responder:` default.
pub(super) fn parse_suite_input_responder(
    root: &YamlValue,
) -> Result<Option<InputResponderSpec>, ParseError> {
    root.get("input_responder")
        .map(|node| parse_input_responder(node, "/input_responder"))
        .transpose()
}

/// Parse `input_responses:` into a `{ id: value }` map. Values are
/// coerced through the shared YAML-to-JSON path so they line up with the
/// wire `inputResponses[].value`.
fn parse_input_responses(node: &YamlValue, path: &str) -> Result<InputResponseSpec, ParseError> {
    let map = node.as_mapping().ok_or(ParseError::BadShape {
        path: path.to_string(),
        field: "input_responses",
        expected: "a mapping of inputRequest id to answer value",
    })?;
    let mut values = BTreeMap::new();
    for (key, value) in map {
        let id = key.as_str().ok_or(ParseError::BadShape {
            path: path.to_string(),
            field: "input_responses",
            expected: "string keys (inputRequest ids)",
        })?;
        values.insert(id.to_string(), yaml_to_json(value)?);
    }
    Ok(InputResponseSpec { values })
}

/// Parse `input_responder: { rest: { ... } }`. Only the `rest` provider
/// exists today; an unknown provider key is a load-time error.
fn parse_input_responder(node: &YamlValue, path: &str) -> Result<InputResponderSpec, ParseError> {
    let map = node.as_mapping().ok_or(ParseError::BadShape {
        path: path.to_string(),
        field: "input_responder",
        expected: "a mapping with a `rest` provider",
    })?;
    let rest = map
        .get(YamlValue::from("rest"))
        .ok_or(ParseError::BadShape {
            path: path.to_string(),
            field: "input_responder",
            expected: "a `rest` provider block",
        })?;
    Ok(InputResponderSpec::Rest(parse_rest(rest, path)?))
}

fn parse_rest(node: &YamlValue, path: &str) -> Result<RestInputResponderSpec, ParseError> {
    let url = node
        .get("url")
        .and_then(|u| u.as_str())
        .ok_or(ParseError::BadShape {
            path: path.to_string(),
            field: "input_responder.rest.url",
            expected: "string (the endpoint the runner POSTs elicitations to)",
        })?
        .to_string();
    let headers = node
        .get("headers")
        .map(|h| parse_headers(h, path))
        .transpose()?
        .unwrap_or_default();
    let timeout_ms = match node.get("timeout_ms") {
        Some(t) => Some(t.as_u64().ok_or(ParseError::BadShape {
            path: path.to_string(),
            field: "input_responder.rest.timeout_ms",
            expected: "a non-negative integer (milliseconds)",
        })?),
        None => None,
    };
    Ok(RestInputResponderSpec {
        url,
        headers,
        timeout_ms,
    })
}

fn parse_headers(node: &YamlValue, path: &str) -> Result<BTreeMap<String, String>, ParseError> {
    let map = node.as_mapping().ok_or(ParseError::BadShape {
        path: path.to_string(),
        field: "input_responder.rest.headers",
        expected: "a mapping of header name to value",
    })?;
    let mut headers = BTreeMap::new();
    for (key, value) in map {
        let name = key.as_str().ok_or(ParseError::BadShape {
            path: path.to_string(),
            field: "input_responder.rest.headers",
            expected: "string header names",
        })?;
        let value = value.as_str().ok_or(ParseError::BadShape {
            path: path.to_string(),
            field: "input_responder.rest.headers",
            expected: "string header values",
        })?;
        headers.insert(name.to_string(), value.to_string());
    }
    Ok(headers)
}

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

    #[test]
    fn parses_static_input_responses_into_a_value_map() {
        let entry: YamlValue =
            from_str("input_responses:\n  destination: Denver\n  seats: 2\n").unwrap();
        let (responses, responder) = parse_test_input_config(&entry, "/tools/0").unwrap();
        assert!(responder.is_none());
        let spec = responses.expect("input_responses parsed");
        assert_eq!(spec.values["destination"], serde_json::json!("Denver"));
        assert_eq!(spec.values["seats"], serde_json::json!(2));
    }

    #[test]
    fn parses_rest_responder_with_headers_and_timeout() {
        let entry: YamlValue = from_str(
            "input_responder:\n  rest:\n    url: https://x/answer\n    timeout_ms: 5000\n    headers:\n      Authorization: Bearer t\n",
        )
        .unwrap();
        let (responses, responder) = parse_test_input_config(&entry, "/tools/0").unwrap();
        assert!(responses.is_none());
        match responder.expect("responder parsed") {
            InputResponderSpec::Rest(rest) => {
                assert_eq!(rest.url, "https://x/answer");
                assert_eq!(rest.timeout_ms, Some(5000));
                assert_eq!(rest.headers["Authorization"], "Bearer t");
            }
        }
    }

    #[test]
    fn rejects_a_test_that_sets_both_sources() {
        let entry: YamlValue =
            from_str("input_responses:\n  a: b\ninput_responder:\n  rest:\n    url: https://x\n")
                .unwrap();
        let err = parse_test_input_config(&entry, "/tools/0").unwrap_err();
        assert!(matches!(
            err,
            ParseError::BadShape {
                field: "input_responder",
                ..
            }
        ));
    }

    #[test]
    fn rest_provider_requires_a_url() {
        let entry: YamlValue = from_str("input_responder:\n  rest:\n    headers: {}\n").unwrap();
        assert!(parse_test_input_config(&entry, "/tools/0").is_err());
    }

    #[test]
    fn suite_level_responder_is_optional() {
        let none: YamlValue = from_str("servers: {}\n").unwrap();
        assert!(parse_suite_input_responder(&none).unwrap().is_none());
        let some: YamlValue = from_str("input_responder:\n  rest:\n    url: https://x\n").unwrap();
        assert!(parse_suite_input_responder(&some).unwrap().is_some());
    }
}
