//! Parse the top-level `security:` block into the suite security model.
//!
//! Lives alongside `suite::mod` so the parser file stays under the
//! production-module-size cap. The YAML shape is
//! `security: { scan: <bool>, trust_boundary: { allowed_flows: [{from, to}],
//! required_sanitizers: [..] }, capability_negotiation: {...} }`. The `scan`
//! flag opts into the deterministic security scan lane and `trust_boundary` is
//! lowered into an assertion; the `capability_negotiation` sub-block targets the
//! forward stateless model and is ignored here (the runner does not yet honor
//! it). The detector for the trust boundary is the deterministic
//! [`crate::security::cross_server_trust_findings`].

use serde_yaml::Value as YamlValue;

use super::types::SecurityAssertions;
use super::ParseError;
use crate::security::TrustBoundaryAssertion;

/// Parse the optional top-level `security:` block. Returns `None` when the key
/// is absent, leaving every suite unchanged. A non-object `security:` value, or
/// a malformed `trust_boundary`, is a load error so a typo never silently drops
/// the assertion.
pub(super) fn parse(root: &YamlValue) -> Result<Option<SecurityAssertions>, ParseError> {
    let Some(node) = root.get("security") else {
        return Ok(None);
    };
    if node.as_mapping().is_none() {
        return Err(ParseError::BadShape {
            path: "/security".to_string(),
            field: "security",
            expected: "an object",
        });
    }
    let trust_boundary = parse_trust_boundary(node)?;
    let scan = parse_scan(node)?;
    Ok(Some(SecurityAssertions {
        trust_boundary,
        scan,
    }))
}

/// Parse the optional `security.scan` flag. Absent or `false` leaves the scan
/// lane off; `true` opts the suite into the deterministic security scan over
/// its server tool surface. A non-boolean value is a load error so a typo never
/// silently disables the lane.
fn parse_scan(security: &YamlValue) -> Result<bool, ParseError> {
    let Some(node) = security.get("scan") else {
        return Ok(false);
    };
    node.as_bool().ok_or_else(|| ParseError::BadShape {
        path: "/security/scan".to_string(),
        field: "scan",
        expected: "a boolean",
    })
}

/// Parse the `security.trust_boundary` sub-block. Returns `None` when absent.
/// Maps each `allowed_flows` entry `{from, to}` to a directed
/// `(producer_server, consumer_server)` pair and `required_sanitizers` to a
/// string list.
fn parse_trust_boundary(
    security: &YamlValue,
) -> Result<Option<TrustBoundaryAssertion>, ParseError> {
    let Some(node) = security.get("trust_boundary") else {
        return Ok(None);
    };
    if node.as_mapping().is_none() {
        return Err(ParseError::BadShape {
            path: "/security/trust_boundary".to_string(),
            field: "trust_boundary",
            expected: "an object",
        });
    }
    let allowed_flows = parse_allowed_flows(node)?;
    let required_sanitizers = parse_required_sanitizers(node)?;
    Ok(Some(TrustBoundaryAssertion {
        allowed_flows,
        required_sanitizers,
    }))
}

/// Parse `trust_boundary.allowed_flows` into directed `(from, to)` pairs. A
/// missing key yields an empty list. Each entry must be an object with non-empty
/// `from` and `to` strings.
fn parse_allowed_flows(node: &YamlValue) -> Result<Vec<(String, String)>, ParseError> {
    let Some(value) = node.get("allowed_flows") else {
        return Ok(Vec::new());
    };
    let seq = value.as_sequence().ok_or(ParseError::BadShape {
        path: "/security/trust_boundary/allowed_flows".to_string(),
        field: "allowed_flows",
        expected: "a sequence of {from, to} objects",
    })?;
    let mut out = Vec::with_capacity(seq.len());
    for (i, item) in seq.iter().enumerate() {
        let path = format!("/security/trust_boundary/allowed_flows[{i}]");
        let from = require_flow_field(item, "from", &path)?;
        let to = require_flow_field(item, "to", &path)?;
        out.push((from, to));
    }
    Ok(out)
}

