//! Matcher-result conversion, diff rendering, and the `subset` check.
//!
//! Split out of `executor/mod.rs` to keep that module under the
//! production-line guard. These helpers fold a matchers-crate
//! [`AssertionResult`] into the executor's `Result<(), String>` failure
//! type and render a readable diff on failure.

use serde_json::Value;

use crate::matchers::{AssertionDiff, AssertionResult};

/// Fold a matchers-crate [`AssertionResult`] into the executor's
/// `Result<(), String>` failure type.
///
/// A structural error (bad regex, malformed schema) maps to the error
/// string directly. A clean pass maps to `Ok(())`. A failing assertion
/// renders every [`AssertionDiff`] so the JSON-Pointer path and the
/// matcher's note survive into the failure message instead of being
/// flattened to a single "values differ" line.
pub(super) fn from_result(result: crate::Result<AssertionResult>) -> Result<(), String> {
    let outcome = result.map_err(|e| e.to_string())?;
    from_assertion(outcome)
}

/// Fold an already-unwrapped [`AssertionResult`] into the executor's
/// `Result<(), String>` failure type. Shared by the synchronous matchers
/// (via [`from_result`]) and the async `similar` path, which has already
/// mapped its provider error to a string before it has a result.
pub(super) fn from_assertion(outcome: AssertionResult) -> Result<(), String> {
    if outcome.pass {
        return Ok(());
    }
    if outcome.diffs.is_empty() {
        return Err("assertion failed".to_string());
    }
    let rendered: Vec<String> = outcome.diffs.iter().map(render_diff).collect();
    Err(rendered.join("; "))
}

/// Render a single [`AssertionDiff`] as a one-line failure fragment.
/// Keeps the JSON Pointer path (empty path renders as the root `(root)`)
/// and the matcher note so the user sees both where and why the
/// assertion failed.
fn render_diff(diff: &AssertionDiff) -> String {
    let path = if diff.path.is_empty() {
        "(root)"
    } else {
        diff.path.as_str()
    };
    let mut parts = vec![format!("at `{path}`")];
    if let Some(expected) = &diff.expected {
        parts.push(format!("expected {expected}"));
    }
    if let Some(actual) = &diff.actual {
        parts.push(format!("got {actual}"));
    }
    if let Some(note) = &diff.note {
        parts.push(note.clone());
    }
    parts.join(", ")
}

/// Recursive subset check for the `subset` matcher: every key in
/// `expected` must be present in `actual` with a value that is itself a
/// subset; leaves compare by equality.
pub(super) fn check_subset(actual: &Value, expected: &Value) -> Result<(), String> {
    match (actual, expected) {
        (Value::Object(actual_map), Value::Object(expected_map)) => {
            for (k, v) in expected_map {
                let Some(actual_v) = actual_map.get(k) else {
                    return Err(format!("subset: missing key `{k}`"));
                };
                check_subset(actual_v, v).map_err(|inner| format!("subset[{k}]: {inner}"))?;
            }
            Ok(())
        }
        (actual, expected) => {
            if actual == expected {
                Ok(())
            } else {
                Err(format!(
                    "subset leaf mismatch: expected {expected}, got {actual}"
                ))
            }
        }
    }
}
