//! Schema-aware argument synthesis shared by `generate stubs` and
//! `generate suite` (and, next, the agent-facing scaffold verbs).
//!
//! Why this exists: a happy-path stub is only useful when its synthesized
//! arguments actually satisfy the tool's input schema. A fixed per-type table
//! breaks on enums, formats, bounds, and `$ref` composition, so this module
//! reads the schema the way a validator would and picks the most plausible
//! minimal value. Unsatisfiable constraints surface as notes the generators
//! render as YAML comments instead of emitting a test that can never pass.

use serde_json::{json, Map, Value};

use super::resolve::{merge, resolve};

/// Placeholder for strings with no usable constraints. Kept stable across
/// releases so regenerated stubs do not drift under `--check`.
const STRING_SAMPLE: &str = "sample";

/// Outcome of synthesizing a value for a schema.
///
/// `value: None` means the constraints are unsatisfiable (or the reference
/// depth cap was hit) and the caller should skip the happy-path case; `notes`
/// carries human-readable caveats either way, rendered as YAML comments.
#[derive(Debug, Clone, PartialEq)]
pub struct SynthesisResult {
    /// The synthesized value, or `None` when the schema cannot be satisfied.
    pub value: Option<Value>,
    /// Caveats for the user: unhonored patterns, depth caps, missing schemas.
    pub notes: Vec<String>,
}

/// Synthesize the minimal `args` object for a tool's `inputSchema`.
///
/// Only required properties are populated, mirroring the smallest call a
/// developer would write by hand. A missing or empty schema yields `{}` plus
/// a note; contradictory constraints yield `value: None` plus the reason.
///
/// ```
/// use mcptest_core::stub_gen::synthesize_args;
/// use serde_json::json;
///
/// let schema = json!({
///     "type": "object",
///     "properties": { "mode": { "type": "string", "enum": ["fast", "slow"] } },
///     "required": ["mode"]
/// });
/// let result = synthesize_args(&schema);
/// assert_eq!(result.value, Some(json!({ "mode": "fast" })));
/// ```
pub fn synthesize_args(input_schema: &Value) -> SynthesisResult {
    let mut notes = Vec::new();
    if schema_is_absent(input_schema) {
        notes.push("tool declares no input schema; starting from empty args".to_string());
        return SynthesisResult {
            value: Some(json!({})),
            notes,
        };
    }
    let outcome = resolve(input_schema, input_schema, 0, &mut notes)
        .and_then(|(schema, depth)| object_args(&schema, input_schema, depth, &mut notes));
    match outcome {
        Ok(args) => SynthesisResult {
            value: Some(Value::Object(args)),
            notes,
        },
        Err(reason) => {
            notes.push(reason);
            SynthesisResult { value: None, notes }
        }
    }
}

/// Synthesize one value for an arbitrary subschema. `root` is the document
/// local `$ref` pointers resolve against (usually the whole `inputSchema`).
/// Exposed so callers outside the generators (the mcp-server scaffold verb)
/// can synthesize a single property without building a fake args object.
pub fn synthesize_value(schema: &Value, root: &Value) -> SynthesisResult {
    let mut notes = Vec::new();
    match synth(schema, root, 0, &mut notes) {
        Ok(value) => SynthesisResult {
            value: Some(value),
            notes,
        },
        Err(reason) => {
            notes.push(reason);
            SynthesisResult { value: None, notes }
        }
    }
}

/// Required property names paired with their raw subschemas, in declaration
/// order, after resolving top-level `$ref` and `allOf`. The generators
/// iterate this to emit one type-mismatch case per required argument.
pub fn required_properties(input_schema: &Value) -> Vec<(String, Value)> {
    let mut notes = Vec::new();
    let resolved = match resolve(input_schema, input_schema, 0, &mut notes) {
        Ok((schema, _)) => schema,
        Err(_) => input_schema.clone(),
    };
    let props = properties_map(&resolved);
    required_list(&resolved)
        .into_iter()
        .map(|name| {
            let prop = props.get(&name).cloned().unwrap_or(Value::Null);
            (name, prop)
        })
        .collect()
}

/// Pick a value that genuinely violates the schema's declared type, for the
/// negative test cases. A string property (enum or not) gets a number, a
/// numeric property gets a string, and so on. Returns `None` when the schema
/// declares nothing concrete to violate, so callers skip the case rather
/// than emit a test that asserts nothing.
pub fn mismatch_value(schema: &Value, root: &Value) -> Option<Value> {
    let mut notes = Vec::new();
    let (resolved, _) = resolve(schema, root, 0, &mut notes).ok()?;
    let forbidden = declared_types(&resolved)?;
    pick_mismatch(&forbidden)
}

