//! Integration tests for the composition load-time validator.
//!
//! Exercises [`mcptest_core::suite::parse`] against compositions that
//! exercise the four failure modes the validator is supposed to catch
//! (duplicate id, unknown need, undeclared reference, cycle, unknown
//! output) plus the happy path. The point of doing this through
//! `suite::parse` instead of the in-crate unit tests is to lock the
//! wire-up between the YAML loader and the validator: a regression
//! that drops the `parse_compositions` call would still pass the
//! unit tests but fail here.

use mcptest_core::composition::OutputSpec;
use mcptest_core::suite::{parse, ParseError};
use serde_yaml::Value;

fn load(yaml: &str) -> Result<mcptest_core::suite::ResolvedSuite, ParseError> {
    let value: Value = serde_yaml::from_str(yaml).unwrap();
    parse(&value)
}

#[test]
fn happy_path_round_trips_through_the_loader() {
    let yaml = r#"
servers:
  m:
    command: ["echo"]
compositions:
  - name: pipe
    nodes:
      - id: a
        tool: t
        args: { q: "q" }
      - id: b
        needs: [a]
        tool: u
        args: { x: "${a.id}" }
    output: b
"#;
    let suite = load(yaml).expect("composition parses");
    assert_eq!(suite.compositions.len(), 1);
    let c = &suite.compositions[0];
    assert_eq!(c.name, "pipe");
    assert_eq!(c.topo_order, vec!["a", "b"]);
    assert!(
        matches!(&c.output, Some(OutputSpec::Single(id)) if id == "b"),
        "output should be single(b), got {:?}",
        c.output
    );
}

#[test]
fn missing_compositions_block_yields_empty_vec() {
    let yaml = r#"
servers:
  m:
    command: ["echo"]
"#;
    let suite = load(yaml).expect("loads without compositions");
    assert!(suite.compositions.is_empty());
}

#[test]
fn duplicate_node_id_is_a_load_time_error() {
    let yaml = r#"
servers:
  m:
    command: ["echo"]
compositions:
  - name: dup
    nodes:
      - id: a
        tool: t
      - id: a
        tool: u
"#;
    let err = load(yaml).expect_err("duplicate id rejected");
    match err {
        ParseError::BadComposition { detail, path } => {
            assert_eq!(path, "compositions[0]");
            assert!(detail.contains("declares node `a` twice"), "{detail}");
        }
        other => panic!("expected BadComposition, got {other:?}"),
    }
}

#[test]
fn unknown_need_is_a_load_time_error() {
    let yaml = r#"
servers:
  m:
    command: ["echo"]
compositions:
  - name: lost
    nodes:
      - id: b
        needs: [a]
        tool: u
"#;
    let err = load(yaml).expect_err("unknown need rejected");
    match err {
        ParseError::BadComposition { detail, .. } => {
            assert!(detail.contains("not declared"), "{detail}");
        }
        other => panic!("expected BadComposition, got {other:?}"),
    }
}

#[test]
fn undeclared_reference_is_a_load_time_error() {
    let yaml = r#"
servers:
  m:
    command: ["echo"]
compositions:
  - name: lazy
    nodes:
      - id: a
        tool: t
      - id: b
        tool: u
        args: { x: "${a.id}" }
"#;
    let err = load(yaml).expect_err("undeclared reference rejected");
    match err {
        ParseError::BadComposition { detail, .. } => {
            assert!(
                detail.contains("${a.id}") && detail.contains("needs:"),
                "{detail}"
            );
        }
        other => panic!("expected BadComposition, got {other:?}"),
    }
}

#[test]
fn cycle_is_a_load_time_error() {
    let yaml = r#"
servers:
  m:
    command: ["echo"]
compositions:
  - name: cyc
    nodes:
      - id: a
        needs: [b]
        tool: t
      - id: b
        needs: [a]
        tool: u
"#;
    let err = load(yaml).expect_err("cycle rejected");
    match err {
        ParseError::BadComposition { detail, .. } => {
            assert!(detail.contains("cycle through node"), "{detail}");
        }
        other => panic!("expected BadComposition, got {other:?}"),
    }
}

#[test]
fn unknown_output_node_is_a_load_time_error() {
    let yaml = r#"
servers:
  m:
    command: ["echo"]
compositions:
  - name: out
    output: nope
    nodes:
      - id: a
        tool: t
"#;
    let err = load(yaml).expect_err("unknown output rejected");
    match err {
        ParseError::BadComposition { detail, .. } => {
            assert!(detail.contains("output: nope"), "{detail}");
        }
        other => panic!("expected BadComposition, got {other:?}"),
    }
}

#[test]
fn shipped_example_round_trips_through_the_loader() {
    let yaml = include_str!("../../../examples/composition-pipeline.yml");
    // The example references a mock-registry server that the schema
    // accepts even though the binary does not exist in this test
    // process; the validator runs without dispatching.
    let suite = load(yaml).expect("shipped example loads");
    assert_eq!(suite.compositions.len(), 1);
    let c = &suite.compositions[0];
    assert_eq!(c.name, "render-top-readme");
    assert_eq!(c.topo_order, vec!["search", "readme"]);
    assert!(
        matches!(&c.output, Some(OutputSpec::Single(id)) if id == "readme"),
        "output should be single(readme), got {:?}",
        c.output
    );
}
