//! Integration tests for the published JSON Schema at `schemas/v1.json`.
//!
//! These tests are the load-bearing guard rail behind the schema contract:
//! if a future change to `schemas/v1.json` breaks either of the shipped
//! examples, or stops rejecting an obviously invalid config, this test fails
//! and we have to either fix the schema or own the breakage in CHANGELOG.

use std::path::{Path, PathBuf};

use serde_json::Value;

/// Walks up from the crate dir to find the workspace root (the directory
/// holding the top-level `schemas/` and `examples/` folders).
fn workspace_root() -> PathBuf {
    // CARGO_MANIFEST_DIR points at crates/mcptest/. Two parents up is the
    // workspace root. We resolve via PathBuf rather than string surgery so
    // this works on Windows as well.
    let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
    manifest
        .parent()
        .and_then(|p| p.parent())
        .expect("workspace root above crates/mcptest")
        .to_path_buf()
}

fn load_schema() -> Value {
    let path = workspace_root().join("schemas").join("v1.json");
    let raw =
        std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
    serde_json::from_str(&raw).expect("schemas/v1.json is valid JSON")
}

fn yaml_to_json(path: &Path) -> Value {
    let raw =
        std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
    let as_yaml: serde_yaml::Value = serde_yaml::from_str(&raw).expect("example YAML parses");
    serde_json::to_value(as_yaml).expect("YAML value converts to JSON")
}

fn example(name: &str) -> Value {
    yaml_to_json(&workspace_root().join("examples").join(name))
}

#[test]
fn stdio_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("server-stdio.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "server-stdio.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn named_errors_stdio_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("named-errors-stdio.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "named-errors-stdio.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn named_errors_url_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("named-errors-url.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "named-errors-url.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn url_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("server-url.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "server-url.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn server_with_both_command_and_url_is_rejected() {
    // A server entry that sets both `command` and `url` violates the oneOf
    // union; the schema must reject it.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": {
            "broken": {
                "command": ["echo", "hi"],
                "url": "https://example.com/mcp"
            }
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "server with both `command` and `url` must fail validation"
    );
}

#[test]
fn url_server_without_url_is_rejected() {
    // A URL-style entry that forgets `url` is the most common typo; make
    // sure the schema flags it.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": {
            "broken": {
                "auth": { "bearer_token_env": "TOKEN" }
            }
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "URL server missing `url` must fail validation"
    );
}

/// Helper that builds a minimal tools-style config with `expect` set to the
/// provided JSON value. Keeps each test case below focused on the expect
/// shape under exam.
fn tool_with_expect(expect: Value) -> Value {
    serde_json::json!({
        "servers": {
            "fs": { "command": ["echo", "hi"] }
        },
        "tools": [{
            "name": "t",
            "server": "fs",
            "tool": "list",
            "expect": expect
        }]
    })
}

#[test]
fn expectation_block_accepts_max_duration_ms() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = tool_with_expect(serde_json::json!({ "max_duration_ms": 500 }));
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "Expectation with max_duration_ms should validate, errors: {errors:#?}"
    );
}

#[test]
fn max_duration_ms_rejects_zero_and_negative() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let zero = tool_with_expect(serde_json::json!({ "max_duration_ms": 0 }));
    assert!(
        !validator.is_valid(&zero),
        "max_duration_ms = 0 must be rejected"
    );
    let neg = tool_with_expect(serde_json::json!({ "max_duration_ms": -10 }));
    assert!(
        !validator.is_valid(&neg),
        "negative max_duration_ms must be rejected"
    );
}

#[test]
fn max_response_tokens_accepts_short_and_long_forms() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let short = tool_with_expect(serde_json::json!({ "max_response_tokens": 200 }));
    assert!(
        validator.is_valid(&short),
        "short form max_response_tokens: N must validate"
    );
    let long = tool_with_expect(serde_json::json!({
        "max_response_tokens": {
            "budget": 200,
            "tokenizer": "whitespace",
            "mode": "warn",
            "image_cost": 750
        }
    }));
    let errors: Vec<String> = validator
        .iter_errors(&long)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "long form max_response_tokens must validate, errors: {errors:#?}"
    );
}

#[test]
fn max_response_tokens_rejects_unknown_mode() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = tool_with_expect(serde_json::json!({
        "max_response_tokens": { "budget": 100, "mode": "yell" }
    }));
    assert!(
        !validator.is_valid(&bad),
        "unknown mode value must be rejected"
    );
}

#[test]
fn response_headers_accepts_all_value_variants() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = tool_with_expect(serde_json::json!({
        "response_headers": {
            "content-type": "application/json",
            "x-trace-id": { "regex": "^trace-\\d+$" },
            "x-count": { "schema": { "type": "string" } },
            "x-custom": { "exists": true },
            "server": { "contains": "nginx" }
        },
        "response_headers_absent": ["set-cookie"]
    }));
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "response_headers variants must validate, errors: {errors:#?}"
    );
}

#[test]
fn response_headers_rejects_value_with_two_variants() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = tool_with_expect(serde_json::json!({
        "response_headers": {
            "x-trace": { "regex": "^a", "contains": "b" }
        }
    }));
    assert!(
        !validator.is_valid(&bad),
        "header value with two variants must be rejected"
    );
}

#[test]
fn matcher_accepts_each_canonical_name() {
    // The schema documents what the runner ships. Exercise every canonical
    // matcher name (including the  deterministic vocabulary and the
    // `not` wrapper) so a future rename has to update this list deliberately.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let cases = [
        serde_json::json!({ "exact": "hello" }),
        serde_json::json!({ "contains": "ello" }),
        serde_json::json!({ "regex": "^h" }),
        serde_json::json!({ "schema": { "type": "string" } }),
        serde_json::json!({ "snapshot": "greeting" }),
        serde_json::json!({
            "llm-judge": { "rubric": "Answer must be polite.", "threshold": 0.8 }
        }),
        serde_json::json!({ "contains-all": ["foo", "bar"] }),
        serde_json::json!({ "contains-any": ["foo", "bar"] }),
        serde_json::json!({ "icontains": "ELLO" }),
        serde_json::json!({ "starts-with": "hel" }),
        serde_json::json!({ "is-json": serde_json::Value::Null }),
        serde_json::json!({ "is-json": { "schema": { "type": "object" } } }),
        serde_json::json!({ "is-valid-tools-call": serde_json::Value::Null }),
        serde_json::json!({ "is-valid-tools-call": { "schema": { "type": "object" } } }),
        serde_json::json!({ "levenshtein": { "value": "sitting", "max": 3 } }),
        serde_json::json!({ "is-xml": serde_json::Value::Null }),
        serde_json::json!({ "is-xml": { "root": "note" } }),
        serde_json::json!({ "is-sql": serde_json::Value::Null }),
        serde_json::json!({
            "similar": { "value": "hi", "threshold": 0.85, "model": "text-embedding-3-small" }
        }),
        serde_json::json!({ "cel": "value.size() > 0" }),
        serde_json::json!({ "factuality": { "reference": "Paris is the capital of France" } }),
        serde_json::json!({ "answer-relevance": { "query": "What is the capital of France?" } }),
        serde_json::json!({ "context-faithfulness": { "context": "France's capital is Paris." } }),
        serde_json::json!({ "not": { "contains": "ello" } }),
    ];
    for matcher in cases {
        let good = tool_with_expect(serde_json::json!([{
            "target": "result.content[0].text",
            "matcher": matcher,
        }]));
        let errors: Vec<String> = validator
            .iter_errors(&good)
            .map(|e| e.to_string())
            .collect();
        assert!(
            errors.is_empty(),
            "canonical matcher should validate, errors: {errors:#?}"
        );
    }
}