/// Read a required non-empty string field off an `allowed_flows` entry.
fn require_flow_field(
    item: &YamlValue,
    field: &'static str,
    path: &str,
) -> Result<String, ParseError> {
    let raw = item
        .get(field)
        .and_then(YamlValue::as_str)
        .ok_or(ParseError::BadShape {
            path: path.to_string(),
            field,
            expected: "a non-empty server-name string",
        })?;
    if raw.is_empty() {
        return Err(ParseError::BadShape {
            path: path.to_string(),
            field,
            expected: "a non-empty server-name string",
        });
    }
    Ok(raw.to_string())
}

/// Parse `trust_boundary.required_sanitizers` into a string list. A missing key
/// yields an empty list. Each entry must be a non-empty string.
fn parse_required_sanitizers(node: &YamlValue) -> Result<Vec<String>, ParseError> {
    let Some(value) = node.get("required_sanitizers") else {
        return Ok(Vec::new());
    };
    let seq = value.as_sequence().ok_or(ParseError::BadShape {
        path: "/security/trust_boundary/required_sanitizers".to_string(),
        field: "required_sanitizers",
        expected: "a sequence of sanitizer-name strings",
    })?;
    let mut out = Vec::with_capacity(seq.len());
    for (i, item) in seq.iter().enumerate() {
        let name = item.as_str().ok_or(ParseError::BadShape {
            path: format!("/security/trust_boundary/required_sanitizers[{i}]"),
            field: "required_sanitizers",
            expected: "a sanitizer-name string",
        })?;
        out.push(name.to_string());
    }
    Ok(out)
}

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

    fn yaml(src: &str) -> YamlValue {
        serde_yaml::from_str(src).unwrap()
    }

    #[test]
    fn parses_trust_boundary_into_assertion() {
        let root = yaml(
            "security:\n  trust_boundary:\n    allowed_flows:\n      - from: a\n        to: b\n    required_sanitizers:\n      - clean\n",
        );
        let parsed = parse(&root).unwrap().unwrap();
        let tb = parsed.trust_boundary.unwrap();
        assert_eq!(tb.allowed_flows, vec![("a".to_string(), "b".to_string())]);
        assert_eq!(tb.required_sanitizers, vec!["clean".to_string()]);
    }

    #[test]
    fn absent_security_is_none() {
        let root = yaml("servers:\n  s: { command: [\"echo\"] }\n");
        assert!(parse(&root).unwrap().is_none());
    }

    #[test]
    fn security_without_trust_boundary_yields_none_boundary() {
        let root = yaml("security:\n  capability_negotiation:\n    target: \"2026-07-28\"\n");
        let parsed = parse(&root).unwrap().unwrap();
        assert!(parsed.trust_boundary.is_none());
    }

    #[test]
    fn empty_trust_boundary_parses_to_empty_assertion() {
        let root = yaml("security:\n  trust_boundary: {}\n");
        let tb = parse(&root).unwrap().unwrap().trust_boundary.unwrap();
        assert!(tb.allowed_flows.is_empty());
        assert!(tb.required_sanitizers.is_empty());
    }

    #[test]
    fn non_object_security_is_a_load_error() {
        let root = yaml("security: 7\n");
        assert!(parse(&root).is_err());
    }

    #[test]
    fn scan_defaults_to_false_when_absent() {
        let root = yaml("security:\n  trust_boundary: {}\n");
        assert!(!parse(&root).unwrap().unwrap().scan);
    }

    #[test]
    fn scan_true_opts_into_the_security_lane() {
        let root = yaml("security:\n  scan: true\n");
        assert!(parse(&root).unwrap().unwrap().scan);
    }

    #[test]
    fn non_boolean_scan_is_a_load_error() {
        let root = yaml("security:\n  scan: yep\n");
        assert!(parse(&root).is_err());
    }

    #[test]
    fn allowed_flow_missing_to_is_a_load_error() {
        let root = yaml("security:\n  trust_boundary:\n    allowed_flows:\n      - from: a\n");
        assert!(parse(&root).is_err());
    }
}
