//! `oneOf` / `anyOf` / `allOf` composition wrappers.
//!
//! Composition wrappers carry a list of inner matchers and pass when the
//! list satisfies a quantifier. They mirror JSON Schema 2020-12's
//! `oneOf` / `anyOf` / `allOf` at the matcher level so suites can compose
//! any matchers, not just schemas, across one extracted target.
//!
//! Composition wraps only deterministic matchers, mirroring `not:`'s
//! scope. A deferred or async inner child (snapshot, llm-judge, llm-jury,
//! similar) is a structural error rather than a silent skip, because a
//! composition that silently dropped a child would change its truth
//! value without the suite author knowing. A structural error from an
//! inner matcher (bad regex, malformed schema) propagates unchanged.
//!
//! The module lives alongside `mod.rs` rather than inside it so the
//! executor's dispatch file stays under the module-size cap; the helpers
//! it needs (`DeterministicOutcome`, `deterministic_pass`) are
//! `pub(super)` for that reason.

use serde_json::Value;

use super::{deterministic_pass, DeterministicOutcome};
use crate::suite::Matcher;

/// Which composition rule a multi-matcher wrapper applies. The variants
/// are distinct from the wire-level matcher names so the dispatch arm in
/// `check_matcher` reads as `Rule::OneOf` rather than `OneOf::OneOf`.
///
/// The shared `Of` suffix is intentional: the names mirror the wire-level
/// JSON Schema 2020-12 keywords (`oneOf` / `anyOf` / `allOf`) so a reader
/// trips on neither side.
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy)]
pub(super) enum Rule {
    /// Exactly one inner matcher must pass.
    OneOf,
    /// At least one inner matcher must pass.
    AnyOf,
    /// Every inner matcher must pass.
    AllOf,
}

impl Rule {
    fn label(self) -> &'static str {
        match self {
            Self::OneOf => "oneOf",
            Self::AnyOf => "anyOf",
            Self::AllOf => "allOf",
        }
    }
}

/// Evaluate a composition for the synchronous dispatch path.
///
/// Returns `Ok(())` on pass and a rendered failure message otherwise.
/// Mirrors `check_matcher`'s contract.
pub(super) fn check(actual: &Value, children: &[Matcher], rule: Rule) -> Result<(), String> {
    let mut passed = 0usize;
    let mut failures: Vec<String> = Vec::new();
    for (i, child) in children.iter().enumerate() {
        match deterministic_pass(actual, child) {
            DeterministicOutcome::Passed => passed += 1,
            DeterministicOutcome::Failed => {
                failures.push(format!("  child #{i}: failed"));
            }
            DeterministicOutcome::Error(reason) => return Err(reason),
            DeterministicOutcome::NotNegatable(name) => {
                return Err(format!(
                    "matcher `{}` cannot wrap the deferred/async matcher `{name}`; \
                     composition is limited to deterministic matchers",
                    rule.label()
                ));
            }
        }
    }
    decide(passed, children.len(), rule, &failures)
}

/// Evaluate a composition for the `deterministic_pass` recursion path so
/// nested compositions surface a clean `Passed` / `Failed` rather than
/// the prose failure message. The caller already knows the matcher is one
/// of the three composition variants, so we extract `children` and `rule`
/// from it here and keep the dispatch site readable.
pub(super) fn deterministic_from(actual: &Value, matcher: &Matcher) -> DeterministicOutcome {
    let (children, rule) = match matcher {
        Matcher::OneOf(c) => (c.as_slice(), Rule::OneOf),
        Matcher::AnyOf(c) => (c.as_slice(), Rule::AnyOf),
        Matcher::AllOf(c) => (c.as_slice(), Rule::AllOf),
        // Caller is `deterministic_pass`, which only routes the three
        // composition variants here; reaching anything else is a bug.
        _ => unreachable!("deterministic_from called with a non-composition matcher"),
    };
    match check(actual, children, rule) {
        Ok(()) => DeterministicOutcome::Passed,
        Err(_) => DeterministicOutcome::Failed,
    }
}

/// Decide pass / fail from the counts. Kept in its own function so the
/// quantifier table reads as a vertical list rather than a deeply
/// nested if/else inside `check`.
fn decide(passed: usize, total: usize, rule: Rule, failures: &[String]) -> Result<(), String> {
    match rule {
        Rule::OneOf if passed == 1 => Ok(()),
        Rule::OneOf => Err(format!(
            "expected `oneOf` to match exactly one inner matcher, {passed} of {total} matched",
        )),
        Rule::AnyOf if passed >= 1 => Ok(()),
        Rule::AnyOf => Err(format!(
            "expected `anyOf` to match at least one inner matcher, none of {total} matched:\n{}",
            failures.join("\n")
        )),
        Rule::AllOf if passed == total => Ok(()),
        Rule::AllOf => Err(format!(
            "expected `allOf` to match every inner matcher, {} of {total} failed:\n{}",
            total - passed,
            failures.join("\n")
        )),
    }
}