#[test]
fn new_matchers_round_trip_parse_and_validate() {
    // Each new matcher key must both (a) parse through the suite
    // parser into its typed Matcher variant and (b) validate against the
    // published schema. Pinning both sides together catches drift between the
    // schema property names and the hand-written parser.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let matcher_yamls = [
        "contains-all: [foo, bar]",
        "contains-any: [foo, bar]",
        "icontains: World",
        "starts-with: Hello",
        "is-json: ~",
        "is-json: { schema: { type: object } }",
        "is-valid-tools-call: ~",
        "is-valid-tools-call: { schema: { type: object } }",
        "levenshtein: { value: sitting, max: 3 }",
        "is-xml: ~",
        "is-xml: { root: note }",
        "is-sql: ~",
        "similar: { value: hi, threshold: 0.85, model: text-embedding-3-small }",
        "cel: \"value.size() > 0\"",
        "factuality: { reference: \"Paris is the capital of France\" }",
        "answer-relevance: { query: \"What is the capital of France?\" }",
        "context-faithfulness: { context: \"France's capital is Paris.\" }",
        "not: { contains: hello }",
    ];
    for body in matcher_yamls {
        let doc = format!(
            "servers:\n  fs: {{ command: [\"a\"] }}\ntools:\n  - name: t\n    server: fs\n    tool: t\n    expect:\n      - target: result\n        matcher:\n          {body}\n"
        );
        // (a) The suite parser accepts it.
        let yaml: serde_yaml::Value =
            serde_yaml::from_str(&doc).unwrap_or_else(|e| panic!("yaml for `{body}`: {e}"));
        mcptest_core::suite::parse(&yaml)
            .unwrap_or_else(|e| panic!("suite parse for `{body}`: {e}"));
        // (b) The schema validates it.
        let as_json = serde_json::to_value(&yaml).expect("yaml to json");
        let errors: Vec<String> = validator
            .iter_errors(&as_json)
            .map(|e| e.to_string())
            .collect();
        assert!(
            errors.is_empty(),
            "matcher `{body}` should validate against the schema, errors: {errors:#?}"
        );
    }
}

/// Wrap one matcher value in a full config document, the way every
/// llm-judge criteria case below exercises the schema.
fn config_with_matcher(matcher: serde_json::Value) -> serde_json::Value {
    tool_with_expect(serde_json::json!([{
        "target": "result.content[0].text",
        "matcher": matcher,
    }]))
}

#[test]
fn llm_judge_criteria_variants_validate() {
    // WOR-1300: binary criteria decomposition. Criteria alone (combine
    // defaults to "all"), explicit combine "all", and combine {fraction}.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let criteria = serde_json::json!([
        { "name": "tone", "require": "Is the reply polite?" },
        { "name": "accuracy", "require": "Is the reply factually correct?" }
    ]);
    let cases = [
        serde_json::json!({
            "llm-judge": { "rubric": "Grade the reply.", "criteria": criteria }
        }),
        serde_json::json!({
            "llm-judge": { "rubric": "Grade the reply.", "criteria": criteria, "combine": "all" }
        }),
        serde_json::json!({
            "llm-judge": {
                "rubric": "Grade the reply.",
                "criteria": criteria,
                "combine": { "fraction": 0.5 },
                "samples": 3
            }
        }),
    ];
    for matcher in cases {
        let good = config_with_matcher(matcher.clone());
        let errors: Vec<String> = validator
            .iter_errors(&good)
            .map(|e| e.to_string())
            .collect();
        assert!(
            errors.is_empty(),
            "llm-judge criteria variant should validate: {matcher}, errors: {errors:#?}"
        );
    }
}

#[test]
fn llm_judge_criteria_rejects_malformed_shapes() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let nine: Vec<serde_json::Value> = (0..9)
        .map(|i| serde_json::json!({ "name": format!("c{i}"), "require": "q" }))
        .collect();
    let cases = [
        // Empty criteria list.
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [] } }),
        // More than eight criteria.
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": nine } }),
        // Unknown key inside a criterion.
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [
            { "name": "a", "require": "q", "weight": 2 }
        ] } }),
        // Criterion missing its question.
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [
            { "name": "a" }
        ] } }),
        // Empty criterion name.
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [
            { "name": "", "require": "q" }
        ] } }),
        // Fraction at the open lower bound and above the upper bound.
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [
            { "name": "a", "require": "q" }
        ], "combine": { "fraction": 0 } } }),
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [
            { "name": "a", "require": "q" }
        ], "combine": { "fraction": 1.5 } } }),
        // Combine strings other than "all", and junk combine objects.
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [
            { "name": "a", "require": "q" }
        ], "combine": "any" } }),
        serde_json::json!({ "llm-judge": { "rubric": "r", "criteria": [
            { "name": "a", "require": "q" }
        ], "combine": { "fraction": 0.5, "extra": true } } }),
    ];
    for matcher in cases {
        let bad = config_with_matcher(matcher.clone());
        assert!(
            !validator.is_valid(&bad),
            "malformed llm-judge criteria shape must be rejected: {matcher}"
        );
    }
}

/// A full `llm-jury` matcher with the WOR-1296 `bias:` block. Shared by the
/// accept and reject tests below so both sides exercise the same shape.
fn jury_matcher_with_bias(bias: Value) -> Value {
    serde_json::json!({
        "llm-jury": {
            "rubric": "Is the answer helpful?",
            "jurors": [
                { "model": "claude-sonnet-4-5" },
                { "model": "gpt-5", "threshold": 0.8 }
            ],
            "quorum": 0.5,
            "bias": bias
        }
    })
}

#[test]
fn llm_jury_bias_block_validates_and_parses() {
    // WOR-1296: the bias block must clear both gates: the published schema
    // and the suite parser, so the YAML a user writes is the YAML the
    // matcher grades with.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = tool_with_expect(serde_json::json!([{
        "target": "result.content[0].text",
        "matcher": jury_matcher_with_bias(serde_json::json!({
            "chain_of_thought": true,
            "verbosity_guard": 2.0,
            "anchors": [
                { "candidate": "a known good answer", "expected": 0.9 },
                { "candidate": { "text": "structured anchor" }, "expected": 0.2 }
            ]
        })),
    }]));
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "llm-jury bias block should validate, errors: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_yaml::from_str(&serde_yaml::to_string(&good).expect("config to YAML"))
            .expect("round-trip YAML parses");
    mcptest_core::suite::parse(&as_yaml).expect("suite parser accepts the bias block");
}