fn schema_is_absent(schema: &Value) -> bool {
    match schema {
        Value::Null => true,
        Value::Object(map) => map.is_empty(),
        _ => false,
    }
}

fn properties_map(schema: &Value) -> Map<String, Value> {
    schema
        .get("properties")
        .and_then(Value::as_object)
        .cloned()
        .unwrap_or_default()
}

fn required_list(schema: &Value) -> Vec<String> {
    schema
        .get("required")
        .and_then(Value::as_array)
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(str::to_string))
                .collect()
        })
        .unwrap_or_default()
}

/// Build the minimal object for an (already resolved) object schema: one
/// synthesized value per required property. Any unsatisfiable required
/// property makes the whole object unsatisfiable.
fn object_args(
    schema: &Value,
    root: &Value,
    depth: usize,
    notes: &mut Vec<String>,
) -> Result<Map<String, Value>, String> {
    let props = properties_map(schema);
    let mut out = Map::new();
    for name in required_list(schema) {
        let prop = props.get(&name).cloned().unwrap_or(Value::Null);
        let value = synth(&prop, root, depth, notes)
            .map_err(|reason| format!("required property `{name}` is unsatisfiable: {reason}"))?;
        out.insert(name, value);
    }
    Ok(out)
}

/// The single-value synthesis pipeline: resolve refs and allOf, then prefer
/// author-provided literals, then composition branches, then type rules.
fn synth(
    schema: &Value,
    root: &Value,
    depth: usize,
    notes: &mut Vec<String>,
) -> Result<Value, String> {
    match schema {
        Value::Null | Value::Bool(true) => return Ok(json!(STRING_SAMPLE)),
        Value::Bool(false) => return Err("schema `false` admits no value".to_string()),
        _ => {}
    }
    let (resolved, depth) = resolve(schema, root, depth, notes)?;
    if let Some(value) = literal_candidate(&resolved) {
        return Ok(value);
    }
    if let Some(branches) = branch_list(&resolved) {
        return synth_branches(&resolved, &branches, root, depth, notes);
    }
    synth_typed(&resolved, root, depth, notes)
}

/// Author-provided values win over synthetic ones: `examples[0]`, then
/// `default`, then `const`, then the first `enum` entry. The author's own
/// sample is always more plausible than anything we invent.
fn literal_candidate(schema: &Value) -> Option<Value> {
    if let Some(first) = schema
        .get("examples")
        .and_then(Value::as_array)
        .and_then(|a| a.first())
    {
        return Some(first.clone());
    }
    if let Some(default) = schema.get("default") {
        return Some(default.clone());
    }
    if let Some(constant) = schema.get("const") {
        return Some(constant.clone());
    }
    schema
        .get("enum")
        .and_then(Value::as_array)
        .and_then(|a| a.first())
        .cloned()
}

fn branch_list(schema: &Value) -> Option<Vec<Value>> {
    schema
        .get("anyOf")
        .or_else(|| schema.get("oneOf"))
        .and_then(Value::as_array)
        .cloned()
}

/// Try `anyOf`/`oneOf` branches in declaration order and keep the first one
/// whose synthesized value actually validates against the merged branch.
fn synth_branches(
    resolved: &Value,
    branches: &[Value],
    root: &Value,
    depth: usize,
    notes: &mut Vec<String>,
) -> Result<Value, String> {
    let mut base = resolved.as_object().cloned().unwrap_or_default();
    base.remove("anyOf");
    base.remove("oneOf");
    let base = Value::Object(base);
    let mut last_failure = "no branches present".to_string();
    for branch in branches {
        let mut branch_notes = Vec::new();
        let attempt = resolve(branch, root, depth, &mut branch_notes)
            .and_then(|(schema, d)| merge(&base, &schema).map(|merged| (merged, d)))
            .and_then(|(merged, d)| {
                synth(&merged, root, d, &mut branch_notes).map(|value| (value, merged))
            });
        match attempt {
            Ok((value, merged)) if branch_satisfied(&value, &merged, root) => {
                notes.append(&mut branch_notes);
                return Ok(value);
            }
            Ok(_) => last_failure = "synthesized value failed branch validation".to_string(),
            Err(reason) => last_failure = reason,
        }
    }
    Err(format!(
        "no anyOf/oneOf branch is satisfiable ({last_failure})"
    ))
}

