//! Parser for the top-level `scenarios:` block.
//!
//! Lifts each scenario from the schema-validated YAML into the typed
//! [`crate::scenario::Scenario`] the engine consumes. The world-rule leaves
//! (transitions, guards, effects, forbidden rules, signals, golden path) are
//! plain data with serde derives, so they deserialize directly; the top-level
//! scenario carries an `expect:` assertion list and an optional `rubric:`
//! that go through the shared suite parsers, so those are wired by hand here.

use std::collections::BTreeMap;
use std::path::PathBuf;

use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue;

use super::test_parse::{
    parse_expectations, parse_skip_only, parse_tags, require_str, yaml_to_json,
};
use super::ParseError;
use crate::scenario::{
    EscalationSignals, ForbiddenRule, GoldenTrajectory, Scenario, TextSignals, Transition,
};

/// Parse the optional `scenarios:` block into typed scenarios.
///
/// A missing key returns an empty vector so existing suites are unchanged.
pub(super) fn parse(root: &YamlValue) -> Result<Vec<Scenario>, ParseError> {
    let Some(node) = root.get("scenarios") else {
        return Ok(Vec::new());
    };
    let seq = node.as_sequence().ok_or(ParseError::BadShape {
        path: "scenarios".into(),
        field: "scenarios",
        expected: "a sequence of scenario entries",
    })?;
    seq.iter()
        .enumerate()
        .map(|(i, entry)| parse_one(entry, &format!("scenarios[{i}]")))
        .collect()
}

/// Parse a single scenario entry.
fn parse_one(entry: &YamlValue, path: &str) -> Result<Scenario, ParseError> {
    let name = require_str(entry, path, "name")?.to_string();
    let seed = match entry.get("seed") {
        Some(node) => yaml_to_json(node)?,
        None => JsonValue::Object(serde_json::Map::new()),
    };
    let transitions: Vec<Transition> = seq_field(entry, "transitions", path)?;
    let forbidden: Vec<ForbiddenRule> = seq_field(entry, "forbidden", path)?;
    let refusal: Option<TextSignals> = opt_field(entry, "refusal", path)?;
    let escalation: Option<EscalationSignals> = opt_field(entry, "escalation", path)?;
    let golden: Option<GoldenTrajectory> = opt_field(entry, "golden", path)?;
    let expect_state = parse_expect_state(entry, path)?;
    let expect = parse_expectations(entry, path)?;
    let rubric = match entry.get("rubric") {
        Some(node) => Some(
            crate::eval::parse_rubric(node, &format!("{path}/rubric")).map_err(|e| {
                ParseError::BadRubric {
                    path: e.path,
                    detail: e.message,
                }
            })?,
        ),
        None => None,
    };
    let cassette = entry
        .get("cassette")
        .and_then(|v| v.as_str())
        .map(PathBuf::from);
    let tags = parse_tags(entry, path)?;
    let (skip, _only) = parse_skip_only(entry, path)?;

    Ok(Scenario {
        name,
        seed,
        transitions,
        forbidden,
        refusal,
        escalation,
        golden,
        expect_state,
        expect,
        rubric,
        cassette,
        tags,
        skip,
    })
}

/// Deserialize a sequence-valued field, defaulting to empty when absent.
fn seq_field<T: DeserializeOwned>(
    entry: &YamlValue,
    field: &'static str,
    path: &str,
) -> Result<Vec<T>, ParseError> {
    match entry.get(field) {
        None => Ok(Vec::new()),
        Some(node) => from_yaml(node, field, path),
    }
}

/// Deserialize an optional object-valued field.
fn opt_field<T: DeserializeOwned>(
    entry: &YamlValue,
    field: &'static str,
    path: &str,
) -> Result<Option<T>, ParseError> {
    match entry.get(field) {
        None => Ok(None),
        Some(node) => Ok(Some(from_yaml(node, field, path)?)),
    }
}

/// Run serde over a YAML subtree, mapping the failure to [`ParseError::BadScenario`].
fn from_yaml<T: DeserializeOwned>(
    node: &YamlValue,
    field: &str,
    path: &str,
) -> Result<T, ParseError> {
    serde_yaml::from_value(node.clone()).map_err(|e| ParseError::BadScenario {
        path: format!("{path}/{field}"),
        detail: e.to_string(),
    })
}

/// Parse the `expect_state:` map of dotted path to expected JSON value.
fn parse_expect_state(
    entry: &YamlValue,
    path: &str,
) -> Result<BTreeMap<String, JsonValue>, ParseError> {
    let Some(node) = entry.get("expect_state") else {
        return Ok(BTreeMap::new());
    };
    let map = node.as_mapping().ok_or(ParseError::BadShape {
        path: format!("{path}/expect_state"),
        field: "expect_state",
        expected: "a mapping of dotted state path to expected value",
    })?;
    let mut out = BTreeMap::new();
    for (key, value) in map {
        let key = key.as_str().ok_or(ParseError::BadShape {
            path: format!("{path}/expect_state"),
            field: "expect_state",
            expected: "string keys (dotted state paths)",
        })?;
        out.insert(key.to_string(), yaml_to_json(value)?);
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::scenario::{EffectKind, EffectOp};

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

    #[test]
    fn parses_a_full_scenario() {
        let root = yaml(
            r#"
scenarios:
  - name: restock the shelf
    cassette: cassettes/restock.json
    seed:
      inventory: { widgets: 3 }
      shelf_full: false
    transitions:
      - tool: add_widget
        effect:
          inventory.widgets: { inc: 1 }
      - tool: remove_widget
        when:
          inventory.widgets: { min: 1 }
        effect:
          inventory.widgets: { dec: 1 }
    forbidden:
      - tool: drop_inventory
        reason: destructive bulk delete is never allowed
    refusal:
      markers: ["I can't"]
    escalation:
      tools: [escalate_to_human]
    golden:
      calls: [add_widget, add_widget]
      alternates:
        - [add_widget, add_widget, add_widget, remove_widget]
    expect_state:
      inventory.widgets: 5
      shelf_full: false
    expect:
      - target: invalid_actions
        matcher: { exact: 0 }
"#,
        );
        let scenarios = parse(&root).expect("parse ok");
        assert_eq!(scenarios.len(), 1);
        let s = &scenarios[0];
        assert_eq!(s.name, "restock the shelf");
        assert_eq!(
            s.cassette.as_deref().unwrap().to_str().unwrap(),
            "cassettes/restock.json"
        );
        assert_eq!(s.transitions.len(), 2);
        assert_eq!(
            s.transitions[0].effect.get("inventory.widgets"),
            Some(&EffectOp::Kind(EffectKind::Inc(1.0)))
        );
        assert_eq!(s.forbidden.len(), 1);
        assert_eq!(s.golden.as_ref().unwrap().alternates.len(), 1);
        assert_eq!(
            s.expect_state.get("inventory.widgets"),
            Some(&serde_json::json!(5))
        );
        assert_eq!(s.expect.len(), 1);
    }

    #[test]
    fn absent_block_parses_to_empty() {
        assert!(parse(&yaml("servers: {}")).unwrap().is_empty());
    }

    #[test]
    fn a_malformed_transition_is_a_clear_error() {
        let root = yaml(
            r#"
scenarios:
  - name: bad
    transitions:
      - effect: { x: 1 }
"#,
        );
        let err = parse(&root).expect_err("missing tool field");
        assert!(matches!(err, ParseError::BadScenario { .. }), "{err:?}");
    }
}