#[test]
fn llm_jury_bias_rejects_unknown_keys_and_out_of_range_values() {
    // The schema must reject what the runtime parser rejects: an unknown
    // bias key, a verbosity ratio at or below 1.0, and an anchor expected
    // score outside [0, 1].
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad_blocks = [
        serde_json::json!({ "position_swap": true }),
        serde_json::json!({ "verbosity_guard": 1.0 }),
        serde_json::json!({ "verbosity_guard": 0.5 }),
        serde_json::json!({ "anchors": [{ "candidate": "x", "expected": 1.5 }] }),
        serde_json::json!({ "anchors": [{ "candidate": "x", "expected": -0.1 }] }),
        serde_json::json!({ "anchors": [{ "candidate": "x" }] }),
        serde_json::json!({ "anchors": [] }),
    ];
    for bias in bad_blocks {
        let bad = tool_with_expect(serde_json::json!([{
            "target": "result.content[0].text",
            "matcher": jury_matcher_with_bias(bias.clone()),
        }]));
        assert!(
            !validator.is_valid(&bad),
            "schema must reject bias block {bias}"
        );
    }
}

#[test]
fn not_wrapping_passing_and_failing_inner_matchers_both_validate() {
    // The `not:` wrapper is a recursive $ref; confirm the schema accepts it
    // around both a deterministic matcher that would pass and one that would
    // fail at runtime. (The schema is structural; pass/fail is the runner's
    // job. This guards the recursive shape, not the runtime semantics.)
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let cases = [
        serde_json::json!({ "not": { "exact": "would-pass" } }),
        serde_json::json!({ "not": { "starts-with": "would-fail" } }),
        serde_json::json!({ "not": { "not": { "contains": "nested" } } }),
    ];
    for matcher in cases {
        let good = tool_with_expect(serde_json::json!([{
            "target": "result.content[0].text",
            "matcher": matcher,
        }]));
        assert!(
            validator.is_valid(&good),
            "not-wrapped matcher must validate: {matcher}"
        );
    }
}

#[test]
fn matcher_rejects_two_keys_even_with_a_named_matcher() {
    // maxProperties: 1 still holds for the new vocabulary. A matcher object
    // that pairs `not` with another key must be rejected.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = tool_with_expect(serde_json::json!([{
        "target": "result.content[0].text",
        "matcher": { "starts-with": "a", "icontains": "b" },
    }]));
    assert!(
        !validator.is_valid(&bad),
        "a two-key matcher object must be rejected by maxProperties:1"
    );
}

#[test]
fn matcher_rejects_dropped_pre_v1_names() {
    // The pre-v1 schema draft used `equals`, `length`, and `jsonpath`. We
    // reconciled the names against the runner before v1.0 shipped.
    // The schema must now reject the old names so configs that still use
    // them fail loudly instead of being silently ignored.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let dropped = [
        serde_json::json!({ "equals": "hello" }),
        serde_json::json!({ "length": { "min": 1 } }),
        serde_json::json!({ "jsonpath": { "path": "$.x" } }),
    ];
    for matcher in dropped {
        let bad = tool_with_expect(serde_json::json!([{
            "target": "result.content[0].text",
            "matcher": matcher,
        }]));
        assert!(
            !validator.is_valid(&bad),
            "dropped matcher name {matcher} must be rejected by the schema"
        );
    }
}

/// Helper that wraps a `fixtures` block under a minimal server registry so
/// the example exercises the schema without dragging in unrelated keys.
fn config_with_fixtures(fixtures: Value) -> Value {
    serde_json::json!({
        "servers": {
            "mock": { "command": ["echo", "hi"] }
        },
        "fixtures": fixtures
    })
}

#[test]
fn named_error_with_tool_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = config_with_fixtures(serde_json::json!({
        "server": "mock",
        "errors": [{
            "name": "rate_limited",
            "code": -32000,
            "message": "rate limited",
            "tool": "create_issue"
        }]
    }));
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "named error scoped to a tool must validate, errors: {errors:#?}"
    );
}

#[test]
fn named_error_with_applies_to_any_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = config_with_fixtures(serde_json::json!({
        "errors": [{
            "name": "auth_expired",
            "code": -32001,
            "message": "token expired",
            "applies_to": "any"
        }]
    }));
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "named error with applies_to: any must validate, errors: {errors:#?}"
    );
}

#[test]
fn named_error_with_both_tool_and_applies_to_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = config_with_fixtures(serde_json::json!({
        "errors": [{
            "name": "bad",
            "code": -1,
            "message": "bad",
            "tool": "create_issue",
            "applies_to": "any"
        }]
    }));
    assert!(
        !validator.is_valid(&bad),
        "named error setting both `tool` and `applies_to` must be rejected"
    );
}

#[test]
fn named_error_with_neither_tool_nor_applies_to_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = config_with_fixtures(serde_json::json!({
        "errors": [{
            "name": "bad",
            "code": -1,
            "message": "bad"
        }]
    }));
    assert!(
        !validator.is_valid(&bad),
        "named error missing both `tool` and `applies_to` must be rejected"
    );
}

#[test]
fn inject_error_field_validates_on_tool_test() {
    // The schema does not enforce that the referenced name exists in the
    // fixtures block (that cross-reference check belongs in the loader,
    // not the schema). It only checks the field shape. Both a referenced
    // name and an undeclared name therefore validate at the schema
    // level; the loader will produce the friendlier error message once
    //  wires the runner.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let declared = serde_json::json!({
        "servers": { "mock": { "command": ["echo", "hi"] } },
        "fixtures": {
            "errors": [{
                "name": "rate_limited",
                "code": -32000,
                "message": "rate limited",
                "tool": "create_issue"
            }]
        },
        "tools": [{
            "name": "t",
            "server": "mock",
            "tool": "create_issue",
            "inject_error": "rate_limited"
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&declared)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "inject_error referencing a declared fixture must validate, errors: {errors:#?}"
    );

    let undeclared = serde_json::json!({
        "servers": { "mock": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "t",
            "server": "mock",
            "tool": "create_issue",
            "inject_error": "ghost"
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&undeclared)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "inject_error referencing an undeclared fixture validates at the schema layer (loader catches it), errors: {errors:#?}"
    );
}

/// Helper that builds a minimal tool test with arbitrary extra keys merged
/// onto the test object. Used by the cache-eligibility schema tests below.
fn tool_with_extras(extras: serde_json::Map<String, Value>) -> Value {
    let mut tool = serde_json::Map::new();
    tool.insert("name".into(), Value::String("t".into()));
    tool.insert("server".into(), Value::String("fs".into()));
    tool.insert("tool".into(), Value::String("list".into()));
    for (k, v) in extras {
        tool.insert(k, v);
    }
    serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "tools": [Value::Object(tool)]
    })
}