/// Ground-truth satisfiability check for branch selection, using the real
/// validator. The root's `$defs`/`definitions` are copied alongside so local
/// refs inside the branch still resolve. A schema the validator cannot
/// compile is optimistically accepted; generation should degrade, not fail.
fn branch_satisfied(value: &Value, branch: &Value, root: &Value) -> bool {
    let mut doc = branch.as_object().cloned().unwrap_or_default();
    for key in ["$defs", "definitions"] {
        if !doc.contains_key(key) {
            if let Some(defs) = root.get(key) {
                doc.insert(key.to_string(), defs.clone());
            }
        }
    }
    match jsonschema::validator_for(&Value::Object(doc)) {
        Ok(validator) => validator.is_valid(value),
        Err(_) => true,
    }
}

fn synth_typed(
    schema: &Value,
    root: &Value,
    depth: usize,
    notes: &mut Vec<String>,
) -> Result<Value, String> {
    match effective_type(schema).as_deref() {
        Some("string") => synth_string(schema, notes),
        Some("integer") => synth_numeric(schema, true),
        Some("number") => synth_numeric(schema, false),
        Some("boolean") => Ok(json!(true)),
        Some("array") => synth_array(schema, root, depth, notes),
        Some("object") => object_args(schema, root, depth, notes).map(Value::Object),
        Some("null") => Ok(Value::Null),
        _ => Ok(json!(STRING_SAMPLE)),
    }
}

/// The declared `type`, or a best-effort inference from the keywords present
/// (`properties` implies object, `minItems` implies array, ...). Type arrays
/// pick the first non-null entry so `["string", "null"]` synthesizes a
/// string. Crate-visible so the probe tier dispatches on the same inference.
pub(crate) fn effective_type(schema: &Value) -> Option<String> {
    match schema.get("type") {
        Some(Value::String(t)) => return Some(t.clone()),
        Some(Value::Array(types)) => {
            let names: Vec<&str> = types.iter().filter_map(Value::as_str).collect();
            let first = names.iter().find(|t| **t != "null").or(names.first())?;
            return Some((*first).to_string());
        }
        _ => {}
    }
    let keys = schema.as_object()?;
    let has = |list: &[&str]| list.iter().any(|k| keys.contains_key(*k));
    let inferred = if has(&["properties", "required"]) {
        "object"
    } else if has(&["items", "prefixItems", "minItems", "maxItems"]) {
        "array"
    } else if has(&["pattern", "format", "minLength", "maxLength"]) {
        "string"
    } else if has(&[
        "minimum",
        "maximum",
        "exclusiveMinimum",
        "exclusiveMaximum",
        "multipleOf",
    ]) {
        "number"
    } else {
        return None;
    };
    Some(inferred.to_string())
}

fn synth_string(schema: &Value, notes: &mut Vec<String>) -> Result<Value, String> {
    let min_len = schema.get("minLength").and_then(Value::as_u64).unwrap_or(0);
    let max_len = schema.get("maxLength").and_then(Value::as_u64);
    if let Some(max) = max_len {
        if min_len > max {
            return Err(format!(
                "minLength {min_len} is greater than maxLength {max}"
            ));
        }
    }
    if let Some(formatted) = format_sample(schema) {
        return Ok(formatted);
    }
    if let Some(pattern) = schema.get("pattern").and_then(Value::as_str) {
        if let Some(literal) = invert_pattern(pattern) {
            return Ok(json!(literal));
        }
        notes.push(format!(
            "pattern `{pattern}` was not honored; emitted a length-respecting placeholder"
        ));
    }
    bounded_sample(min_len, max_len)
}

fn bounded_sample(min_len: u64, max_len: Option<u64>) -> Result<Value, String> {
    const CAP: u64 = 4096;
    let sample_len = STRING_SAMPLE.len() as u64;
    let fits = min_len <= sample_len && max_len.is_none_or(|max| max >= sample_len);
    if fits {
        return Ok(json!(STRING_SAMPLE));
    }
    if min_len > CAP {
        return Err(format!(
            "minLength {min_len} is impractically large (cap {CAP})"
        ));
    }
    let len = if min_len > sample_len {
        min_len
    } else {
        max_len.unwrap_or(0)
    };
    Ok(json!("x".repeat(len as usize)))
}