#[test]
fn tool_test_accepts_cache_directives() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    for directive in ["auto", "always", "never"] {
        let mut extras = serde_json::Map::new();
        extras.insert("cache".into(), Value::String(directive.into()));
        let good = tool_with_extras(extras);
        let errors: Vec<String> = validator
            .iter_errors(&good)
            .map(|e| e.to_string())
            .collect();
        assert!(
            errors.is_empty(),
            "cache: {directive} must validate, errors: {errors:#?}"
        );
    }
}

#[test]
fn tool_test_rejects_unknown_cache_directive() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let mut extras = serde_json::Map::new();
    extras.insert("cache".into(), Value::String("sometimes".into()));
    let bad = tool_with_extras(extras);
    assert!(
        !validator.is_valid(&bad),
        "unknown cache directive must be rejected"
    );
}

#[test]
fn tool_test_accepts_effects_list_with_known_variants() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let mut extras = serde_json::Map::new();
    extras.insert(
        "effects".into(),
        serde_json::json!(["external", "local", "filesystem"]),
    );
    let good = tool_with_extras(extras);
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "effects list with known variants must validate, errors: {errors:#?}"
    );
}

#[test]
fn tool_test_rejects_effects_with_unknown_variant() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let mut extras = serde_json::Map::new();
    extras.insert("effects".into(), serde_json::json!(["external", "cosmic"]));
    let bad = tool_with_extras(extras);
    assert!(
        !validator.is_valid(&bad),
        "effects entry outside the enum must be rejected"
    );
}

#[test]
fn tool_test_accepts_server_version_and_hooks() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let mut extras = serde_json::Map::new();
    extras.insert("server_version".into(), Value::String("1.2.3".into()));
    extras.insert(
        "hooks".into(),
        serde_json::json!({ "before": ["./setup.sh"], "anything_else": 42 }),
    );
    let good = tool_with_extras(extras);
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "server_version + hooks must validate, errors: {errors:#?}"
    );
}

#[test]
fn compliance_test_accepts_cache_and_effects() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "compliance": [{
            "name": "init",
            "server": "fs",
            "check": "initialize",
            "cache": "always",
            "effects": ["local"],
            "server_version": "2024-11-05",
            "hooks": { "before": ["./compliance-setup.sh"] }
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "compliance test with cache fields must validate, errors: {errors:#?}"
    );
}

#[test]
fn compliance_test_rejects_unknown_cache_directive() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "compliance": [{
            "name": "init",
            "server": "fs",
            "check": "initialize",
            "cache": "yes_please"
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "unknown cache directive on compliance test must be rejected"
    );
}

#[test]
fn url_server_accepts_custom_headers_with_literal_and_env_values() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = serde_json::json!({
        "servers": {
            "remote": {
                "url": "https://mcp.example.com/v1",
                "headers": {
                    "X-Tenant": "acme",
                    "X-API-Key": { "env": "MY_API_KEY" }
                }
            }
        }
    });
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "server.headers literal + env forms must validate, errors: {errors:#?}"
    );
}

#[test]
fn url_server_rejects_header_env_object_without_env_key() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": {
            "remote": {
                "url": "https://mcp.example.com/v1",
                "headers": {
                    "X-API-Key": { "name": "MY_API_KEY" }
                }
            }
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "header env object without `env` key must be rejected"
    );
}

#[test]
fn url_server_accepts_full_http_block() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = serde_json::json!({
        "servers": {
            "remote": {
                "url": "https://mcp.example.com/v1",
                "http": {
                    "timeout": "30s",
                    "connect_timeout": "5s",
                    "max_redirects": 5,
                    "user_agent_override": true,
                    "tls": {
                        "insecure_skip_verify": false,
                        "ca_bundle_path": "/etc/ssl/internal-ca.pem",
                        "min_version": "1.3"
                    }
                }
            }
        }
    });
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "full server.http block must validate, errors: {errors:#?}"
    );
}

#[test]
fn url_server_rejects_http_max_redirects_above_limit() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": {
            "remote": {
                "url": "https://mcp.example.com/v1",
                "http": { "max_redirects": 999 }
            }
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "max_redirects above the schema cap must be rejected"
    );
}

#[test]
fn url_server_rejects_http_tls_unknown_min_version() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": {
            "remote": {
                "url": "https://mcp.example.com/v1",
                "http": {
                    "tls": { "min_version": "1.0" }
                }
            }
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "tls.min_version outside the {{1.2, 1.3}} enum must be rejected"
    );
}

// ---------------------------------------------------------------------------
// Compliance corpus tests.
//
// The corpus directory at `compliance/` carries authored YAML files the
// future compliance runner will consume. The schema accepts a
// `corpus:` top-level block, and `compliance/registry.yml` is the canonical
// metadata source. These tests fence the corpus and registry together: every
// authored YAML must validate, every rule_id used in a corpus file must
// appear in the registry, and every registry rule's id must agree with its
// declared category prefix.
// ---------------------------------------------------------------------------

/// Walks the workspace corpus directory and returns every `*.yml` file
/// under one of the per-version or per-category subdirectories.
fn corpus_files() -> Vec<PathBuf> {
    let root = workspace_root().join("compliance");
    let mut files: Vec<PathBuf> = Vec::new();
    for sub in [
        "v2024-11-05",
        "v2025-03-26",
        "v2025-06-18",
        "draft",
        "edge",
        "header",
        "replay",
        "transport",
    ] {
        let dir = root.join(sub);
        if !dir.exists() {
            continue;
        }
        for entry in
            std::fs::read_dir(&dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display()))
        {
            let path = entry.unwrap().path();
            if path.extension().and_then(|s| s.to_str()) == Some("yml") {
                files.push(path);
            }
        }
    }
    files
}

/// Walks every corpus file and extracts the `rule_id` values it declares.
fn corpus_rule_ids() -> Vec<(PathBuf, String)> {
    let mut pairs: Vec<(PathBuf, String)> = Vec::new();
    for path in corpus_files() {
        let value = yaml_to_json(&path);
        let rules = value
            .get("corpus")
            .and_then(|c| c.get("rules"))
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default();
        for rule in rules {
            if let Some(id) = rule.get("rule_id").and_then(Value::as_str) {
                pairs.push((path.clone(), id.to_string()));
            }
        }
    }
    pairs
}

#[test]
fn every_corpus_file_validates_against_the_schema() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let files = corpus_files();
    assert!(
        !files.is_empty(),
        "expected to find authored corpus YAML files"
    );
    for path in files {
        let value = yaml_to_json(&path);
        let errors: Vec<String> = validator
            .iter_errors(&value)
            .map(|e| e.to_string())
            .collect();
        assert!(
            errors.is_empty(),
            "{} failed schema validation: {errors:#?}",
            path.display()
        );
    }
}

#[test]
fn every_corpus_rule_id_appears_in_the_registry() {
    // Load the registry once, then assert every rule_id used by any corpus
    // file is present. Catches a typo in either side without needing the
    // future runner.
    let registry_path = workspace_root().join("compliance").join("registry.yml");
    let raw = std::fs::read_to_string(&registry_path)
        .unwrap_or_else(|e| panic!("read {}: {e}", registry_path.display()));
    let registry: serde_yaml::Value =
        serde_yaml::from_str(&raw).expect("registry.yml parses as YAML");
    let registered: std::collections::HashSet<String> = registry
        .get("rules")
        .and_then(serde_yaml::Value::as_sequence)
        .expect("registry has rules sequence")
        .iter()
        .filter_map(|r| {
            r.get("id")
                .and_then(serde_yaml::Value::as_str)
                .map(String::from)
        })
        .collect();

    for (path, rule_id) in corpus_rule_ids() {
        assert!(
            registered.contains(&rule_id),
            "{} references unknown rule_id {} (not in registry)",
            path.display(),
            rule_id
        );
    }
}

#[test]
fn every_registry_rule_id_matches_its_declared_category() {
    let registry_path = workspace_root().join("compliance").join("registry.yml");
    let raw = std::fs::read_to_string(&registry_path)
        .unwrap_or_else(|e| panic!("read {}: {e}", registry_path.display()));
    let registry: serde_yaml::Value =
        serde_yaml::from_str(&raw).expect("registry.yml parses as YAML");
    let rules = registry
        .get("rules")
        .and_then(serde_yaml::Value::as_sequence)
        .expect("registry has rules sequence");

    for rule in rules {
        let id = rule
            .get("id")
            .and_then(serde_yaml::Value::as_str)
            .expect("every rule has an id");
        let category = rule
            .get("category")
            .and_then(serde_yaml::Value::as_str)
            .expect("every rule has a category");
        let prefix = id.split('-').next().unwrap_or("");
        assert_eq!(
            prefix, category,
            "rule {id} has prefix {prefix} but declared category {category}"
        );
    }
}

#[test]
fn corpus_block_with_unknown_category_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "corpus": {
            "category": "BOGUS",
            "rules": [
                { "rule_id": "PROTO-001", "name": "x" }
            ]
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "corpus block with unknown category must be rejected"
    );
}

#[test]
fn corpus_rule_id_pattern_is_enforced() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "corpus": {
            "category": "PROTO",
            "rules": [
                { "rule_id": "proto-1", "name": "lowercase" }
            ]
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "rule_id must match the PREFIX-NNN pattern"
    );
}

#[test]
fn schema_id_and_meta_schema_are_stable() {
    // The published `$id` is a public contract: editors pin to it via the
    // `yaml-language-server` comment. Fail loudly if anyone changes it.
    let schema = load_schema();
    assert_eq!(
        schema.get("$id").and_then(Value::as_str),
        Some("https://mcptest.sh/schema/v1.json"),
        "schema $id must stay at the v1 URL"
    );
    assert_eq!(
        schema.get("$schema").and_then(Value::as_str),
        Some("https://json-schema.org/draft/2020-12/schema"),
        "schema must declare draft 2020-12"
    );
}

// --- mcptest pipe schema (schemas/pipe/v0.json) ---

fn load_pipe_schema() -> Value {
    let path = workspace_root()
        .join("schemas")
        .join("pipe")
        .join("v0.json");
    let raw =
        std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
    serde_json::from_str(&raw).expect("schemas/pipe/v0.json is valid JSON")
}

#[test]
fn pipe_schema_compiles_and_declares_v0_id() {
    let schema = load_pipe_schema();
    jsonschema::validator_for(&schema).expect("pipe schema compiles");
    assert_eq!(
        schema.get("$id").and_then(Value::as_str),
        Some("https://mcptest.sh/schema/pipe/v0.json"),
        "pipe schema $id must stay at the v0 URL"
    );
}

#[test]
fn pipe_example_validates() {
    let schema = load_pipe_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("pipe-search-then-update.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "pipe example should validate, got errors: {errors:#?}"
    );
}

#[test]
fn pipe_step_without_tool_is_rejected() {
    let schema = load_pipe_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({ "pipe": [{ "id": "a" }] });
    assert!(
        !validator.is_valid(&bad),
        "a step without `tool` must fail validation"
    );
}

#[test]
fn pipe_bad_on_error_is_rejected() {
    let schema = load_pipe_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "pipe": [{ "id": "a", "tool": "t", "on_error": "explode" }]
    });
    assert!(
        !validator.is_valid(&bad),
        "an unknown on_error value must fail validation"
    );
}

#[test]
fn pipe_empty_list_is_rejected() {
    let schema = load_pipe_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({ "pipe": [] });
    assert!(
        !validator.is_valid(&bad),
        "an empty pipe list must fail validation"
    );
}

// --- : suite-composition primitives ---

/// A minimal composition suite with the given `max_iterations` value on a
/// fan-out node (WOR-1322).
fn composition_with_max_iterations(value: serde_json::Value) -> serde_json::Value {
    serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "compositions": [{
            "name": "fan",
            "nodes": [
                { "id": "search", "tool": "t" },
                {
                    "id": "enrich",
                    "needs": ["search"],
                    "for_each": "${search}",
                    "max_iterations": value,
                    "tool": "u"
                }
            ]
        }]
    })
}

#[test]
fn composition_max_iterations_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = composition_with_max_iterations(serde_json::json!(25000));
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "max_iterations must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(good).expect("json value is valid yaml value");
    mcptest_core::suite::parse(&as_yaml).expect("max_iterations parses");
}

#[test]
fn composition_max_iterations_outside_the_range_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    for bad_value in [serde_json::json!(0), serde_json::json!(100001)] {
        let bad = composition_with_max_iterations(bad_value.clone());
        assert!(
            !validator.is_valid(&bad),
            "max_iterations {bad_value} must be rejected (range 1..=100000)"
        );
    }
}

#[test]
fn assertion_weight_and_name_validate() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let good = tool_with_expect(serde_json::json!([{
        "target": "result.content[0].text",
        "matcher": { "contains": "ok" },
        "weight": 2.5,
        "name": "primary"
    }]));
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "weight + name must validate: {errors:#?}"
    );
}

#[test]
fn assertion_zero_weight_is_rejected_by_schema() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = tool_with_expect(serde_json::json!([{
        "target": "result.content[0].text",
        "matcher": { "contains": "ok" },
        "weight": 0
    }]));
    assert!(
        !validator.is_valid(&bad),
        "a zero weight must be rejected (exclusiveMinimum 0)"
    );
}