/// A plausible value per JSON Schema `format`. The goal is a value a real
/// server accepts on the first run, not RFC-grade coverage; unknown formats
/// fall through to the pattern and length rules.
fn format_sample(schema: &Value) -> Option<Value> {
    let format = schema.get("format").and_then(Value::as_str)?;
    let sample = match format {
        "uri" | "url" | "uri-reference" => "https://example.com/resource",
        "email" | "idn-email" => "user@example.com",
        "date-time" => "2026-01-15T10:30:00Z",
        "date" => "2026-01-15",
        "time" => "10:30:00Z",
        "duration" => "PT1H",
        "uuid" => "00000000-0000-4000-8000-000000000000",
        "hostname" | "idn-hostname" => "example.com",
        "ipv4" => "192.0.2.1",
        "ipv6" => "2001:db8::1",
        _ => return None,
    };
    Some(json!(sample))
}

/// Best-effort literal extraction from trivially-invertible patterns. A
/// pattern whose body is plain literal characters after stripping `^`/`$`
/// anchors matches its own body (JSON Schema patterns are unanchored
/// searches), so `^us-` yields `us-`. Anything with real regex machinery
/// returns `None` and the caller notes the unhonored pattern.
fn invert_pattern(pattern: &str) -> Option<String> {
    let body = pattern.strip_prefix('^').unwrap_or(pattern);
    let body = body.strip_suffix('$').unwrap_or(body);
    let literal = !body.is_empty()
        && body
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || " _-/:@.,".contains(c));
    literal.then(|| body.to_string())
}

/// One inclusive-or-strict numeric bound.
struct Bound {
    value: f64,
    strict: bool,
}

fn synth_numeric(schema: &Value, integer: bool) -> Result<Value, String> {
    let (lower, upper) = numeric_bounds(schema)?;
    let candidate = numeric_candidate(&lower, &upper, integer);
    let candidate = apply_multiple_of(schema, candidate, &lower, &upper, integer)?;
    if !within(candidate, &lower, &upper) {
        return Err(format!(
            "no {} satisfies the declared bounds",
            if integer { "integer" } else { "number" }
        ));
    }
    Ok(number_value(candidate, integer))
}

/// Read `minimum`/`maximum` and both spellings of the exclusive bounds
/// (draft-4 boolean flags and the modern numeric keywords), then reject
/// empty ranges up front so the caller can emit a clear skip comment.
fn numeric_bounds(schema: &Value) -> Result<(Option<Bound>, Option<Bound>), String> {
    let mut lower = schema
        .get("minimum")
        .and_then(Value::as_f64)
        .map(|value| Bound {
            value,
            strict: schema.get("exclusiveMinimum") == Some(&Value::Bool(true)),
        });
    if let Some(value) = schema.get("exclusiveMinimum").and_then(Value::as_f64) {
        if lower.as_ref().is_none_or(|b| value >= b.value) {
            lower = Some(Bound {
                value,
                strict: true,
            });
        }
    }
    let mut upper = schema
        .get("maximum")
        .and_then(Value::as_f64)
        .map(|value| Bound {
            value,
            strict: schema.get("exclusiveMaximum") == Some(&Value::Bool(true)),
        });
    if let Some(value) = schema.get("exclusiveMaximum").and_then(Value::as_f64) {
        if upper.as_ref().is_none_or(|b| value <= b.value) {
            upper = Some(Bound {
                value,
                strict: true,
            });
        }
    }
    if let (Some(lo), Some(hi)) = (&lower, &upper) {
        let empty = lo.value > hi.value || (lo.value == hi.value && (lo.strict || hi.strict));
        if empty {
            return Err(format!(
                "minimum {} is greater than maximum {} (or the range is exclusive-empty)",
                lo.value, hi.value
            ));
        }
    }
    Ok((lower, upper))
}

/// Pick the bound-respecting candidate: sit on the minimum when present,
/// else on the maximum, else default to 1 (the historical placeholder).
fn numeric_candidate(lower: &Option<Bound>, upper: &Option<Bound>, integer: bool) -> f64 {
    match (lower, upper) {
        (Some(lo), _) if !lo.strict => {
            if integer {
                lo.value.ceil()
            } else {
                lo.value
            }
        }
        (Some(lo), Some(hi)) => {
            if integer {
                lo.value.floor() + 1.0
            } else {
                lo.value + (hi.value - lo.value) / 2.0
            }
        }
        (Some(lo), None) => {
            if integer {
                lo.value.floor() + 1.0
            } else {
                lo.value + 1.0
            }
        }
        (None, Some(hi)) => {
            if integer {
                if hi.strict {
                    hi.value.ceil() - 1.0
                } else {
                    hi.value.floor()
                }
            } else if hi.strict {
                hi.value - 1.0
            } else {
                hi.value
            }
        }
        (None, None) => 1.0,
    }
}

/// Snap the candidate to a `multipleOf` step when one is declared. Only the
/// trivial adjustments are attempted; an integer type with no integral
/// multiple in range is reported as unsatisfiable rather than guessed at.
fn apply_multiple_of(
    schema: &Value,
    candidate: f64,
    lower: &Option<Bound>,
    upper: &Option<Bound>,
    integer: bool,
) -> Result<f64, String> {
    let Some(step) = schema.get("multipleOf").and_then(Value::as_f64) else {
        return Ok(candidate);
    };
    if step <= 0.0 {
        return Ok(candidate);
    }
    let snapped = if lower.is_none() && upper.is_some() {
        (candidate / step).floor() * step
    } else if lower.is_none() && upper.is_none() {
        step
    } else {
        (candidate / step).ceil() * step
    };
    if integer && snapped.fract() != 0.0 {
        // Walk up to the next integer that is still a multiple of the step.
        let start = snapped.ceil();
        for offset in 0..100 {
            let probe = start + f64::from(offset);
            let ratio = probe / step;
            if (ratio - ratio.round()).abs() < 1e-9 {
                return Ok(probe);
            }
        }
        return Err(format!(
            "multipleOf {step} has no integer multiple near {candidate}"
        ));
    }
    Ok(snapped)
}

fn within(candidate: f64, lower: &Option<Bound>, upper: &Option<Bound>) -> bool {
    let above = lower.as_ref().is_none_or(|lo| {
        if lo.strict {
            candidate > lo.value
        } else {
            candidate >= lo.value
        }
    });
    let below = upper.as_ref().is_none_or(|hi| {
        if hi.strict {
            candidate < hi.value
        } else {
            candidate <= hi.value
        }
    });
    above && below
}

fn number_value(candidate: f64, integer: bool) -> Value {
    if integer || candidate.fract() == 0.0 {
        return json!(candidate as i64);
    }
    // from_f64 only fails on NaN/infinity, which the bound math cannot
    // produce from finite schema numbers; 1 is the safe historical fallback.
    serde_json::Number::from_f64(candidate)
        .map(Value::Number)
        .unwrap_or_else(|| json!(1))
}

fn synth_array(
    schema: &Value,
    root: &Value,
    depth: usize,
    notes: &mut Vec<String>,
) -> Result<Value, String> {
    let min_items = schema.get("minItems").and_then(Value::as_u64).unwrap_or(0);
    let max_items = schema.get("maxItems").and_then(Value::as_u64);
    if let Some(max) = max_items {
        if min_items > max {
            return Err(format!(
                "minItems {min_items} is greater than maxItems {max}"
            ));
        }
    }
    if min_items == 0 {
        return Ok(json!([]));
    }
    const CAP: u64 = 64;
    if min_items > CAP {
        return Err(format!(
            "minItems {min_items} is impractically large (cap {CAP})"
        ));
    }
    let mut items = Vec::with_capacity(min_items as usize);
    for index in 0..min_items {
        let item_schema = item_schema_at(schema, index as usize);
        items.push(synth(&item_schema, root, depth, notes)?);
    }
    Ok(Value::Array(items))
}

fn item_schema_at(schema: &Value, index: usize) -> Value {
    if let Some(prefix) = schema.get("prefixItems").and_then(Value::as_array) {
        if let Some(s) = prefix.get(index) {
            return s.clone();
        }
    }
    match schema.get("items") {
        Some(Value::Array(tuple)) => tuple
            .get(index)
            .or_else(|| tuple.last())
            .cloned()
            .unwrap_or(Value::Null),
        Some(s) => s.clone(),
        None => Value::Null,
    }
}

/// JSON types the mismatch value must avoid, taken from the declared `type`
/// (all entries of a type array), or inferred from `enum`/`const`/keywords.
fn declared_types(schema: &Value) -> Option<Vec<String>> {
    match schema.get("type") {
        Some(Value::String(t)) => return Some(vec![t.clone()]),
        Some(Value::Array(types)) => {
            let list: Vec<String> = types
                .iter()
                .filter_map(Value::as_str)
                .map(str::to_string)
                .collect();
            if !list.is_empty() {
                return Some(list);
            }
        }
        _ => {}
    }
    if let Some(first) = schema
        .get("enum")
        .and_then(Value::as_array)
        .and_then(|a| a.first())
    {
        return Some(vec![json_type_name(first).to_string()]);
    }
    if let Some(constant) = schema.get("const") {
        return Some(vec![json_type_name(constant).to_string()]);
    }
    effective_type(schema).map(|t| vec![t])
}