#[test]
fn assert_set_block_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let expect = serde_json::json!([
        { "target": "result.a", "matcher": { "exact": 1 } },
        { "assert-set": {
            "name": "strings",
            "threshold": 0.5,
            "weight": 2.0,
            "assertions": [
                { "target": "result.b", "matcher": { "contains": "foo" } },
                { "target": "result.c", "matcher": { "contains": "bar" } }
            ]
        }}
    ]);
    let good = tool_with_expect(expect);
    let errors: Vec<String> = validator
        .iter_errors(&good)
        .map(|e| e.to_string())
        .collect();
    assert!(errors.is_empty(), "assert-set must validate: {errors:#?}");
    // The suite parser accepts the same document.
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(good).expect("json value is valid yaml value");
    mcptest_core::suite::parse(&as_yaml).expect("assert-set parses");
}

#[test]
fn assert_set_without_threshold_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = tool_with_expect(serde_json::json!([
        { "assert-set": {
            "assertions": [
                { "target": "result.b", "matcher": { "contains": "foo" } }
            ]
        }}
    ]));
    assert!(
        !validator.is_valid(&bad),
        "an assert-set without a threshold must be rejected"
    );
}

#[test]
fn test_threshold_and_derived_metrics_validate_and_parse() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "scored",
            "server": "fs",
            "tool": "list",
            "threshold": 0.75,
            "expect": [
                { "target": "result.a", "matcher": { "exact": 1 }, "name": "a" },
                { "target": "result.b", "matcher": { "exact": 2 }, "name": "b" }
            ],
            "derivedMetrics": [{
                "name": "blended",
                "threshold": 0.7,
                "value": {
                    "weighted_average": [
                        { "ref": "a", "weight": 2.0 },
                        { "ref": "b", "weight": 1.0 }
                    ]
                }
            }]
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "threshold + derivedMetrics must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    mcptest_core::suite::parse(&as_yaml).expect("threshold + derivedMetrics parse");
}

#[test]
fn derived_metric_requires_one_aggregation_key() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "t",
            "server": "fs",
            "tool": "list",
            "derivedMetrics": [{
                "name": "blended",
                "value": {
                    "weighted_sum": [{ "ref": "a" }],
                    "weighted_average": [{ "ref": "b" }]
                }
            }]
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "a derived metric value with two aggregation keys must be rejected"
    );
}

#[test]
fn default_test_and_hooks_validate_and_parse() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "defaultTest": {
            "threshold": 0.5,
            "expect": [
                { "target": "result.isError", "matcher": { "exact": false } }
            ]
        },
        "hooks": {
            "beforeAll": "./setup-db.sh",
            "afterAll": ["./teardown-db.sh", "./report.sh"],
            "beforeEach": "./reset-session.sh"
        },
        "tools": [{
            "name": "t",
            "server": "fs",
            "tool": "list",
            "expect": [
                { "target": "result.a", "matcher": { "exact": 1 } }
            ]
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "defaultTest + hooks must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    mcptest_core::suite::parse(&as_yaml).expect("defaultTest + hooks parse");
}

#[test]
fn hooks_reject_unknown_phase_key() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "hooks": { "beforeSometimes": "./x.sh" }
    });
    assert!(
        !validator.is_valid(&bad),
        "an unknown suite-level hook phase must be rejected"
    );
}

#[test]
fn context_hook_map_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "hooks": {
            "beforeEach": [
                "echo plain-still-works",
                { "command": "node ./compute-vars.js", "mode": "context" }
            ]
        },
        "tools": [{ "name": "t", "server": "fs", "tool": "list" }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(errors.is_empty(), "context hook must validate: {errors:#?}");
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    mcptest_core::suite::parse(&as_yaml).expect("context hook parses");
}

#[test]
fn context_hook_map_missing_mode_is_rejected_by_schema() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "fs": { "command": ["echo", "hi"] } },
        "hooks": {
            "beforeEach": [{ "command": "node ./compute-vars.js" }]
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "a map hook without `mode: context` must be rejected"
    );
}

// --- : computed-metric check blocks ---

#[test]
fn calibration_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("calibration.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "calibration.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn calibration_block_validates_and_parses() {
    // A full calibration entry (with the optional reliability matrix and an
    // explicit expect:) must validate against the schema and round-trip
    // through the suite parser into a typed CalibrationCheck.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "calibration": [{
            "name": "judge stays calibrated",
            "labels": "./labels.jsonl",
            "reliability": { "tp": 90, "fn": 10, "tn": 80, "fp": 20 },
            "observed_positive_rate": 0.5,
            "expect": [
                { "target": "ece", "matcher": { "schema": { "maximum": 0.10 } } },
                { "target": "brier", "matcher": { "schema": { "maximum": 0.25 } } },
                { "target": "corrected_rate", "matcher": { "schema": { "maximum": 0.60 } } }
            ]
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "calibration block must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("calibration block parses");
    assert_eq!(suite.calibration.len(), 1);
    assert_eq!(suite.calibration[0].name, "judge stays calibrated");
    assert!(suite.calibration[0].reliability.is_some());
}

#[test]
fn calibration_minimal_form_validates_and_parses() {
    // The smallest useful form: just a name and a labels path, no expect.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "calibration": [{ "name": "judge", "labels": "./labels.jsonl" }]
    });
    assert!(
        validator.is_valid(&config),
        "minimal calibration block must validate"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("minimal calibration parses");
    assert_eq!(suite.calibration.len(), 1);
    assert!(suite.calibration[0].expect.is_empty());
}

#[test]
fn calibration_entry_without_labels_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "calibration": [{ "name": "judge" }]
    });
    assert!(
        !validator.is_valid(&bad),
        "a calibration entry without `labels` must be rejected"
    );
}

#[test]
fn calibration_reliability_missing_a_count_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "calibration": [{
            "name": "judge",
            "labels": "./labels.jsonl",
            "reliability": { "tp": 90, "fn": 10, "tn": 80 }
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "a reliability matrix missing `fp` must be rejected"
    );
}

#[test]
fn suite_with_no_new_blocks_still_validates() {
    // The additive guarantee: a suite that uses none of the  blocks
    // validates exactly as before.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("server-stdio.yml");
    assert!(
        validator.is_valid(&value),
        "an existing suite must still validate after the  additions"
    );
}

#[test]
fn tool_quality_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("tool-quality.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "tool-quality.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn tool_quality_block_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "tool_quality": [{
            "name": "tool descriptions meet the bar",
            "server": "local",
            "expect": [
                { "target": "min_score", "matcher": { "schema": { "minimum": 0.5 } } },
                { "target": "mean_score", "matcher": { "schema": { "minimum": 0.7 } } },
                { "target": "tool[\"search\"].score", "matcher": { "schema": { "minimum": 0.6 } } }
            ]
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "tool_quality block must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("tool_quality block parses");
    assert_eq!(suite.tool_quality.len(), 1);
    assert_eq!(suite.tool_quality[0].server, "local");
}

#[test]
fn tool_quality_minimal_form_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "tool_quality": [{ "name": "q", "server": "local" }]
    });
    assert!(
        validator.is_valid(&config),
        "minimal tool_quality block must validate"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("minimal tool_quality parses");
    assert!(suite.tool_quality[0].expect.is_empty());
}