fn pick_mismatch(forbidden: &[String]) -> Option<Value> {
    let has = |t: &str| {
        forbidden.iter().any(|f| {
            f == t || (f == "integer" && t == "number") || (f == "number" && t == "integer")
        })
    };
    if has("string") && !has("number") {
        return Some(json!(12345));
    }
    if has("number") && !has("string") {
        return Some(json!("not-a-number"));
    }
    if has("boolean") && !has("string") {
        return Some(json!("true"));
    }
    if has("array") && !has("string") {
        return Some(json!("not-an-array"));
    }
    if has("object") && !has("array") {
        return Some(json!([]));
    }
    let fallbacks = [
        json!(12345),
        json!("type-mismatch"),
        json!(true),
        json!([]),
        json!({}),
    ];
    fallbacks
        .into_iter()
        .find(|candidate| !has(json_type_name(candidate)))
}

fn json_type_name(value: &Value) -> &'static str {
    match value {
        Value::Null => "null",
        Value::Bool(_) => "boolean",
        Value::Number(_) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    }
}

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

    /// Property-style guard: the synthesized args must validate against the
    /// very schema they were synthesized from, for every satisfiable fixture.
    fn assert_args_self_validate(schema: &Value) -> Value {
        let result = synthesize_args(schema);
        let args = result
            .value
            .unwrap_or_else(|| panic!("satisfiable schema produced no args: {:?}", result.notes));
        let validator = jsonschema::validator_for(schema).expect("fixture schema compiles");
        let errors: Vec<String> = validator
            .iter_errors(&args)
            .map(|e| e.to_string())
            .collect();
        assert!(
            errors.is_empty(),
            "args {args} should validate against {schema}, got: {errors:#?}"
        );
        args
    }

    fn satisfiable_corpus() -> Vec<Value> {
        let mut corpus = scalar_fixtures();
        corpus.extend(composition_fixtures());
        corpus
    }

    fn scalar_fixtures() -> Vec<Value> {
        vec![
            // Empty-ish object schema.
            json!({ "type": "object" }),
            // Enum first value.
            json!({
                "type": "object",
                "properties": { "mode": { "type": "string", "enum": ["fast", "slow"] } },
                "required": ["mode"]
            }),
            // examples and default beat type defaults.
            json!({
                "type": "object",
                "properties": {
                    "city": { "type": "string", "examples": ["Sacramento"] },
                    "limit": { "type": "integer", "default": 25 }
                },
                "required": ["city", "limit"]
            }),
            // Format-specific plausible values.
            json!({
                "type": "object",
                "properties": {
                    "homepage": { "type": "string", "format": "uri" },
                    "contact": { "type": "string", "format": "email" },
                    "when": { "type": "string", "format": "date-time" },
                    "day": { "type": "string", "format": "date" },
                    "id": { "type": "string", "format": "uuid" },
                    "host": { "type": "string", "format": "hostname" },
                    "addr": { "type": "string", "format": "ipv4" }
                },
                "required": ["homepage", "contact", "when", "day", "id", "host", "addr"]
            }),
            // Numeric bounds, exclusives, and multipleOf.
            json!({
                "type": "object",
                "properties": {
                    "page": { "type": "integer", "minimum": 5 },
                    "ratio": { "type": "number", "exclusiveMinimum": 0 },
                    "step": { "type": "integer", "minimum": 3, "multipleOf": 5 },
                    "cap": { "type": "integer", "maximum": -2 }
                },
                "required": ["page", "ratio", "step", "cap"]
            }),
            // String length bounds.
            json!({
                "type": "object",
                "properties": {
                    "long": { "type": "string", "minLength": 10 },
                    "short": { "type": "string", "maxLength": 3 }
                },
                "required": ["long", "short"]
            }),
            // Trivially-invertible pattern (fixed prefix).
            json!({
                "type": "object",
                "properties": { "region": { "type": "string", "pattern": "^us-east-1" } },
                "required": ["region"]
            }),
        ]
    }

    fn composition_fixtures() -> Vec<Value> {
        vec![
            // Local $ref into $defs.
            json!({
                "type": "object",
                "$defs": { "city": { "type": "string", "enum": ["Berlin"] } },
                "properties": { "city": { "$ref": "#/$defs/city" } },
                "required": ["city"]
            }),
            // oneOf picks the first satisfiable branch.
            json!({
                "type": "object",
                "properties": {
                    "target": {
                        "oneOf": [
                            { "type": "string", "minLength": 3 },
                            { "type": "integer" }
                        ]
                    }
                },
                "required": ["target"]
            }),
            // anyOf where the first branch is unsatisfiable.
            json!({
                "type": "object",
                "properties": {
                    "pick": {
                        "anyOf": [
                            { "type": "string", "minLength": 9, "maxLength": 2 },
                            { "type": "boolean" }
                        ]
                    }
                },
                "required": ["pick"]
            }),
            // allOf merge of two object halves.
            json!({
                "type": "object",
                "allOf": [
                    { "properties": { "a": { "type": "string" } }, "required": ["a"] },
                    { "properties": { "b": { "type": "integer", "minimum": 7 } }, "required": ["b"] }
                ]
            }),
            // Nested objects and arrays with minItems.
            json!({
                "type": "object",
                "properties": {
                    "filters": {
                        "type": "object",
                        "properties": { "tag": { "type": "string" } },
                        "required": ["tag"]
                    },
                    "ids": {
                        "type": "array",
                        "minItems": 2,
                        "items": { "type": "integer", "minimum": 1 }
                    }
                },
                "required": ["filters", "ids"]
            }),
            // Optional recursive self-reference: never forced, so it
            // synthesizes the empty object.
            json!({
                "type": "object",
                "$defs": {
                    "node": {
                        "type": "object",
                        "properties": { "child": { "$ref": "#/$defs/node" } }
                    }
                },
                "properties": { "tree": { "$ref": "#/$defs/node" } },
                "required": ["tree"]
            }),
        ]
    }

    #[test]
    fn every_satisfiable_fixture_self_validates() {
        for schema in satisfiable_corpus() {
            assert_args_self_validate(&schema);
        }
    }

    #[test]
    fn empty_schema_yields_empty_args_with_a_note() {
        let result = synthesize_args(&json!({}));
        assert_eq!(result.value, Some(json!({})));
        assert!(result.notes[0].contains("no input schema"));

        let null_result = synthesize_args(&Value::Null);
        assert_eq!(null_result.value, Some(json!({})));
    }

    #[test]
    fn examples_win_over_default_and_enum() {
        let schema = json!({
            "type": "object",
            "properties": {
                "mode": {
                    "type": "string",
                    "examples": ["from-examples"],
                    "default": "from-default",
                    "enum": ["from-examples", "from-default", "from-enum"]
                }
            },
            "required": ["mode"]
        });
        let args = assert_args_self_validate(&schema);
        assert_eq!(args["mode"], "from-examples");
    }

    #[test]
    fn numeric_minimum_beats_the_historical_placeholder() {
        let schema = json!({
            "type": "object",
            "properties": { "page": { "type": "integer", "minimum": 5 } },
            "required": ["page"]
        });
        let args = assert_args_self_validate(&schema);
        assert_eq!(args["page"], 5);
    }

    #[test]
    fn exclusive_minimum_lands_strictly_above_the_bound() {
        let schema = json!({
            "type": "object",
            "properties": { "n": { "type": "integer", "exclusiveMinimum": 5 } },
            "required": ["n"]
        });
        let args = assert_args_self_validate(&schema);
        assert_eq!(args["n"], 6);
    }

    #[test]
    fn fixed_prefix_pattern_is_inverted_to_its_literal() {
        let schema = json!({
            "type": "object",
            "properties": { "region": { "type": "string", "pattern": "^us-" } },
            "required": ["region"]
        });
        let result = synthesize_args(&schema);
        assert_eq!(result.value.unwrap()["region"], "us-");
        assert!(result.notes.is_empty());
    }

    #[test]
    fn complex_pattern_falls_back_with_a_note() {
        let schema = json!({
            "type": "object",
            "properties": {
                "code": { "type": "string", "pattern": "^[A-Z]{3}[0-9]{4}$" }
            },
            "required": ["code"]
        });
        let result = synthesize_args(&schema);
        // A best-effort value is still emitted; the note flags the gap.
        assert!(result.value.is_some());
        assert!(result.notes.iter().any(|n| n.contains("was not honored")));
    }

    #[test]
    fn contradictory_string_lengths_are_unsatisfiable() {
        let schema = json!({
            "type": "object",
            "properties": { "s": { "type": "string", "minLength": 9, "maxLength": 2 } },
            "required": ["s"]
        });
        let result = synthesize_args(&schema);
        assert_eq!(result.value, None);
        assert!(result
            .notes
            .iter()
            .any(|n| n.contains("minLength 9 is greater than maxLength 2")));
    }

    #[test]
    fn contradictory_numeric_bounds_are_unsatisfiable() {
        let schema = json!({
            "type": "object",
            "properties": { "n": { "type": "integer", "minimum": 10, "maximum": 3 } },
            "required": ["n"]
        });
        let result = synthesize_args(&schema);
        assert_eq!(result.value, None);
    }

    #[test]
    fn all_of_type_conflict_is_unsatisfiable() {
        let schema = json!({
            "type": "object",
            "properties": {
                "x": { "allOf": [{ "type": "string" }, { "type": "object" }] }
            },
            "required": ["x"]
        });
        let result = synthesize_args(&schema);
        assert_eq!(result.value, None);
        assert!(result.notes.iter().any(|n| n.contains("conflicting types")));
    }

    #[test]
    fn required_recursive_self_reference_hits_the_depth_cap() {
        let schema = json!({
            "type": "object",
            "$defs": {
                "node": {
                    "type": "object",
                    "properties": { "child": { "$ref": "#/$defs/node" } },
                    "required": ["child"]
                }
            },
            "properties": { "tree": { "$ref": "#/$defs/node" } },
            "required": ["tree"]
        });
        let result = synthesize_args(&schema);
        assert_eq!(result.value, None);
        assert!(result.notes.iter().any(|n| n.contains("depth cap")));
    }

    #[test]
    fn one_of_falls_through_to_a_satisfiable_branch() {
        let schema = json!({
            "type": "object",
            "properties": {
                "pick": {
                    "oneOf": [
                        { "type": "string", "minLength": 9, "maxLength": 2 },
                        { "type": "integer", "minimum": 4 }
                    ]
                }
            },
            "required": ["pick"]
        });
        let args = assert_args_self_validate(&schema);
        assert_eq!(args["pick"], 4);
    }

    #[test]
    fn min_items_array_synthesizes_conforming_elements() {
        let schema = json!({
            "type": "object",
            "properties": {
                "ids": { "type": "array", "minItems": 3, "items": { "type": "integer", "minimum": 2 } }
            },
            "required": ["ids"]
        });
        let args = assert_args_self_validate(&schema);
        assert_eq!(args["ids"], json!([2, 2, 2]));
    }

    #[test]
    fn mismatch_for_string_enum_is_a_number() {
        let prop = json!({ "type": "string", "enum": ["fast", "slow"] });
        assert_eq!(mismatch_value(&prop, &prop), Some(json!(12345)));
    }

    #[test]
    fn mismatch_for_integer_is_a_string() {
        let prop = json!({ "type": "integer" });
        assert_eq!(mismatch_value(&prop, &prop), Some(json!("not-a-number")));
    }

    #[test]
    fn mismatch_for_boolean_is_a_string() {
        let prop = json!({ "type": "boolean" });
        assert_eq!(mismatch_value(&prop, &prop), Some(json!("true")));
    }

    #[test]
    fn mismatch_for_untyped_property_is_skipped() {
        let prop = json!({ "description": "anything goes" });
        assert_eq!(mismatch_value(&prop, &prop), None);
    }

    #[test]
    fn mismatch_for_multi_type_avoids_every_listed_type() {
        let prop = json!({ "type": ["string", "integer"] });
        let wrong = mismatch_value(&prop, &prop).expect("a violating value exists");
        assert!(!wrong.is_string() && !wrong.is_number(), "got {wrong}");
    }

    #[test]
    fn mismatch_resolves_local_refs_before_deciding() {
        let root = json!({
            "$defs": { "n": { "type": "integer" } },
            "properties": { "count": { "$ref": "#/$defs/n" } },
            "required": ["count"]
        });
        let prop = json!({ "$ref": "#/$defs/n" });
        assert_eq!(mismatch_value(&prop, &root), Some(json!("not-a-number")));
    }

    #[test]
    fn required_properties_sees_through_top_level_all_of() {
        let schema = json!({
            "type": "object",
            "allOf": [
                { "properties": { "a": { "type": "string" } }, "required": ["a"] },
                { "properties": { "b": { "type": "integer" } }, "required": ["b"] }
            ]
        });
        let pairs = required_properties(&schema);
        let names: Vec<&str> = pairs.iter().map(|(n, _)| n.as_str()).collect();
        assert_eq!(names, vec!["a", "b"]);
    }

    #[test]
    fn synthesize_value_handles_a_bare_subschema() {
        let schema = json!({ "type": "string", "format": "uuid" });
        let result = synthesize_value(&schema, &schema);
        assert_eq!(
            result.value,
            Some(json!("00000000-0000-4000-8000-000000000000"))
        );
    }
}