#[test]
fn tool_quality_entry_without_server_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "tool_quality": [{ "name": "q" }]
    });
    assert!(
        !validator.is_valid(&bad),
        "a tool_quality entry without `server` must be rejected"
    );
}

#[test]
fn grade_delta_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("compliance-grade-delta.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "compliance-grade-delta.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn grade_delta_block_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "compliance": {
            "spec_version": "v2025-06-18",
            "grade_delta": {
                "against": "./scorecard.json",
                "expect": [
                    { "target": "grade_regressed", "matcher": { "exact": false } },
                    { "target": "regressed_sections", "matcher": { "schema": { "maxItems": 0 } } }
                ]
            },
            "tests": [
                { "name": "PROTO-001", "server": "local", "check": "initialize" }
            ]
        }
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "grade_delta block must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("grade_delta block parses");
    let delta = suite.grade_delta.expect("grade_delta parsed");
    assert_eq!(
        delta.against.as_deref(),
        Some(std::path::Path::new("./scorecard.json"))
    );
}

#[test]
fn grade_delta_with_no_against_validates_and_parses() {
    // The bootstrap form: a grade_delta block with no `against:` (and no
    // expect) is valid, and parses to an empty-baseline GradeDelta.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "compliance": {
            "grade_delta": {},
            "tests": [{ "name": "PROTO-001", "server": "local", "check": "initialize" }]
        }
    });
    assert!(
        validator.is_valid(&config),
        "a grade_delta block with no against must validate"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("bootstrap grade_delta parses");
    assert!(suite.grade_delta.unwrap().against.is_none());
}

#[test]
fn grade_delta_rejects_unknown_key() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "compliance": {
            "grade_delta": { "agains": "./typo.json" },
            "tests": [{ "name": "PROTO-001", "server": "local", "check": "initialize" }]
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "a grade_delta block with an unknown key must be rejected"
    );
}

// --- : jury bias-signal targets ---

#[test]
fn agent_jury_bias_example_validates_and_parses() {
    // The bias-signal example asserts jury.same_family / jury.bias_warning,
    // which are free-string targets (no schema change), so it validates and
    // round-trips through the suite parser (which enforces the one-jury rule).
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("agent-jury-bias.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "agent-jury-bias.yml should validate, got errors: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(value).expect("json value is valid yaml value");
    mcptest_core::suite::parse(&as_yaml).expect("agent-jury-bias.yml parses");
}

// --- : spec-version cross-version gate ---

#[test]
fn spec_version_check_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("spec-version-pinning.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "spec-version-pinning.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn spec_version_check_block_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "compliance": {
            "spec_version": "v2024-11-05",
            "spec_version_check": {
                "against": "v2025-06-18",
                "expect": [
                    { "target": "spec_clean", "matcher": { "exact": true } },
                    { "target": "spec_breaking_changes", "matcher": { "schema": { "maxItems": 0 } } }
                ]
            },
            "tests": [
                { "name": "PROTO-001", "server": "local", "check": "initialize" }
            ]
        }
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "spec_version_check block must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("spec_version_check block parses");
    let check = suite.spec_version_check.expect("spec_version_check parsed");
    assert_eq!(check.against.as_deref(), Some("v2025-06-18"));
    assert_eq!(check.expect.len(), 2);
}

#[test]
fn spec_version_check_with_no_against_validates_and_parses() {
    // The bootstrap form: a block with no `against:` is valid and parses to a
    // no-target SpecVersionCheck.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "compliance": {
            "spec_version": "v2024-11-05",
            "spec_version_check": {},
            "tests": [{ "name": "PROTO-001", "server": "local", "check": "initialize" }]
        }
    });
    assert!(
        validator.is_valid(&config),
        "a spec_version_check block with no against must validate"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("bootstrap spec_version_check parses");
    assert!(suite.spec_version_check.unwrap().against.is_none());
}

#[test]
fn spec_version_check_rejects_unknown_against_revision() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "compliance": {
            "spec_version_check": { "against": "v1999-01-01" },
            "tests": [{ "name": "PROTO-001", "server": "local", "check": "initialize" }]
        }
    });
    assert!(
        !validator.is_valid(&bad),
        "an unknown `against:` revision must be rejected by the enum"
    );
}

#[test]
fn all_four_wor852_blocks_validate_together() {
    // The design's acceptance check: a suite using every  surface
    // validates. jury.* targets live inside an agent test (free-string
    // targets, no schema change), so this exercises calibration, tool_quality,
    // and grade_delta together with a jury assertion.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "local": { "command": ["echo", "hi"] } },
        "calibration": [{ "name": "cal", "labels": "./labels.jsonl" }],
        "tool_quality": [{ "name": "tq", "server": "local" }],
        "compliance": {
            "grade_delta": { "against": "./scorecard.json" },
            "tests": [{ "name": "PROTO-001", "server": "local", "check": "initialize" }]
        }
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "a suite using all  blocks must validate: {errors:#?}"
    );
}

// --- WOR-1237: metamorphic (oracle-free) relations ---

#[test]
fn metamorphic_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("metamorphic/suite.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "metamorphic/suite.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn metamorphic_block_validates_and_parses() {
    // Every relation form (bare strings and the three single-key objects) plus
    // an explicit expect: validates against the schema and round-trips through
    // the suite parser.
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "catalog": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "search relations",
            "server": "catalog",
            "tool": "search",
            "args": { "query": "x", "filters": ["a"], "limit": 5 },
            "metamorphic": {
                "relations": [
                    "idempotent",
                    "arg_order_insensitive",
                    "case_insensitive",
                    "whitespace_insensitive",
                    { "noop_filter": { "arg": "filters", "value": "*" } },
                    { "subset_under_filter": { "arg": "filters", "value": "kind:doc" } },
                    { "monotone": { "arg": "limit" } }
                ],
                "expect": [
                    { "target": "metamorphic.violations", "matcher": { "schema": { "maximum": 0 } } }
                ]
            }
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "metamorphic block must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("metamorphic block parses");
    let spec = suite.tests[0]
        .metamorphic
        .as_ref()
        .expect("metamorphic block captured on the tool test");
    assert_eq!(spec.relations.len(), 7, "all seven relations parsed");
    assert_eq!(spec.expect.len(), 1, "the explicit expect floor parsed");
}

#[test]
fn metamorphic_rejects_unknown_relation_name() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "catalog": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "t", "server": "catalog", "tool": "search",
            "metamorphic": { "relations": ["teleport"] }
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "an unknown relation name must be rejected by the enum"
    );
}

#[test]
fn metamorphic_rejects_relation_object_with_two_keys() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "catalog": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "t", "server": "catalog", "tool": "search",
            "metamorphic": { "relations": [
                { "monotone": { "arg": "limit" }, "noop_filter": { "arg": "filters", "value": "*" } }
            ] }
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "a relation object with two keys must be rejected by maxProperties:1"
    );
}

#[test]
fn metamorphic_block_without_relations_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "catalog": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "t", "server": "catalog", "tool": "search",
            "metamorphic": {}
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "a metamorphic block without relations must be rejected"
    );
}

// --- WOR-1238: schema-driven input fuzzing ---

#[test]
fn fuzz_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("fuzz/schema-fuzz.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "fuzz/schema-fuzz.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn fuzz_block_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "api": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "fuzz search",
            "server": "api",
            "tool": "search",
            "args": { "query": "x" },
            "fuzz": {
                "seed": 1729,
                "cases": 32,
                "max_call_ms": 1500,
                "expect": [
                    { "target": "fuzz.crashes", "matcher": { "schema": { "maximum": 0 } } }
                ]
            }
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(errors.is_empty(), "fuzz block must validate: {errors:#?}");
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("fuzz block parses");
    let spec = suite.tests[0]
        .fuzz
        .as_ref()
        .expect("fuzz block captured on the tool test");
    assert_eq!(spec.seed, 1729);
    assert_eq!(spec.cases, 32);
    assert_eq!(spec.max_call_ms, 1500);
    assert_eq!(spec.expect.len(), 1);
}

#[test]
fn fuzz_block_defaults_when_scalars_omitted() {
    let as_yaml: serde_yaml::Value = serde_yaml::from_str(
        "servers:\n  api: { command: [\"a\"] }\ntools:\n  - name: t\n    server: api\n    tool: search\n    fuzz: {}\n",
    )
    .expect("yaml parses");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("empty fuzz block parses");
    let spec = suite.tests[0].fuzz.as_ref().expect("fuzz captured");
    assert_eq!(spec.seed, 0, "seed defaults to 0");
    assert!(spec.cases > 0, "cases defaults to a positive count");
    assert!(
        spec.max_call_ms > 0,
        "max_call_ms defaults to a positive bound"
    );
}

#[test]
fn fuzz_block_rejects_zero_cases() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "api": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "t", "server": "api", "tool": "search",
            "fuzz": { "cases": 0 }
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "fuzz cases: 0 must be rejected (minimum 1)"
    );
}

// --- WOR-1242: structural tool-edge coverage ---

#[test]
fn tool_edges_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("coverage/tool-edges.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "coverage/tool-edges.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn tool_edges_block_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "repo": { "command": ["echo", "hi"] } },
        "agents": [{
            "name": "stays in bounds",
            "model": "claude-sonnet-4-5",
            "servers": ["repo"],
            "prompt": "triage",
            "tool_edges": {
                "allowed": ["search", "summarize"],
                "restricted": ["delete_repo"],
                "delegation": [{ "from": "planner", "to": "worker" }],
                "expect": [
                    { "target": "edges.restricted_attempts", "matcher": { "schema": { "maximum": 0 } } }
                ]
            }
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "tool_edges block must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("tool_edges block parses");
    let gate = suite.agents[0]
        .tool_edges
        .as_ref()
        .expect("tool_edges gate captured on the agent test");
    assert_eq!(
        gate.set.allowed,
        vec!["search".to_string(), "summarize".to_string()]
    );
    assert_eq!(gate.set.restricted, vec!["delete_repo".to_string()]);
    assert_eq!(
        gate.set.delegation,
        vec![("planner".to_string(), "worker".to_string())]
    );
    assert_eq!(gate.expect.len(), 1);
}

#[test]
fn tool_edges_delegation_without_to_is_rejected() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "repo": { "command": ["echo", "hi"] } },
        "agents": [{
            "name": "t", "model": "m", "servers": ["repo"], "prompt": "p",
            "tool_edges": { "delegation": [{ "from": "planner" }] }
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "a delegation entry without `to` must be rejected"
    );
}

// --- WOR-1239: negative-path conformance ---

#[test]
fn negative_path_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("negative-path.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "negative-path.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn negative_path_block_validates_and_parses() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let config = serde_json::json!({
        "servers": { "api": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "rejects bad input",
            "server": "api",
            "tool": "search",
            "args": { "query": "x" },
            "negative_path": {
                "checks": ["unknown_tool", "missing_required", "wrong_type"],
                "expect": [
                    { "target": "negative_path.failures", "matcher": { "schema": { "maximum": 0 } } }
                ]
            }
        }]
    });
    let errors: Vec<String> = validator
        .iter_errors(&config)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "negative_path block must validate: {errors:#?}"
    );
    let as_yaml: serde_yaml::Value =
        serde_json::from_value(config).expect("json value is valid yaml value");
    let suite = mcptest_core::suite::parse(&as_yaml).expect("negative_path block parses");
    let spec = suite.tests[0]
        .negative_path
        .as_ref()
        .expect("negative_path captured on the tool test");
    assert_eq!(spec.checks.len(), 3);
    assert_eq!(spec.expect.len(), 1);
}

#[test]
fn negative_path_rejects_unknown_probe_name() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let bad = serde_json::json!({
        "servers": { "api": { "command": ["echo", "hi"] } },
        "tools": [{
            "name": "t", "server": "api", "tool": "search",
            "negative_path": { "checks": ["teleport"] }
        }]
    });
    assert!(
        !validator.is_valid(&bad),
        "an unknown probe name must be rejected by the enum"
    );
}

// --- WOR-1240: three-valued verdict (run_options.inconclusive) ---

#[test]
fn inconclusive_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("inconclusive.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "inconclusive.yml should validate, got errors: {errors:#?}"
    );
}

#[test]
fn run_options_inconclusive_block_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let servers = serde_json::json!({ "x": { "command": ["echo", "hi"] } });
    let good = serde_json::json!({
        "servers": servers,
        "run_options": {
            "inconclusive": { "on": true, "min_trials": 5, "confidence_pct": 95, "treat_as": "fail" }
        }
    });
    assert!(
        validator.is_valid(&good),
        "run_options.inconclusive must validate"
    );
    let bad = serde_json::json!({
        "servers": servers,
        "run_options": { "inconclusive": { "treat_as": "explode" } }
    });
    assert!(
        !validator.is_valid(&bad),
        "an unknown treat_as must be rejected"
    );
    let bad_conf = serde_json::json!({
        "servers": servers,
        "run_options": { "inconclusive": { "confidence_pct": 50 } }
    });
    assert!(
        !validator.is_valid(&bad_conf),
        "confidence_pct below 80 must be rejected"
    );
}

// --- WOR-1250: oracle-free robustness walkthrough ---

#[test]
fn robustness_walkthrough_example_validates() {
    let schema = load_schema();
    let validator = jsonschema::validator_for(&schema).expect("schema compiles");
    let value = example("robustness-walkthrough/suite.yml");
    let errors: Vec<String> = validator
        .iter_errors(&value)
        .map(|e| e.to_string())
        .collect();
    assert!(
        errors.is_empty(),
        "robustness-walkthrough/suite.yml should validate, got errors: {errors:#?}"
    );
}
