use std::{borrow::Cow, cmp::Ordering, collections::HashMap, error::Error, fmt};

use regex::Regex;
use serde_json::{json, Value};

use crate::{
    parser::context,
    schema::{
        NumericValue, RegistryEntry, ScalarLiteral, ScalarValidationError,
        ScalarValidationErrorKind, SchemaRegistry, SchemaValidationError,
        SchemaValidationErrorKind,
    },
};
use crate::request::response_capture::{
    parse_capture_operand, resolve_capture_value_with_path_for_assertion, CaptureSource,
    CaptureTarget, CaptureValue, ResolvedAssertionCaptureValue, ResponseSnapshot,
};

#[derive(Debug, Clone)]
pub struct Assertion {
    pub raw: String,
    left: Operand,
    comparison: Comparison,
    schema_registry: SchemaRegistry,
    message: Option<String>,
    guard: Option<Guard>,
}

#[derive(Debug, Clone)]
pub struct Guard {
    display: String,
    left: Operand,
    comparison: Comparison,
}

#[derive(Debug, Clone)]
enum Operand {
    Variable(String),
    Literal(LiteralValue),
    Capture {
        source: CaptureSource,
        target: CaptureTarget,
        raw_path: String,
    },
}

#[derive(Debug, Clone)]
enum LiteralValue {
    Null,
    Bool(bool),
    Number(String),
    String(String),
    Json(Value),
}

#[derive(Debug, Clone)]
enum Comparison {
    Binary {
        operator: BinaryOperator,
        right: Operand,
    },
    Schema {
        target: String,
    },
    Matches {
        pattern: MatchPattern,
    },
}

#[derive(Debug, Clone, Copy)]
enum BinaryOperator {
    Eq,
    Ne,
    Lt,
    Lte,
    Gt,
    Gte,
}

#[derive(Debug, Clone)]
enum MatchPattern {
    Regex(Regex),
    Contains(String),
    Json(Value),
}

#[derive(Debug)]
pub struct AssertionError(pub String, pub Option<AssertionMismatch>);

#[derive(Debug, Clone, PartialEq)]
pub struct AssertionMismatch {
    pub kind: AssertionMismatchKind,
    pub reason: AssertionMismatchReason,
    pub target: Option<String>,
    pub path: Option<String>,
    pub actual_path: Option<String>,
    pub compared_path: Option<String>,
    pub operator: Option<String>,
    pub actual: AssertionMismatchValue,
    pub expected: AssertionMismatchValue,
}

#[derive(Debug, Clone, PartialEq)]
pub struct AssertionMismatchValue {
    pub value_type: String,
    pub value: Value,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssertionMismatchKind {
    Comparison,
    Match,
    Schema,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssertionMismatchReason {
    ValueMismatch,
    TypeMismatch,
    UnexpectedEqual,
    OrderingMismatch,
    NonComparable,
    RegexNoMatch,
    SubstringNotFound,
    InvalidActualType,
    MissingField,
    MissingRequiredField,
    ArrayMemberNotFound,
    ArrayElementNotFound,
    EnumMismatch,
    FormatMismatch,
    LengthOutOfRange,
    PatternMismatch,
    RangeOutOfRange,
    ArrayItemMismatch,
    NullNotAllowed,
}

#[derive(Debug, Clone)]
struct ResolvedValue {
    value: AssertionValue,
    path: Option<String>,
}

#[derive(Debug, Clone)]
struct MatchFailure {
    path: Option<String>,
    reason: AssertionMismatchReason,
    actual: AssertionMismatchValue,
    expected: AssertionMismatchValue,
}

#[derive(Debug, Clone)]
enum AssertionValue {
    Missing,
    Json(Value),
    RawText(String),
}

impl fmt::Display for AssertionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Error for AssertionError {}

impl AssertionError {
    fn new(message: impl Into<String>) -> Self {
        Self(message.into(), None)
    }

    fn with_mismatch(message: impl Into<String>, mismatch: AssertionMismatch) -> Self {
        Self(message.into(), Some(mismatch))
    }
}

impl AssertionMismatchKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Comparison => "comparison",
            Self::Match => "match",
            Self::Schema => "schema",
        }
    }
}

impl AssertionMismatchReason {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::ValueMismatch => "value_mismatch",
            Self::TypeMismatch => "type_mismatch",
            Self::UnexpectedEqual => "unexpected_equal",
            Self::OrderingMismatch => "ordering_mismatch",
            Self::NonComparable => "non_comparable",
            Self::RegexNoMatch => "regex_no_match",
            Self::SubstringNotFound => "substring_not_found",
            Self::InvalidActualType => "invalid_actual_type",
            Self::MissingField => "missing_field",
            Self::ArrayMemberNotFound => "array_member_not_found",
            Self::ArrayElementNotFound => "array_element_not_found",
            Self::MissingRequiredField => "missing_required_field",
            Self::EnumMismatch => "enum_mismatch",
            Self::FormatMismatch => "format_mismatch",
            Self::LengthOutOfRange => "length_out_of_range",
            Self::PatternMismatch => "pattern_mismatch",
            Self::RangeOutOfRange => "range_out_of_range",
            Self::ArrayItemMismatch => "array_item_mismatch",
            Self::NullNotAllowed => "null_not_allowed",
        }
    }
}

impl Assertion {
    pub fn parse(raw: &str, context_map: &HashMap<String, String>) -> Result<Self, AssertionError> {
        let schema_registry = SchemaRegistry::default();
        Self::parse_with_registry(raw, context_map, &schema_registry)
    }

    pub fn parse_with_registry(
        raw: &str,
        context_map: &HashMap<String, String>,
        schema_registry: &SchemaRegistry,
    ) -> Result<Self, AssertionError> {
        let trimmed = raw.trim();
        let (guard_source, remainder) = extract_guard_prefix(trimmed)?;
        let remainder = remainder.trim_start();
        let without_prefix = remainder
            .strip_prefix('^')
            .ok_or_else(|| AssertionError::new("assertion must begin with '^'"))?
            .trim();

        if without_prefix.is_empty() {
            return Err(AssertionError::new("assertion expression is empty"));
        }

        let (expr_part, message_part) = split_message(without_prefix)?;

        let expr_injected = context::inject_from_variable(expr_part, context_map);
        let message = message_part.map(|m| {
            let injected = context::inject_from_variable(m, context_map);
            normalize_literal(&injected)
        });

        let (lhs, operator, rhs) = split_expression(expr_injected.as_str())?;

        let left = Operand::parse(lhs)?;
        let comparison = build_comparison(operator, rhs, schema_registry)?;

        let guard = match guard_source {
            Some(expr) => Some(Guard::parse(expr.as_str(), context_map)?),
            None => None,
        };

        Ok(Self {
            raw: trimmed.to_string(),
            left,
            comparison,
            schema_registry: schema_registry.clone(),
            message,
            guard,
        })
    }

    pub fn evaluate(
        &self,
        env: &HashMap<String, String>,
        current_snapshot: &ResponseSnapshot,
        dependency_snapshots: &HashMap<String, ResponseSnapshot>,
    ) -> Result<(), AssertionError> {
        let left_value = self
            .left
            .resolve(env, current_snapshot, dependency_snapshots)?;

        match &self.comparison {
            Comparison::Binary { operator, right } => {
                let right_value = right.resolve(env, current_snapshot, dependency_snapshots)?;
                if evaluate_binary(operator, &left_value, &right_value) {
                    Ok(())
                } else {
                    let mismatch = build_binary_mismatch(*operator, &left_value, &right_value);
                    let failure_message = self.message.clone().unwrap_or_else(|| {
                        format_binary_failure(&self.raw, &mismatch)
                    });
                    Err(AssertionError::with_mismatch(failure_message, mismatch))
                }
            }
            Comparison::Schema { target } => evaluate_schema_comparison(
                &self.raw,
                &self.schema_registry,
                target,
                &left_value,
                self.message.as_deref(),
            ),
            Comparison::Matches { pattern } => {
                match pattern.evaluate(&left_value) {
                    Ok(()) => Ok(()),
                    Err(detail) => {
                        let mismatch = build_match_mismatch(&left_value, pattern, &detail);
                        let failure_message = self.message.clone().unwrap_or_else(|| {
                            format_match_failure_with_detail(&self.raw, &left_value, pattern, &mismatch)
                        });
                        Err(AssertionError::with_mismatch(failure_message, mismatch))
                    }
                }
            }
        }
    }

    pub fn should_execute(
        &self,
        env: &HashMap<String, String>,
        current_snapshot: &ResponseSnapshot,
        dependency_snapshots: &HashMap<String, ResponseSnapshot>,
    ) -> Result<bool, AssertionError> {
        match &self.guard {
            Some(condition) => condition.evaluate(env, current_snapshot, dependency_snapshots),
            None => Ok(true),
        }
    }
}

impl Guard {
    pub fn parse(raw: &str, context_map: &HashMap<String, String>) -> Result<Self, AssertionError> {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            return Err(AssertionError::new("guard predicate is empty"));
        }

        let injected = context::inject_from_variable(trimmed, context_map);
        let (lhs, operator, rhs) = split_expression(injected.as_str())?;
        if matches!(operator, OperatorToken::SchemaEq) {
            return Err(AssertionError::new(
                "schema validation operator is not supported in guards",
            ));
        }
        let normalized_rhs = normalize_guard_rhs(rhs);

        let left = Operand::parse(lhs)?;
        let schema_registry = SchemaRegistry::default();
        let comparison = build_comparison(operator, normalized_rhs.as_ref(), &schema_registry)?;

        Ok(Self {
            display: injected,
            left,
            comparison,
        })
    }

    pub fn evaluate(
        &self,
        env: &HashMap<String, String>,
        current_snapshot: &ResponseSnapshot,
        dependency_snapshots: &HashMap<String, ResponseSnapshot>,
    ) -> Result<bool, AssertionError> {
        let label = format!("[{}]", self.display);
        let left_value = self
            .left
            .resolve(env, current_snapshot, dependency_snapshots)
            .map_err(|err| {
                AssertionError::new(format!("Failed to evaluate guard {}: {}", label, err.0))
            })?;

        match &self.comparison {
            Comparison::Binary { operator, right } => {
                let right_value = right
                    .resolve(env, current_snapshot, dependency_snapshots)
                    .map_err(|err| {
                        AssertionError::new(format!("Failed to evaluate guard {}: {}", label, err.0))
                    })?;

                Ok(evaluate_binary(operator, &left_value, &right_value))
            }
            Comparison::Schema { .. } => Err(AssertionError::new(format!(
                "Failed to evaluate guard {}: schema validation operator is not supported in guards",
                label
            ))),
            Comparison::Matches { pattern } => Ok(pattern.evaluate(&left_value).is_ok()),
        }
    }

    pub fn evaluate_with_env(&self, env: &HashMap<String, String>) -> Result<bool, AssertionError> {
        let snapshot = ResponseSnapshot::default();
        let deps = HashMap::new();
        self.evaluate(env, &snapshot, &deps)
    }
}

fn extract_guard_prefix(input: &str) -> Result<(Option<String>, &str), AssertionError> {
    let trimmed = input.trim_start();
    if !trimmed.starts_with('[') {
        return Ok((None, input));
    }

    let bytes = trimmed.as_bytes();
    let mut idx = 1;
    let mut in_single = false;
    let mut in_double = false;

    while idx < bytes.len() {
        let ch = bytes[idx] as char;
        match ch {
            '\'' if !in_double => in_single = !in_single,
            '"' if !in_single => in_double = !in_double,
            ']' if !in_single && !in_double => {
                let predicate = trimmed[1..idx].trim();
                if predicate.is_empty() {
                    return Err(AssertionError::new("guard predicate is empty"));
                }
                let remainder = &trimmed[idx + 1..];
                return Ok((Some(predicate.to_string()), remainder));
            }
            _ => {}
        }
        idx += 1;
    }

    Err(AssertionError::new("guard predicate is missing closing ']'") )
}

fn build_comparison(
    operator: OperatorToken,
    rhs: &str,
    schema_registry: &SchemaRegistry,
) -> Result<Comparison, AssertionError> {
    match operator {
        OperatorToken::SchemaEq => {
            let target = parse_schema_target(rhs, schema_registry)?;
            Ok(Comparison::Schema { target })
        }
        OperatorToken::Matches => {
            let pattern = MatchPattern::parse(rhs)?;
            Ok(Comparison::Matches { pattern })
        }
        OperatorToken::Eq => Ok(Comparison::Binary {
            operator: BinaryOperator::Eq,
            right: Operand::parse(rhs)?,
        }),
        OperatorToken::Ne => Ok(Comparison::Binary {
            operator: BinaryOperator::Ne,
            right: Operand::parse(rhs)?,
        }),
        OperatorToken::Lt => Ok(Comparison::Binary {
            operator: BinaryOperator::Lt,
            right: Operand::parse(rhs)?,
        }),
        OperatorToken::Lte => Ok(Comparison::Binary {
            operator: BinaryOperator::Lte,
            right: Operand::parse(rhs)?,
        }),
        OperatorToken::Gt => Ok(Comparison::Binary {
            operator: BinaryOperator::Gt,
            right: Operand::parse(rhs)?,
        }),
        OperatorToken::Gte => Ok(Comparison::Binary {
            operator: BinaryOperator::Gte,
            right: Operand::parse(rhs)?,
        }),
    }
}

impl Operand {
    fn parse(input: &str) -> Result<Self, AssertionError> {
        let trimmed = input.trim();
        if trimmed.is_empty() {
            return Err(AssertionError::new("missing operand"));
        }

        if trimmed.starts_with('&') {
            let (source, target, raw_path) =
                parse_capture_operand(trimmed).map_err(|err| AssertionError::new(err.0))?;
            return Ok(Operand::Capture {
                source,
                target,
                raw_path,
            });
        }

        if trimmed.starts_with('$') {
            let key = trimmed.trim_start_matches('$').trim().to_string();
            if key.is_empty() {
                return Err(AssertionError::new("invalid variable reference"));
            }
            return Ok(Operand::Variable(key));
        }

        if is_null_literal(trimmed) {
            return Ok(Operand::Literal(LiteralValue::Null));
        }

        if is_numeric_literal(trimmed) {
            return Ok(Operand::Literal(LiteralValue::Number(trimmed.to_string())));
        }

        if is_boolean_literal(trimmed) {
            return Ok(Operand::Literal(LiteralValue::Bool(trimmed == "true")));
        }

        if (trimmed.starts_with('"') && trimmed.ends_with('"'))
            || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
        {
            return Ok(Operand::Literal(LiteralValue::String(normalize_literal(trimmed))));
        }

        if let Some(value) = parse_json_literal(trimmed) {
            return Ok(Operand::Literal(LiteralValue::Json(value)));
        }

        Ok(Operand::Variable(trimmed.to_string()))
    }

    fn resolve(
        &self,
        env: &HashMap<String, String>,
        current_snapshot: &ResponseSnapshot,
        dependency_snapshots: &HashMap<String, ResponseSnapshot>,
    ) -> Result<ResolvedValue, AssertionError> {
        match self {
            Operand::Variable(name) => env
                .get(name)
                .cloned()
                .map(ResolvedValue::raw_text)
                .ok_or_else(|| AssertionError::new(format!("No value found for variable '{}'.", name))),
            Operand::Literal(value) => Ok(ResolvedValue::from_literal(value)),
            Operand::Capture {
                source,
                target,
                raw_path,
            } => {
                let snapshot = match source {
                    CaptureSource::Current => current_snapshot,
                    CaptureSource::Dependency(dep) => dependency_snapshots.get(dep).ok_or_else(|| {
                        AssertionError::new(format!(
                            "No response snapshot available for dependency '{}' referenced in assertion.",
                            dep
                        ))
                    })?,
                };

                resolve_capture_value_with_path_for_assertion(target, raw_path, snapshot, env)
                    .map(|value| ResolvedValue::from_capture(source, raw_path, value))
                    .map_err(|err| AssertionError::new(err.0))
            }
        }
    }
}

impl MatchPattern {
    fn parse(input: &str) -> Result<Self, AssertionError> {
        let trimmed = input.trim();
        if trimmed.is_empty() {
            return Err(AssertionError::new(
                "matches operator requires a right-hand operand",
            ));
        }

        let literal = if (trimmed.starts_with('"') && trimmed.ends_with('"'))
            || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
        {
            normalize_literal(trimmed)
        } else {
            trimmed.to_string()
        };

        if literal.starts_with('/') && literal.ends_with('/') && literal.len() >= 2 {
            let pattern = &literal[1..literal.len() - 1];
            let regex = Regex::new(pattern)
                .map_err(|e| AssertionError::new(format!("Invalid regex in assertion: {}", e)))?;
            Ok(MatchPattern::Regex(regex))
        } else if let Some(value) = parse_json_literal(trimmed) {
            Ok(MatchPattern::Json(value))
        } else {
            Ok(MatchPattern::Contains(literal))
        }
    }

    fn evaluate(&self, value: &ResolvedValue) -> Result<(), MatchFailure> {
        match self {
            MatchPattern::Regex(regex) => {
                let Some(actual) = value.as_match_text() else {
                    return Err(match_failure(
                        value,
                        value.path.clone(),
                        AssertionMismatchReason::InvalidActualType,
                        self.mismatch_value(),
                    ));
                };

                if regex.is_match(actual) {
                    Ok(())
                } else {
                    Err(match_failure(
                        value,
                        value.path.clone(),
                        AssertionMismatchReason::RegexNoMatch,
                        self.mismatch_value(),
                    ))
                }
            }
            MatchPattern::Contains(expected) => {
                let Some(actual) = value.as_match_text() else {
                    return Err(match_failure(
                        value,
                        value.path.clone(),
                        AssertionMismatchReason::InvalidActualType,
                        self.mismatch_value(),
                    ));
                };

                if actual.contains(expected) {
                    Ok(())
                } else {
                    Err(match_failure(
                        value,
                        value.path.clone(),
                        AssertionMismatchReason::SubstringNotFound,
                        self.mismatch_value(),
                    ))
                }
            }
            MatchPattern::Json(expected) => match_structural_value(&value.value, expected, value.path.as_deref()),
        }
    }

    fn display(&self) -> String {
        match self {
            MatchPattern::Regex(regex) => format!("/{}/", regex.as_str()),
            MatchPattern::Contains(expected) => render_string(expected),
            MatchPattern::Json(value) => value.to_string(),
        }
    }

    fn mismatch_value(&self) -> AssertionMismatchValue {
        match self {
            MatchPattern::Regex(regex) => AssertionMismatchValue {
                value_type: "regex".to_string(),
                value: Value::String(regex.as_str().to_string()),
            },
            MatchPattern::Contains(expected) => AssertionMismatchValue {
                value_type: "substring".to_string(),
                value: Value::String(expected.clone()),
            },
            MatchPattern::Json(value) => AssertionMismatchValue {
                value_type: json_value_type_name(value).to_string(),
                value: value.clone(),
            },
        }
    }
}

#[derive(Debug, Clone, Copy)]
enum OperatorToken {
    SchemaEq,
    Eq,
    Ne,
    Lt,
    Lte,
    Gt,
    Gte,
    Matches,
}

fn split_expression(input: &str) -> Result<(&str, OperatorToken, &str), AssertionError> {
    let bytes = input.as_bytes();
    let mut in_single = false;
    let mut in_double = false;
    let mut bracket_depth = 0usize;
    let mut i = 0;

    while i < bytes.len() {
        let ch = bytes[i] as char;
        match ch {
            '\'' if !in_double => in_single = !in_single,
            '"' if !in_single => in_double = !in_double,
            '[' if !in_single && !in_double => bracket_depth += 1,
            ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
            _ => {}
        }

        if in_single || in_double || bracket_depth > 0 {
            i += 1;
            continue;
        }

        if i + 2 < bytes.len() && &input[i..i + 3] == "===" {
            let left = input[..i].trim();
            let right = input[i + 3..].trim();
            if left.is_empty() || right.is_empty() {
                return Err(AssertionError::new("invalid assertion expression"));
            }
            return Ok((left, OperatorToken::SchemaEq, right));
        }

        if i + 1 < bytes.len() {
            let two = &input[i..i + 2];
            let op = match two {
                "==" => Some(OperatorToken::Eq),
                "!=" => Some(OperatorToken::Ne),
                ">=" => Some(OperatorToken::Gte),
                "<=" => Some(OperatorToken::Lte),
                "~=" => Some(OperatorToken::Matches),
                _ => None,
            };
            if let Some(token) = op {
                let left = input[..i].trim();
                let right = input[i + 2..].trim();
                if left.is_empty() || right.is_empty() {
                    return Err(AssertionError::new("invalid assertion expression"));
                }
                return Ok((left, token, right));
            }
        }

        let op = match ch {
            '>' => Some(OperatorToken::Gt),
            '<' => Some(OperatorToken::Lt),
            _ => None,
        };

        if let Some(token) = op {
            let left = input[..i].trim();
            let right = input[i + 1..].trim();
            if left.is_empty() || right.is_empty() {
                return Err(AssertionError::new("invalid assertion expression"));
            }
            return Ok((left, token, right));
        }

        i += 1;
    }

    Err(AssertionError::new("No valid operator found in assertion"))
}

fn split_message(input: &str) -> Result<(&str, Option<&str>), AssertionError> {
    let bytes = input.as_bytes();
    let mut in_single = false;
    let mut in_double = false;
    let mut bracket_depth = 0usize;
    let mut i = 0;

    while i + 1 < bytes.len() {
        let ch = bytes[i] as char;
        match ch {
            '\'' if !in_double => in_single = !in_single,
            '"' if !in_single => in_double = !in_double,
            '[' if !in_single && !in_double => bracket_depth += 1,
            ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
            _ => {}
        }

        if !in_single
            && !in_double
            && bracket_depth == 0
            && ch == '|'
            && bytes[i + 1] as char == '|'
        {
            let expr = input[..i].trim();
            let message = input[i + 2..].trim();
            if expr.is_empty() {
                return Err(AssertionError::new("assertion expression is empty"));
            }
            if message.is_empty() {
                return Err(AssertionError::new("assertion message is empty"));
            }
            return Ok((expr, Some(message)));
        }

        i += 1;
    }

    Ok((input, None))
}

fn normalize_literal(input: &str) -> String {
    let trimmed = input.trim();
    if (trimmed.starts_with('"') && trimmed.ends_with('"'))
        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
    {
        trimmed[1..trimmed.len() - 1].to_string()
    } else {
        trimmed.to_string()
    }
}

fn parse_schema_target(
    input: &str,
    schema_registry: &SchemaRegistry,
) -> Result<String, AssertionError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(AssertionError::new(
            "schema validation operator requires a target name",
        ));
    }

    if !is_identifier(trimmed) {
        return Err(AssertionError::new(
            "schema validation target must be a declared scalar or schema name",
        ));
    }

    if schema_registry.get(trimmed).is_none() {
        return Err(AssertionError::new(format!(
            "Unknown schema validation target '{}'",
            trimmed
        )));
    }

    Ok(trimmed.to_string())
}

fn is_identifier(input: &str) -> bool {
    let mut chars = input.chars();
    let Some(first) = chars.next() else {
        return false;
    };

    if !(first.is_ascii_alphabetic() || first == '_') {
        return false;
    }

    chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}

fn is_numeric_literal(input: &str) -> bool {
    input.trim().parse::<f64>().is_ok()
}

fn is_boolean_literal(input: &str) -> bool {
    matches!(input.trim(), "true" | "false")
}

fn is_null_literal(input: &str) -> bool {
    input.trim() == "null"
}

fn parse_json_literal(input: &str) -> Option<Value> {
    let trimmed = input.trim();
    if !(trimmed.starts_with('{') || trimmed.starts_with('[')) {
        return None;
    }

    serde_json::from_str::<Value>(trimmed).ok()
}

fn normalize_guard_rhs(input: &str) -> Cow<'_, str> {
    let trimmed = input.trim();
    if trimmed.is_empty()
        || trimmed.starts_with('$')
        || trimmed.starts_with('&')
        || trimmed.starts_with('{')
        || trimmed.starts_with('[')
        || trimmed.starts_with('"')
        || trimmed.starts_with('\'')
        || trimmed.starts_with('/')
        || is_boolean_literal(trimmed)
        || is_null_literal(trimmed)
        || is_numeric_literal(trimmed)
        || trimmed.chars().any(|ch| ch.is_ascii_uppercase())
    {
        Cow::Borrowed(trimmed)
    } else {
        Cow::Owned(format!("'{}'", trimmed))
    }
}

impl BinaryOperator {
    fn as_str(&self) -> &'static str {
        match self {
            Self::Eq => "==",
            Self::Ne => "!=",
            Self::Lt => "<",
            Self::Lte => "<=",
            Self::Gt => ">",
            Self::Gte => ">=",
        }
    }
}

impl ResolvedValue {
    fn raw_text(value: String) -> Self {
        Self {
            value: AssertionValue::RawText(value),
            path: None,
        }
    }

    fn from_literal(value: &LiteralValue) -> Self {
        let value = match value {
            LiteralValue::Null => AssertionValue::Json(Value::Null),
            LiteralValue::Bool(boolean) => AssertionValue::Json(Value::Bool(*boolean)),
            LiteralValue::Number(number) => AssertionValue::Json(Value::from(
                number.parse::<f64>().expect("validated numeric literal"),
            )),
            LiteralValue::String(text) => AssertionValue::Json(Value::String(text.clone())),
            LiteralValue::Json(value) => AssertionValue::Json(value.clone()),
        };

        Self { value, path: None }
    }

    fn from_capture(
        source: &CaptureSource,
        raw_path: &str,
        resolved: ResolvedAssertionCaptureValue,
    ) -> Self {
        let resolved_path = resolved.resolved_path.unwrap_or_else(|| raw_path.to_string());
        let path = match source {
            CaptureSource::Current => resolved_path,
            CaptureSource::Dependency(name) => format!("[{}].{}", name, resolved_path),
        };

        let value = match resolved.value {
            CaptureValue::Missing => AssertionValue::Missing,
            CaptureValue::Json(value) => AssertionValue::Json(value),
            CaptureValue::RawText(value) => AssertionValue::RawText(value),
        };

        Self {
            value,
            path: Some(path),
        }
    }

    fn type_name(&self) -> &'static str {
        match &self.value {
            AssertionValue::Missing => "missing",
            AssertionValue::RawText(_) => "string",
            AssertionValue::Json(Value::Null) => "null",
            AssertionValue::Json(Value::Bool(_)) => "boolean",
            AssertionValue::Json(Value::Number(_)) => "number",
            AssertionValue::Json(Value::String(_)) => "string",
            AssertionValue::Json(Value::Array(_)) => "array",
            AssertionValue::Json(Value::Object(_)) => "object",
        }
    }

    fn as_match_text(&self) -> Option<&str> {
        match &self.value {
            AssertionValue::Missing => None,
            AssertionValue::RawText(text) => Some(text.as_str()),
            AssertionValue::Json(Value::String(text)) => Some(text.as_str()),
            _ => None,
        }
    }

    fn match_display(&self) -> String {
        match &self.value {
            AssertionValue::Missing => "<missing>".to_string(),
            AssertionValue::RawText(text) => render_string(text),
            AssertionValue::Json(Value::String(text)) => render_string(text),
            AssertionValue::Json(value) => value.to_string(),
        }
    }

    fn mismatch_value(&self) -> AssertionMismatchValue {
        mismatch_value_from_assertion_value(&self.value)
    }
}

fn render_string(value: &str) -> String {
    serde_json::to_string(value).unwrap_or_else(|_| format!("\"{}\"", value))
}

fn values_equal(left: &AssertionValue, right: &AssertionValue) -> bool {
    match (left, right) {
        (AssertionValue::Missing, AssertionValue::Missing) => true,
        (AssertionValue::Json(lhs), AssertionValue::Json(rhs)) => json_values_equal(lhs, rhs),
        (AssertionValue::RawText(lhs), AssertionValue::RawText(rhs)) => lhs == rhs,
        (AssertionValue::RawText(text), AssertionValue::Json(json))
        | (AssertionValue::Json(json), AssertionValue::RawText(text)) => {
            raw_text_equals_json(text, json)
        }
        _ => false,
    }
}

fn json_values_equal(left: &Value, right: &Value) -> bool {
    match (left, right) {
        (Value::Number(lhs), Value::Number(rhs)) => compare_json_numbers(lhs, rhs)
            .map(|ordering| ordering == Ordering::Equal)
            .unwrap_or(false),
        _ => left == right,
    }
}

fn raw_text_equals_json(text: &str, json: &Value) -> bool {
    match json {
        Value::String(expected) => text == expected,
        Value::Bool(expected) => parse_bool(text) == Some(*expected),
        Value::Number(expected) => compare_text_to_number(text, expected)
            .map(|ordering| ordering == Ordering::Equal)
            .unwrap_or(false),
        _ => false,
    }
}

fn compare_json_numbers(left: &serde_json::Number, right: &serde_json::Number) -> Option<Ordering> {
    left.as_f64()?.partial_cmp(&right.as_f64()?)
}

fn compare_text_to_number(text: &str, number: &serde_json::Number) -> Option<Ordering> {
    text.trim().parse::<f64>().ok()?.partial_cmp(&number.as_f64()?)
}

fn compare_values(left: &AssertionValue, right: &AssertionValue) -> Option<Ordering> {
    match (left, right) {
        (AssertionValue::Json(Value::Number(lhs)), AssertionValue::Json(Value::Number(rhs))) => {
            compare_json_numbers(lhs, rhs)
        }
        (AssertionValue::RawText(lhs), AssertionValue::RawText(rhs)) => Some(lhs.cmp(rhs)),
        (AssertionValue::Json(Value::String(lhs)), AssertionValue::Json(Value::String(rhs))) => {
            Some(lhs.cmp(rhs))
        }
        (AssertionValue::RawText(text), AssertionValue::Json(Value::Number(number))) => {
            compare_text_to_number(text, number)
        }
        (AssertionValue::Json(Value::Number(number)), AssertionValue::RawText(text)) => {
            compare_text_to_number(text, number).map(Ordering::reverse)
        }
        (AssertionValue::RawText(lhs), AssertionValue::Json(Value::String(rhs))) => {
            Some(lhs.as_str().cmp(rhs.as_str()))
        }
        (AssertionValue::Json(Value::String(lhs)), AssertionValue::RawText(rhs)) => {
            Some(lhs.as_str().cmp(rhs.as_str()))
        }
        _ => None,
    }
}

fn join_object_path(base: Option<&str>, key: &str) -> String {
    match base {
        Some(base) if !base.is_empty() => format!("{}.{}", base, key),
        _ => key.to_string(),
    }
}

fn join_array_path(base: Option<&str>, index: usize) -> String {
    match base {
        Some(base) if !base.is_empty() => format!("{}[{}]", base, index),
        _ => format!("[{}]", index),
    }
}

fn match_failure(
    actual: &ResolvedValue,
    path: Option<String>,
    reason: AssertionMismatchReason,
    expected: AssertionMismatchValue,
) -> MatchFailure {
    MatchFailure {
        path,
        reason,
        actual: actual.mismatch_value(),
        expected,
    }
}

fn match_failure_from_value(
    actual: &AssertionValue,
    path: Option<String>,
    expected: &Value,
) -> MatchFailure {
    MatchFailure {
        path,
        reason: mismatch_reason_for_values(actual, &AssertionValue::Json(expected.clone())),
        actual: mismatch_value_from_assertion_value(actual),
        expected: AssertionMismatchValue {
            value_type: json_value_type_name(expected).to_string(),
            value: expected.clone(),
        },
    }
}

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

fn json_value_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",
    }
}

fn mismatch_value_from_assertion_value(value: &AssertionValue) -> AssertionMismatchValue {
    match value {
        AssertionValue::Missing => AssertionMismatchValue {
            value_type: "missing".to_string(),
            value: Value::Null,
        },
        AssertionValue::RawText(text) => AssertionMismatchValue {
            value_type: "string".to_string(),
            value: Value::String(text.clone()),
        },
        AssertionValue::Json(value) => AssertionMismatchValue {
            value_type: json_value_type_name(value).to_string(),
            value: value.clone(),
        },
    }
}

fn mismatch_reason_for_values(
    actual: &AssertionValue,
    expected: &AssertionValue,
) -> AssertionMismatchReason {
    match actual {
        AssertionValue::Missing => AssertionMismatchReason::MissingField,
        _ if assertion_value_type_name(actual) != assertion_value_type_name(expected) => {
            AssertionMismatchReason::TypeMismatch
        }
        _ => AssertionMismatchReason::ValueMismatch,
    }
}

fn build_binary_mismatch(
    operator: BinaryOperator,
    left: &ResolvedValue,
    right: &ResolvedValue,
) -> AssertionMismatch {
    let reason = match operator {
        BinaryOperator::Eq => mismatch_reason_for_values(&left.value, &right.value),
        BinaryOperator::Ne => AssertionMismatchReason::UnexpectedEqual,
        BinaryOperator::Lt | BinaryOperator::Lte | BinaryOperator::Gt | BinaryOperator::Gte => {
            if compare_values(&left.value, &right.value).is_none() {
                AssertionMismatchReason::NonComparable
            } else {
                AssertionMismatchReason::OrderingMismatch
            }
        }
    };

    AssertionMismatch {
        kind: AssertionMismatchKind::Comparison,
        reason,
        target: None,
        path: left.path.clone(),
        actual_path: left.path.clone(),
        compared_path: right.path.clone(),
        operator: Some(operator.as_str().to_string()),
        actual: left.mismatch_value(),
        expected: right.mismatch_value(),
    }
}

fn build_match_mismatch(
    left: &ResolvedValue,
    pattern: &MatchPattern,
    failure: &MatchFailure,
) -> AssertionMismatch {
    AssertionMismatch {
        kind: AssertionMismatchKind::Match,
        reason: failure.reason,
        target: None,
        path: failure.path.clone().or_else(|| left.path.clone()),
        actual_path: left.path.clone(),
        compared_path: None,
        operator: Some("~=".to_string()),
        actual: failure.actual.clone(),
        expected: if matches!(pattern, MatchPattern::Json(_)) {
            failure.expected.clone()
        } else {
            pattern.mismatch_value()
        },
    }
}

fn evaluate_schema_comparison(
    raw: &str,
    schema_registry: &SchemaRegistry,
    target: &str,
    left: &ResolvedValue,
    custom_message: Option<&str>,
) -> Result<(), AssertionError> {
    let AssertionValue::Json(value) = &left.value else {
        let mismatch = build_schema_invalid_actual_mismatch(left, target);
        let failure_message = custom_message
            .map(str::to_string)
            .unwrap_or_else(|| {
                format_schema_failure(
                    raw,
                    left,
                    target,
                    &mismatch,
                    "schema validation requires a typed JSON value on the left-hand side",
                )
            });
        return Err(AssertionError::with_mismatch(failure_message, mismatch));
    };

    let result = match schema_registry.get(target) {
        Some(RegistryEntry::BuiltinScalar(_) | RegistryEntry::Scalar(_)) => schema_registry
            .validate_scalar_target(target, value)
            .map_err(SchemaAssertionFailure::Scalar),
        Some(RegistryEntry::Schema(_)) => schema_registry
            .validate_schema_target(target, value)
            .map_err(SchemaAssertionFailure::Schema),
        None => {
            return Err(AssertionError::new(format!(
                "Unknown schema validation target '{}'",
                target
            )))
        }
    };

    match result {
        Ok(()) => Ok(()),
        Err(failure) => {
            let mismatch = build_schema_mismatch(left, target, &failure);
            let failure_message = custom_message
                .map(str::to_string)
                .unwrap_or_else(|| format_schema_failure(raw, left, target, &mismatch, failure.detail()));
            Err(AssertionError::with_mismatch(failure_message, mismatch))
        }
    }
}

#[derive(Debug)]
enum SchemaAssertionFailure {
    Scalar(ScalarValidationError),
    Schema(SchemaValidationError),
}

impl SchemaAssertionFailure {
    fn detail(&self) -> String {
        match self {
            Self::Scalar(error) => error.to_string(),
            Self::Schema(error) => error.to_string(),
        }
    }
}

fn build_schema_invalid_actual_mismatch(
    left: &ResolvedValue,
    target: &str,
) -> AssertionMismatch {
    AssertionMismatch {
        kind: AssertionMismatchKind::Schema,
        reason: AssertionMismatchReason::InvalidActualType,
        target: Some(target.to_string()),
        path: left.path.clone(),
        actual_path: left.path.clone(),
        compared_path: None,
        operator: Some("===".to_string()),
        actual: left.mismatch_value(),
        expected: assertion_mismatch_value("typed_json", Value::String(target.to_string())),
    }
}

fn build_schema_mismatch(
    left: &ResolvedValue,
    target: &str,
    failure: &SchemaAssertionFailure,
) -> AssertionMismatch {
    match failure {
        SchemaAssertionFailure::Scalar(error) => {
            let reason = mismatch_reason_for_scalar_validation(error.actual_type.as_str(), &error.reason);
            let reason = apply_array_item_context(left.path.as_deref(), reason);

            AssertionMismatch {
                kind: AssertionMismatchKind::Schema,
                reason,
                target: Some(target.to_string()),
                path: left.path.clone(),
                actual_path: left.path.clone(),
                compared_path: None,
                operator: Some("===".to_string()),
                actual: AssertionMismatchValue {
                    value_type: error.actual_type.clone(),
                    value: error.actual.clone(),
                },
                expected: expected_value_for_scalar_validation(&error.reason),
            }
        }
        SchemaAssertionFailure::Schema(error) => {
            let path = compose_schema_path(left.path.as_deref(), error.path.as_str());
            let reason = mismatch_reason_for_schema_validation(error);
            let reason = apply_array_item_context(path.as_deref(), reason);

            AssertionMismatch {
                kind: AssertionMismatchKind::Schema,
                reason,
                target: Some(target.to_string()),
                path,
                actual_path: left.path.clone(),
                compared_path: None,
                operator: Some("===".to_string()),
                actual: AssertionMismatchValue {
                    value_type: error.actual_type.clone(),
                    value: error.actual.clone(),
                },
                expected: expected_value_for_schema_validation(&error.reason),
            }
        }
    }
}

fn mismatch_reason_for_scalar_validation(
    actual_type: &str,
    reason: &ScalarValidationErrorKind,
) -> AssertionMismatchReason {
    match reason {
        ScalarValidationErrorKind::TypeMismatch { .. } if actual_type == "null" => {
            AssertionMismatchReason::NullNotAllowed
        }
        ScalarValidationErrorKind::TypeMismatch { .. } => AssertionMismatchReason::TypeMismatch,
        ScalarValidationErrorKind::EnumMismatch { .. } => AssertionMismatchReason::EnumMismatch,
        ScalarValidationErrorKind::FormatMismatch { .. }
        | ScalarValidationErrorKind::UnknownFormat(_) => AssertionMismatchReason::FormatMismatch,
        ScalarValidationErrorKind::LengthOutOfRange { .. } => {
            AssertionMismatchReason::LengthOutOfRange
        }
        ScalarValidationErrorKind::PatternMismatch { .. }
        | ScalarValidationErrorKind::InvalidPattern { .. } => AssertionMismatchReason::PatternMismatch,
        ScalarValidationErrorKind::RangeOutOfRange { .. } => AssertionMismatchReason::RangeOutOfRange,
        ScalarValidationErrorKind::UnknownTarget(_) | ScalarValidationErrorKind::TargetIsSchema(_) => {
            AssertionMismatchReason::ValueMismatch
        }
    }
}

fn mismatch_reason_for_schema_validation(
    error: &SchemaValidationError,
) -> AssertionMismatchReason {
    match &error.reason {
        SchemaValidationErrorKind::MissingRequiredField { .. } => {
            AssertionMismatchReason::MissingRequiredField
        }
        SchemaValidationErrorKind::TypeMismatch { .. } if error.actual_type == "null" => {
            AssertionMismatchReason::NullNotAllowed
        }
        SchemaValidationErrorKind::TypeMismatch { .. } => AssertionMismatchReason::TypeMismatch,
        SchemaValidationErrorKind::ScalarViolation { reason, .. } => {
            mismatch_reason_for_scalar_validation(error.actual_type.as_str(), reason)
        }
        SchemaValidationErrorKind::UnknownTarget(_) | SchemaValidationErrorKind::TargetIsScalar(_) => {
            AssertionMismatchReason::ValueMismatch
        }
    }
}

fn apply_array_item_context(
    path: Option<&str>,
    reason: AssertionMismatchReason,
) -> AssertionMismatchReason {
    if path.is_some_and(|value| value.contains('['))
        && matches!(
            reason,
            AssertionMismatchReason::TypeMismatch
                | AssertionMismatchReason::EnumMismatch
                | AssertionMismatchReason::FormatMismatch
                | AssertionMismatchReason::LengthOutOfRange
                | AssertionMismatchReason::PatternMismatch
                | AssertionMismatchReason::RangeOutOfRange
                | AssertionMismatchReason::NullNotAllowed
        )
    {
        AssertionMismatchReason::ArrayItemMismatch
    } else {
        reason
    }
}

fn expected_value_for_scalar_validation(
    reason: &ScalarValidationErrorKind,
) -> AssertionMismatchValue {
    match reason {
        ScalarValidationErrorKind::UnknownTarget(target) => {
            assertion_mismatch_value("scalar_target", Value::String(target.clone()))
        }
        ScalarValidationErrorKind::TargetIsSchema(target) => {
            assertion_mismatch_value("schema_target", Value::String(target.clone()))
        }
        ScalarValidationErrorKind::TypeMismatch { expected } => {
            assertion_mismatch_value("expected_type", Value::String(expected.clone()))
        }
        ScalarValidationErrorKind::EnumMismatch { expected } => assertion_mismatch_value(
            "enum",
            Value::Array(expected.iter().map(scalar_literal_to_json).collect()),
        ),
        ScalarValidationErrorKind::FormatMismatch { format }
        | ScalarValidationErrorKind::UnknownFormat(format) => {
            assertion_mismatch_value("format", Value::String(format.clone()))
        }
        ScalarValidationErrorKind::LengthOutOfRange { min, max, .. } => assertion_mismatch_value(
            "length_range",
            json!({ "min": min, "max": max }),
        ),
        ScalarValidationErrorKind::PatternMismatch { pattern } => {
            assertion_mismatch_value("pattern", Value::String(pattern.clone()))
        }
        ScalarValidationErrorKind::InvalidPattern { pattern, message } => assertion_mismatch_value(
            "pattern",
            json!({ "pattern": pattern, "message": message }),
        ),
        ScalarValidationErrorKind::RangeOutOfRange { min, max, .. } => assertion_mismatch_value(
            "numeric_range",
            json!({
                "min": min.as_ref().map(numeric_value_to_json),
                "max": max.as_ref().map(numeric_value_to_json),
            }),
        ),
    }
}

fn expected_value_for_schema_validation(
    reason: &SchemaValidationErrorKind,
) -> AssertionMismatchValue {
    match reason {
        SchemaValidationErrorKind::UnknownTarget(target) => {
            assertion_mismatch_value("schema_target", Value::String(target.clone()))
        }
        SchemaValidationErrorKind::TargetIsScalar(target) => {
            assertion_mismatch_value("scalar_target", Value::String(target.clone()))
        }
        SchemaValidationErrorKind::MissingRequiredField { field } => {
            assertion_mismatch_value("required_field", Value::String(field.clone()))
        }
        SchemaValidationErrorKind::TypeMismatch { expected } => {
            assertion_mismatch_value("expected_type", Value::String(expected.clone()))
        }
        SchemaValidationErrorKind::ScalarViolation { reason, .. } => {
            expected_value_for_scalar_validation(reason)
        }
    }
}

fn assertion_mismatch_value(kind: &str, value: Value) -> AssertionMismatchValue {
    AssertionMismatchValue {
        value_type: kind.to_string(),
        value,
    }
}

fn scalar_literal_to_json(value: &ScalarLiteral) -> Value {
    match value {
        ScalarLiteral::String(value) => Value::String(value.clone()),
        ScalarLiteral::Integer(value) => json!(value),
        ScalarLiteral::Number(value) => json!(value),
        ScalarLiteral::Boolean(value) => Value::Bool(*value),
        ScalarLiteral::Null => Value::Null,
    }
}

fn numeric_value_to_json(value: &NumericValue) -> Value {
    match value {
        NumericValue::Integer(value) => json!(value),
        NumericValue::Number(value) => json!(value),
    }
}

fn compose_schema_path(base_path: Option<&str>, schema_path: &str) -> Option<String> {
    match (base_path, schema_path) {
        (Some(base), "$") => Some(base.to_string()),
        (None, "$") => Some("$".to_string()),
        (Some(base), _) => Some(format!("{base}{}", schema_path.strip_prefix('$').unwrap_or(schema_path))),
        (None, _) => Some(schema_path.to_string()),
    }
}

fn deeper_match_failure(left: MatchFailure, right: MatchFailure) -> MatchFailure {
    let left_depth = left.path.as_ref().map(|path| path.matches('.').count() + path.matches('[').count()).unwrap_or(0);
    let right_depth = right.path.as_ref().map(|path| path.matches('.').count() + path.matches('[').count()).unwrap_or(0);

    if right_depth > left_depth {
        right
    } else {
        left
    }
}

fn match_structural_value(
    actual: &AssertionValue,
    expected: &Value,
    base_path: Option<&str>,
) -> Result<(), MatchFailure> {
    if let AssertionValue::Json(Value::Array(actual_items)) = actual {
        if !matches!(expected, Value::Array(_)) {
            return match_array_member(actual_items, expected, base_path);
        }
    }

    match expected {
        Value::Object(expected_map) => match actual {
            AssertionValue::Json(Value::Object(actual_map)) => {
                for (key, expected_child) in expected_map {
                    let child_path = join_object_path(base_path, key);
                    let actual_child = actual_map
                        .get(key)
                        .cloned()
                        .map(AssertionValue::Json)
                        .unwrap_or(AssertionValue::Missing);

                    match_structural_value(&actual_child, expected_child, Some(child_path.as_str()))?;
                }

                Ok(())
            }
            _ => Err(match_failure_from_value(
                actual,
                base_path.map(str::to_string),
                expected,
            )),
        },
        Value::Array(expected_items) => match actual {
            AssertionValue::Json(Value::Array(actual_items)) => {
                match_array_subset(actual_items, expected_items, base_path)
            }
            _ => Err(match_failure_from_value(
                actual,
                base_path.map(str::to_string),
                expected,
            )),
        },
        _ => {
            let expected_value = AssertionValue::Json(expected.clone());
            if values_equal(actual, &expected_value) {
                Ok(())
            } else {
                Err(match_failure_from_value(
                    actual,
                    base_path.map(str::to_string),
                    expected,
                ))
            }
        }
    }
}

fn match_array_member(
    actual_items: &[Value],
    expected: &Value,
    base_path: Option<&str>,
) -> Result<(), MatchFailure> {
    let mut best_failure: Option<MatchFailure> = None;

    for (index, item) in actual_items.iter().enumerate() {
        let item_path = join_array_path(base_path, index);
        match match_structural_value(&AssertionValue::Json(item.clone()), expected, Some(item_path.as_str())) {
            Ok(()) => return Ok(()),
            Err(failure) => {
                best_failure = Some(match best_failure {
                    Some(current) => deeper_match_failure(current, failure),
                    None => failure,
                });
            }
        }
    }

    Err(best_failure.unwrap_or_else(|| MatchFailure {
        path: base_path.map(str::to_string),
        reason: AssertionMismatchReason::ArrayMemberNotFound,
        actual: AssertionMismatchValue {
            value_type: "array".to_string(),
            value: Value::Array(actual_items.to_vec()),
        },
        expected: AssertionMismatchValue {
            value_type: json_value_type_name(expected).to_string(),
            value: expected.clone(),
        },
    }))
}

fn match_array_subset(
    actual_items: &[Value],
    expected_items: &[Value],
    base_path: Option<&str>,
) -> Result<(), MatchFailure> {
    let mut used = vec![false; actual_items.len()];

    for expected in expected_items {
        let mut found = None;
        let mut best_failure: Option<MatchFailure> = None;

        for (index, item) in actual_items.iter().enumerate() {
            if used[index] {
                continue;
            }

            let item_path = join_array_path(base_path, index);
            match match_structural_value(&AssertionValue::Json(item.clone()), expected, Some(item_path.as_str())) {
                Ok(()) => {
                    found = Some(index);
                    break;
                }
                Err(failure) => {
                    best_failure = Some(match best_failure {
                        Some(current) => deeper_match_failure(current, failure),
                        None => failure,
                    });
                }
            }
        }

        if let Some(index) = found {
            used[index] = true;
            continue;
        }

        return Err(best_failure.unwrap_or_else(|| MatchFailure {
            path: base_path.map(str::to_string),
            reason: AssertionMismatchReason::ArrayElementNotFound,
            actual: AssertionMismatchValue {
                value_type: "array".to_string(),
                value: Value::Array(actual_items.to_vec()),
            },
            expected: AssertionMismatchValue {
                value_type: json_value_type_name(expected).to_string(),
                value: expected.clone(),
            },
        }));
    }

    Ok(())
}

fn parse_bool(input: &str) -> Option<bool> {
    match input.trim() {
        "true" => Some(true),
        "false" => Some(false),
        _ => None,
    }
}

fn evaluate_binary(
    operator: &BinaryOperator,
    left: &ResolvedValue,
    right: &ResolvedValue,
) -> bool {
    match operator {
        BinaryOperator::Eq => values_equal(&left.value, &right.value),
        BinaryOperator::Ne => !values_equal(&left.value, &right.value),
        BinaryOperator::Lt => compare_values(&left.value, &right.value)
            .map(|ordering| ordering.is_lt())
            .unwrap_or(false),
        BinaryOperator::Lte => compare_values(&left.value, &right.value)
            .map(|ordering| !ordering.is_gt())
            .unwrap_or(false),
        BinaryOperator::Gt => compare_values(&left.value, &right.value)
            .map(|ordering| ordering.is_gt())
            .unwrap_or(false),
        BinaryOperator::Gte => compare_values(&left.value, &right.value)
            .map(|ordering| !ordering.is_lt())
            .unwrap_or(false),
    }
}

fn format_binary_failure(
    raw: &str,
    mismatch: &AssertionMismatch,
) -> String {
    let mut details = Vec::new();

    if let Some(path) = mismatch.actual_path.as_deref() {
        details.push(format!("actual path: {}", path));
    }
    if let Some(path) = mismatch.compared_path.as_deref() {
        details.push(format!("compared path: {}", path));
    }

    details.push(format!("actual type: {}", mismatch.actual.value_type));
    details.push(format!("actual value: {}", render_mismatch_value(&mismatch.actual)));
    details.push(format!("operator: {}", mismatch.operator.as_deref().unwrap_or_default()));
    details.push(format!("compared type: {}", mismatch.expected.value_type));
    details.push(format!("compared value: {}", render_mismatch_value(&mismatch.expected)));

    format!("Assertion failed: {} ({})", raw, details.join(", "))
}

fn format_match_failure_with_detail(
    raw: &str,
    left: &ResolvedValue,
    pattern: &MatchPattern,
    mismatch: &AssertionMismatch,
) -> String {
    let mut details = Vec::new();

    if let Some(path) = left.path.as_deref() {
        details.push(format!("actual path: {}", path));
    }

    details.push(format!("actual type: {}", left.type_name()));
    details.push(format!("actual value: {}", left.match_display()));
    details.push("operator: ~=".to_string());
    details.push(format!("pattern: {}", pattern.display()));

    if let Some(path) = mismatch.path.as_deref() {
        details.push(format!("mismatch path: {}", path));
    }

    details.push(format!("mismatch reason: {}", mismatch.reason.as_str()));
    details.push(format!("mismatch actual type: {}", mismatch.actual.value_type));
    details.push(format!("mismatch actual value: {}", render_mismatch_value(&mismatch.actual)));
    details.push(format!("mismatch expected value: {}", render_mismatch_value(&mismatch.expected)));

    format!("Assertion failed: {} ({})", raw, details.join(", "))
}

fn format_schema_failure(
    raw: &str,
    left: &ResolvedValue,
    target: &str,
    mismatch: &AssertionMismatch,
    detail: impl AsRef<str>,
) -> String {
    let mut details = Vec::new();

    if let Some(path) = left.path.as_deref() {
        details.push(format!("actual path: {}", path));
    }

    details.push(format!("actual type: {}", left.type_name()));
    details.push(format!("actual value: {}", left.match_display()));
    details.push("operator: ===".to_string());
    details.push(format!("schema target: {}", target));

    if let Some(path) = mismatch.path.as_deref() {
        details.push(format!("mismatch path: {}", path));
    }

    details.push(format!("mismatch reason: {}", mismatch.reason.as_str()));
    details.push(format!("mismatch actual type: {}", mismatch.actual.value_type));
    details.push(format!("mismatch actual value: {}", render_mismatch_value(&mismatch.actual)));
    details.push(format!("detail: {}", detail.as_ref()));

    format!("Assertion failed: {} ({})", raw, details.join(", "))
}

fn render_mismatch_value(value: &AssertionMismatchValue) -> String {
    match value.value_type.as_str() {
        "missing" => "<missing>".to_string(),
        "string" | "substring" | "regex" => match &value.value {
            Value::String(text) => render_string(text),
            other => other.to_string(),
        },
        _ => value.value.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use reqwest::header::{HeaderMap, HeaderValue};
    use reqwest::StatusCode;
    use crate::request::response_capture::{INTERNAL_SSE_EVENT_HEADER, INTERNAL_WS_KIND_HEADER};

    fn env(vars: &[(&str, &str)]) -> HashMap<String, String> {
        vars.iter()
            .map(|(k, v)| (k.to_string(), v.to_string()))
            .collect()
    }

    fn empty_snapshot() -> ResponseSnapshot {
        ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::new(),
            json: None,
        }
    }

    fn deps() -> HashMap<String, ResponseSnapshot> {
        HashMap::new()
    }

    #[test]
    fn equality_assertion_passes() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ $FOO == 'bar'", &context).unwrap();
        let map = env(&[("FOO", "bar")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn graphql_errors_alias_can_be_used_in_assertions() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & graphql.errors == null", &context).unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"data":{"user":{"id":"123"}},"errors":null}"#),
            json: Some(json!({
                "data": { "user": { "id": "123" } },
                "errors": null
            })),
        };

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps()).is_ok());
    }

    #[test]
    fn graphql_data_alias_reports_graphql_path_on_failure() {
        let context = HashMap::new();
        let assertion =
            Assertion::parse("^ & graphql.data.user.id == '456'", &context).unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"data":{"user":{"id":"123"}},"errors":null}"#),
            json: Some(json!({
                "data": { "user": { "id": "123" } },
                "errors": null
            })),
        };

        let err = assertion
            .evaluate(&HashMap::new(), &snapshot, &deps())
            .expect_err("assertion should fail");

        let mismatch = err.1.expect("mismatch should be present");
        assert_eq!(mismatch.path.as_deref(), Some("graphql.data.user.id"));
        assert_eq!(mismatch.actual_path.as_deref(), Some("graphql.data.user.id"));
    }

    #[test]
    fn sse_event_alias_can_be_used_in_assertions() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & sse.event == 'price'", &context).unwrap();
        let mut headers = HeaderMap::new();
        headers.insert(
            INTERNAL_SSE_EVENT_HEADER,
            HeaderValue::from_static("price"),
        );
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers,
            body: String::from(r#"{"symbol":"AAPL"}"#),
            json: Some(json!({ "symbol": "AAPL" })),
        };

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps()).is_ok());
    }

    #[test]
    fn ws_kind_alias_can_be_used_in_assertions() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & ws.kind == 'text'", &context).unwrap();
        let mut headers = HeaderMap::new();
        headers.insert(INTERNAL_WS_KIND_HEADER, HeaderValue::from_static("text"));
        let snapshot = ResponseSnapshot {
            status: StatusCode::SWITCHING_PROTOCOLS,
            headers,
            body: String::from(r#"{"type":"ack"}"#),
            json: Some(json!({ "type": "ack" })),
        };

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps()).is_ok());
    }

    #[test]
    fn numeric_greater_than_passes() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ $COUNT > 10", &context).unwrap();
        let map = env(&[("COUNT", "12")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn schema_assertion_parses_with_known_target() {
        let context = HashMap::new();
        let schema_registry = SchemaRegistry::default();
        let assertion = Assertion::parse_with_registry("^ & body === UUID", &context, &schema_registry)
            .unwrap();

        assert!(matches!(
            assertion.comparison,
            Comparison::Schema { ref target } if target == "UUID"
        ));
    }

    #[test]
    fn schema_assertion_rejects_unknown_target() {
        let context = HashMap::new();
        let schema_registry = SchemaRegistry::default();
        let error = Assertion::parse_with_registry("^ & body === User", &context, &schema_registry)
            .unwrap_err();

        assert!(error.0.contains("Unknown schema validation target 'User'"));
    }

    #[test]
    fn guard_rejects_schema_operator() {
        let context = HashMap::new();
        let error = Assertion::parse("[status === UUID] ^ $FOO == 'bar'", &context).unwrap_err();

        assert!(error
            .0
            .contains("schema validation operator is not supported in guards"));
    }

    #[test]
    fn schema_assertion_validates_builtin_scalar_targets() {
        let context = HashMap::new();
        let schema_registry = SchemaRegistry::default();
        let assertion = Assertion::parse_with_registry("^ & body.id === UUID", &context, &schema_registry)
            .unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"id":"550e8400-e29b-41d4-a716-446655440000"}"#),
            json: Some(json!({ "id": "550e8400-e29b-41d4-a716-446655440000" })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps).is_ok());

        let numeric_assertion = Assertion::parse_with_registry(
            "^ & body.amount === NUMBER",
            &context,
            &schema_registry,
        )
        .unwrap();
        let numeric_snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"amount":-3.2}"#),
            json: Some(json!({ "amount": -3.2 })),
        };

        assert!(numeric_assertion
            .evaluate(&HashMap::new(), &numeric_snapshot, &deps)
            .is_ok());
    }

    #[test]
    fn schema_assertion_validates_object_targets() {
        let context = HashMap::new();
        let mut schema_registry = SchemaRegistry::default();
        schema_registry
            .register_schema(crate::schema::SchemaDefinition::new(
                "User",
                crate::schema::SchemaShape::Object(crate::schema::ObjectSchema::open(vec![
                    crate::schema::SchemaField::required("id", crate::schema::TypeReference::named("UUID")),
                    crate::schema::SchemaField::optional(
                        "birthday",
                        crate::schema::TypeReference::named("DATE").nullable(),
                    ),
                ])),
            ))
            .unwrap();
        schema_registry.validate_references().unwrap();

        let assertion = Assertion::parse_with_registry("^ & body === User", &context, &schema_registry)
            .unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"id":"550e8400-e29b-41d4-a716-446655440000","birthday":null,"extra":true}"#,
            ),
            json: Some(json!({
                "id": "550e8400-e29b-41d4-a716-446655440000",
                "birthday": null,
                "extra": true
            })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps).is_ok());
    }

    #[test]
    fn schema_assertion_validates_root_array_targets() {
        let context = HashMap::new();
        let mut schema_registry = SchemaRegistry::default();
        schema_registry
            .register_schema(crate::schema::SchemaDefinition::new(
                "User",
                crate::schema::SchemaShape::Object(crate::schema::ObjectSchema::open(vec![
                    crate::schema::SchemaField::required("id", crate::schema::TypeReference::named("UUID")),
                ])),
            ))
            .unwrap();
        schema_registry
            .register_schema(crate::schema::SchemaDefinition::new(
                "Users",
                crate::schema::SchemaShape::Array(crate::schema::TypeReference::array(
                    crate::schema::TypeReference::named("User"),
                )),
            ))
            .unwrap();
        schema_registry.validate_references().unwrap();

        let assertion = Assertion::parse_with_registry("^ & body === Users", &context, &schema_registry)
            .unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"[{"id":"550e8400-e29b-41d4-a716-446655440000"},{"id":"123e4567-e89b-12d3-a456-426614174000"}]"#,
            ),
            json: Some(json!([
                { "id": "550e8400-e29b-41d4-a716-446655440000" },
                { "id": "123e4567-e89b-12d3-a456-426614174000" }
            ])),
        };
        let deps = deps();

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps).is_ok());
    }

    #[test]
    fn schema_assertion_validates_dependency_response_schemas() {
        let context = HashMap::new();
        let mut schema_registry = SchemaRegistry::default();
        schema_registry
            .register_schema(crate::schema::SchemaDefinition::new(
                "LoginResponse",
                crate::schema::SchemaShape::Object(crate::schema::ObjectSchema::open(vec![
                    crate::schema::SchemaField::required("token", crate::schema::TypeReference::primitive(crate::schema::PrimitiveType::String)),
                    crate::schema::SchemaField::required("expiresAt", crate::schema::TypeReference::named("DATE_TIME")),
                ])),
            ))
            .unwrap();
        schema_registry.validate_references().unwrap();

        let assertion = Assertion::parse_with_registry(
            "^ &[Login].body === LoginResponse",
            &context,
            &schema_registry,
        )
        .unwrap();

        let snapshot = empty_snapshot();
        let mut deps = HashMap::new();
        deps.insert(
            "Login".to_string(),
            ResponseSnapshot {
                status: StatusCode::OK,
                headers: HeaderMap::new(),
                body: String::from(
                    r#"{"token":"secret","expiresAt":"2026-05-02T12:00:00Z"}"#,
                ),
                json: Some(json!({
                    "token": "secret",
                    "expiresAt": "2026-05-02T12:00:00Z"
                })),
            },
        );

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps).is_ok());
    }

    #[test]
    fn schema_assertion_rejects_untyped_left_hand_values() {
        let context = HashMap::new();
        let schema_registry = SchemaRegistry::default();
        let assertion = Assertion::parse_with_registry("^ $USER_ID === UUID", &context, &schema_registry)
            .unwrap();
        let snapshot = empty_snapshot();
        let deps = deps();

        let error = assertion
            .evaluate(&env(&[("USER_ID", "550e8400-e29b-41d4-a716-446655440000")]), &snapshot, &deps)
            .unwrap_err();

        assert!(error.0.contains("schema validation requires a typed JSON value on the left-hand side"));
        let mismatch = error.1.expect("expected mismatch details");
        assert_eq!(mismatch.kind, AssertionMismatchKind::Schema);
        assert_eq!(mismatch.reason, AssertionMismatchReason::InvalidActualType);
        assert_eq!(mismatch.target.as_deref(), Some("UUID"));
        assert_eq!(mismatch.operator.as_deref(), Some("==="));
    }

    #[test]
    fn schema_assertion_rejects_invalid_rhs_usage() {
        let context = HashMap::new();
        let schema_registry = SchemaRegistry::default();
        let error = Assertion::parse_with_registry("^ & body === 'UUID'", &context, &schema_registry)
            .unwrap_err();

        assert!(error
            .0
            .contains("schema validation target must be a declared scalar or schema name"));
    }

    #[test]
    fn schema_assertion_reports_nested_schema_failure_path() {
        let context = HashMap::new();
        let mut schema_registry = SchemaRegistry::default();
        schema_registry
            .register_schema(crate::schema::SchemaDefinition::new(
                "Address",
                crate::schema::SchemaShape::Object(crate::schema::ObjectSchema::open(vec![
                    crate::schema::SchemaField::required("postalCode", crate::schema::TypeReference::primitive(crate::schema::PrimitiveType::String)),
                ])),
            ))
            .unwrap();
        schema_registry
            .register_schema(crate::schema::SchemaDefinition::new(
                "User",
                crate::schema::SchemaShape::Object(crate::schema::ObjectSchema::open(vec![
                    crate::schema::SchemaField::required("id", crate::schema::TypeReference::named("UUID")),
                    crate::schema::SchemaField::required("address", crate::schema::TypeReference::named("Address")),
                ])),
            ))
            .unwrap();
        schema_registry.validate_references().unwrap();

        let assertion = Assertion::parse_with_registry("^ & body === User", &context, &schema_registry)
            .unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"id":"550e8400-e29b-41d4-a716-446655440000","address":{}}"#),
            json: Some(json!({
                "id": "550e8400-e29b-41d4-a716-446655440000",
                "address": {}
            })),
        };
        let deps = deps();

        let error = assertion.evaluate(&HashMap::new(), &snapshot, &deps).unwrap_err();

        assert!(error.0.contains("mismatch path: body.address.postalCode"));
        let mismatch = error.1.expect("expected mismatch details");
        assert_eq!(mismatch.kind, AssertionMismatchKind::Schema);
        assert_eq!(mismatch.target.as_deref(), Some("User"));
        assert_eq!(mismatch.path.as_deref(), Some("body.address.postalCode"));
        assert_eq!(mismatch.reason, AssertionMismatchReason::MissingRequiredField);
        assert_eq!(mismatch.expected.value_type, "required_field");
        assert_eq!(mismatch.expected.value, Value::String("postalCode".to_string()));
    }

    #[test]
    fn regex_matches() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ $VALUE ~= /foo.+/", &context).unwrap();
        let map = env(&[("VALUE", "foobarbaz")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn contains_matches() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ $VALUE ~= 'bar'", &context).unwrap();
        let map = env(&[("VALUE", "foobarbaz")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn assertion_failure_uses_default_message() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ $VALUE == 'expected'", &context).unwrap();
        let map = env(&[("VALUE", "actual")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        let result = assertion.evaluate(&map, &snapshot, &deps);
        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("Assertion failed"));
        assert!(message.contains("actual type: string"));
        assert!(message.contains("compared type: string"));
    }

    #[test]
    fn assertion_failure_with_custom_message() {
        let context = HashMap::new();
        let assertion =
            Assertion::parse("^ $VALUE == 'expected' || 'custom failure'", &context).unwrap();
        let map = env(&[("VALUE", "actual")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        let result = assertion.evaluate(&map, &snapshot, &deps);
        assert!(result.is_err());
        assert_eq!(result.err().unwrap().0, "custom failure");
    }

    #[test]
    fn capture_body_matches_regex() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body ~= /foo/", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: "foobarbaz".into(),
            json: None,
        };
        let deps = deps();
        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn capture_dependency_body_token() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ &[Login].body.token == 'secret'", &context).unwrap();
        let map = HashMap::new();
        let snapshot = empty_snapshot();
        let mut deps = HashMap::new();
        let dep_snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: "{\"token\":\"secret\"}".into(),
            json: serde_json::from_str("{\"token\":\"secret\"}").ok(),
        };
        deps.insert("Login".to_string(), dep_snapshot);
        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn capture_body_boolean_equals_boolean_literal() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.boolean == true", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: "{\"boolean\":true}".into(),
            json: serde_json::from_str("{\"boolean\":true}").ok(),
        };
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn decoded_json_capture_operand_can_be_used_in_assertion() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & json(body.result.content[0].text).items[0].id == 'abc123'",
            &context,
        )
        .unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"result":{"content":[{"text":"{\"items\":[{\"id\":\"abc123\",\"active\":true}]}"}]}}"#,
            ),
            json: Some(json!({
                "result": {
                    "content": [
                        {
                            "text": "{\"items\":[{\"id\":\"abc123\",\"active\":true}]}"
                        }
                    ]
                }
            })),
        };

        assert!(assertion.evaluate(&map, &snapshot, &deps()).is_ok());
    }

    #[test]
    fn decoded_json_capture_operand_supports_schema_validation() {
        let context = HashMap::new();
        let mut schema_registry = SchemaRegistry::default();
        schema_registry
            .register_schema(crate::schema::SchemaDefinition::new(
                "DecodedItem",
                crate::schema::SchemaShape::Object(crate::schema::ObjectSchema::open(vec![
                    crate::schema::SchemaField::required(
                        "id",
                        crate::schema::TypeReference::primitive(crate::schema::PrimitiveType::String),
                    ),
                    crate::schema::SchemaField::required(
                        "active",
                        crate::schema::TypeReference::primitive(crate::schema::PrimitiveType::Boolean),
                    ),
                ])),
            ))
            .unwrap();
        schema_registry.validate_references().unwrap();

        let assertion = Assertion::parse_with_registry(
            "^ & json(body.result.content[0].text).items[0] === DecodedItem",
            &context,
            &schema_registry,
        )
        .unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"result":{"content":[{"text":"{\"items\":[{\"id\":\"abc123\",\"active\":true}]}"}]}}"#,
            ),
            json: Some(json!({
                "result": {
                    "content": [
                        {
                            "text": "{\"items\":[{\"id\":\"abc123\",\"active\":true}]}"
                        }
                    ]
                }
            })),
        };

        assert!(assertion.evaluate(&HashMap::new(), &snapshot, &deps()).is_ok());
    }

    #[test]
    fn decoded_json_capture_operand_reports_invalid_json() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & json(body.result.content[0].text).items[0].id == 'abc123'",
            &context,
        )
        .unwrap();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"result":{"content":[{"text":"not json"}]}}"#),
            json: Some(json!({
                "result": {
                    "content": [
                        {
                            "text": "not json"
                        }
                    ]
                }
            })),
        };

        let err = assertion
            .evaluate(&HashMap::new(), &snapshot, &deps())
            .unwrap_err();

        assert!(err.0.contains("Failed to decode JSON from 'body.result.content[0].text'"));
    }

    #[test]
    fn string_variable_can_match_boolean_literal() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ $FEATURE_ENABLED == true", &context).unwrap();
        let map = env(&[("FEATURE_ENABLED", "true")]);
        let snapshot = empty_snapshot();
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn json_string_does_not_equal_number_literal() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.count == 1", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"count":"1"}"#),
            json: Some(json!({ "count": "1" })),
        };
        let deps = deps();

        let result = assertion.evaluate(&map, &snapshot, &deps);

        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("actual path: body.count"));
        assert!(message.contains("actual type: string"));
        assert!(message.contains("actual value: \"1\""));
        assert!(message.contains("compared type: number"));
        assert!(message.contains("compared value: 1.0") || message.contains("compared value: 1"));
    }

    #[test]
    fn null_literal_matches_json_null() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.value == null", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"value":null}"#),
            json: Some(json!({ "value": null })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn missing_path_is_reported_distinctly_from_null() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.value == null", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from("{}"),
            json: Some(json!({})),
        };
        let deps = deps();

        let result = assertion.evaluate(&map, &snapshot, &deps);

        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("actual path: body.value"));
        assert!(message.contains("actual type: missing"));
        assert!(message.contains("actual value: <missing>"));
        assert!(message.contains("compared type: null"));
    }

    #[test]
    fn structured_values_compare_without_stringifying() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.user == &[Login].body.user", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"user":{"id":7,"role":"admin"}}"#),
            json: Some(json!({ "user": { "id": 7, "role": "admin" } })),
        };
        let mut deps = HashMap::new();
        deps.insert(
            "Login".to_string(),
            ResponseSnapshot {
                status: StatusCode::OK,
                headers: HeaderMap::new(),
                body: String::from(r#"{"user":{"id":7,"role":"admin"}}"#),
                json: Some(json!({ "user": { "id": 7, "role": "admin" } })),
            },
        );

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn scalar_and_structured_values_do_not_compare_equal() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.user == 7", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"user":{"id":7}}"#),
            json: Some(json!({ "user": { "id": 7 } })),
        };
        let deps = deps();

        let result = assertion.evaluate(&map, &snapshot, &deps);

        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("actual path: body.user"));
        assert!(message.contains("actual type: object"));
        assert!(message.contains("compared type: number"));
    }

    #[test]
    fn failure_message_includes_compared_path_for_capture_operands() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.user.id == &[Login].body.user.id", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"user":{"id":7}}"#),
            json: Some(json!({ "user": { "id": 7 } })),
        };
        let mut deps = HashMap::new();
        deps.insert(
            "Login".to_string(),
            ResponseSnapshot {
                status: StatusCode::OK,
                headers: HeaderMap::new(),
                body: String::from(r#"{"user":{"id":8}}"#),
                json: Some(json!({ "user": { "id": 8 } })),
            },
        );

        let result = assertion.evaluate(&map, &snapshot, &deps);

        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("actual path: body.user.id"));
        assert!(message.contains("compared path: [Login].body.user.id"));
    }

    #[test]
    fn structural_match_allows_partial_object_subset() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.user ~= {\"id\":7}", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"user":{"id":7,"role":"admin","active":true}}"#),
            json: Some(json!({ "user": { "id": 7, "role": "admin", "active": true } })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn structural_match_allows_unordered_array_membership() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.items ~= {\"id\":2}", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"items":[{"id":1},{"id":2},{"id":3}]}"#),
            json: Some(json!({ "items": [{ "id": 1 }, { "id": 2 }, { "id": 3 }] })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn structural_match_allows_unordered_array_subset() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & body.items ~= [{\"id\":3},{\"id\":1}]",
            &context,
        )
        .unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"items":[{"id":1},{"id":2},{"id":3}]}"#),
            json: Some(json!({ "items": [{ "id": 1 }, { "id": 2 }, { "id": 3 }] })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn structural_match_reports_nested_mismatch_path() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & body.user ~= {\"profile\":{\"name\":\"Alice\"}}",
            &context,
        )
        .unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"user":{"profile":{"name":"Bob"}}}"#),
            json: Some(json!({ "user": { "profile": { "name": "Bob" } } })),
        };
        let deps = deps();

        let result = assertion.evaluate(&map, &snapshot, &deps);

        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("mismatch path: body.user.profile.name"));
    }

    #[test]
    fn structural_match_distinguishes_null_from_missing() {
        let context = HashMap::new();
        let assertion = Assertion::parse("^ & body.user ~= {\"middleName\":null}", &context).unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"user":{"firstName":"Alice"}}"#),
            json: Some(json!({ "user": { "firstName": "Alice" } })),
        };
        let deps = deps();

        let result = assertion.evaluate(&map, &snapshot, &deps);

        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("mismatch path: body.user.middleName"));
        assert!(message.contains("mismatch actual type: missing"));
        assert!(message.contains("mismatch expected value: null"));
    }

    #[test]
    fn structural_match_reuses_same_logic_for_dependency_captures() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ &[Login].body.user ~= {\"profile\":{\"role\":\"admin\"}}",
            &context,
        )
        .unwrap();
        let map = HashMap::new();
        let snapshot = empty_snapshot();
        let mut deps = HashMap::new();
        deps.insert(
            "Login".to_string(),
            ResponseSnapshot {
                status: StatusCode::OK,
                headers: HeaderMap::new(),
                body: String::from(r#"{"user":{"id":7,"profile":{"role":"admin","active":true}}}"#),
                json: Some(json!({ "user": { "id": 7, "profile": { "role": "admin", "active": true } } })),
            },
        );

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn filtered_body_capture_operand_can_be_used_in_assertion() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & body.jobs[? recipient.email == \"alice@example.com\" && active == true].status == 'succeeded'",
            &context,
        )
        .unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"jobs":[{"recipient":{"email":"bob@example.com"},"active":true,"status":"queued"},{"recipient":{"email":"alice@example.com"},"active":true,"status":"succeeded"}]}"#,
            ),
            json: Some(json!({
                "jobs": [
                    {
                        "recipient": { "email": "bob@example.com" },
                        "active": true,
                        "status": "queued"
                    },
                    {
                        "recipient": { "email": "alice@example.com" },
                        "active": true,
                        "status": "succeeded"
                    }
                ]
            })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn filtered_body_capture_operand_can_use_variable_values() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & body.jobs[? recipient.email == $RECIPIENT_EMAIL && active == $EXPECT_ACTIVE].status == 'succeeded'",
            &context,
        )
        .unwrap();
        let map = env(&[("RECIPIENT_EMAIL", "alice@example.com"), ("EXPECT_ACTIVE", "true")]);
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"jobs":[{"recipient":{"email":"bob@example.com"},"active":true,"status":"queued"},{"recipient":{"email":"alice@example.com"},"active":true,"status":"succeeded"}]}"#,
            ),
            json: Some(json!({
                "jobs": [
                    {
                        "recipient": { "email": "bob@example.com" },
                        "active": true,
                        "status": "queued"
                    },
                    {
                        "recipient": { "email": "alice@example.com" },
                        "active": true,
                        "status": "succeeded"
                    }
                ]
            })),
        };
        let deps = deps();

        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn filtered_body_capture_operand_reports_concrete_matched_path_on_failure() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & body.jobs[? recipient == \"alice@example.com\"].status == 'queued'",
            &context,
        )
        .unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"jobs":[{"recipient":"bob@example.com","status":"queued"},{"recipient":"alice@example.com","status":"succeeded"}]}"#,
            ),
            json: Some(json!({
                "jobs": [
                    { "recipient": "bob@example.com", "status": "queued" },
                    { "recipient": "alice@example.com", "status": "succeeded" }
                ]
            })),
        };
        let deps = deps();

        let err = assertion.evaluate(&map, &snapshot, &deps).unwrap_err();
        let mismatch = err.1.expect("mismatch details should be attached");

        assert!(err.0.contains("actual path: body.jobs[1].status"));
        assert_eq!(mismatch.actual_path.as_deref(), Some("body.jobs[1].status"));
        assert_eq!(mismatch.path.as_deref(), Some("body.jobs[1].status"));
    }

    #[test]
    fn filtered_body_capture_operand_requires_exactly_one_match() {
        let context = HashMap::new();
        let assertion = Assertion::parse(
            "^ & body.jobs[? recipient == \"alice@example.com\"].status == 'succeeded'",
            &context,
        )
        .unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"jobs":[{"recipient":"alice@example.com","status":"queued"},{"recipient":"alice@example.com","status":"succeeded"}]}"#,
            ),
            json: Some(json!({
                "jobs": [
                    { "recipient": "alice@example.com", "status": "queued" },
                    { "recipient": "alice@example.com", "status": "succeeded" }
                ]
            })),
        };
        let deps = deps();

        let result = assertion.evaluate(&map, &snapshot, &deps);

        assert!(result.is_err());
        let message = result.err().unwrap().0;
        assert!(message.contains("body.jobs[? recipient == \"alice@example.com\"].status"));
        assert!(message.contains("matched 2 elements"));
        assert!(message.contains("expected exactly one"));
    }

    #[test]
    fn guarded_assertion_parses_guard() {
        let context = HashMap::new();
        let assertion = Assertion::parse("[ROLE == admin] ^ $STATUS == 200", &context).unwrap();
        assert!(assertion.guard.is_some());
    }

    #[test]
    fn guarded_assertion_skips_when_false() {
        let context = HashMap::new();
        let assertion = Assertion::parse("[ROLE == admin] ^ $STATUS == 200", &context).unwrap();
        let map = env(&[("ROLE", "user"), ("STATUS", "200")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        let should_execute = assertion
            .should_execute(&map, &snapshot, &deps)
            .expect("guard evaluation should succeed");
        assert!(!should_execute);
    }

    #[test]
    fn guarded_assertion_executes_when_true() {
        let context = HashMap::new();
        let assertion = Assertion::parse("[ROLE == admin] ^ $STATUS == 200", &context).unwrap();
        let map = env(&[("ROLE", "admin"), ("STATUS", "200")]);
        let snapshot = empty_snapshot();
        let deps = deps();
        let should_execute = assertion
            .should_execute(&map, &snapshot, &deps)
            .expect("guard evaluation should succeed");
        assert!(should_execute);
        assert!(assertion.evaluate(&map, &snapshot, &deps).is_ok());
    }

    #[test]
    fn guarded_assertion_treats_true_as_boolean_literal() {
        let context = HashMap::new();
        let assertion = Assertion::parse("[FEATURE_ENABLED == true] ^ $STATUS == 200", &context)
            .unwrap();
        let map = env(&[("FEATURE_ENABLED", "true"), ("STATUS", "200")]);
        let snapshot = empty_snapshot();
        let deps = deps();

        let should_execute = assertion
            .should_execute(&map, &snapshot, &deps)
            .expect("guard evaluation should succeed");

        assert!(should_execute);
    }

    #[test]
    fn guarded_assertion_treats_null_as_null_literal() {
        let context = HashMap::new();
        let assertion = Assertion::parse("[& body.optional == null] ^ & status == 200", &context)
            .unwrap();
        let map = HashMap::new();
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"optional":null}"#),
            json: Some(json!({ "optional": null })),
        };
        let deps = deps();

        let should_execute = assertion
            .should_execute(&map, &snapshot, &deps)
            .expect("guard evaluation should succeed");

        assert!(should_execute);
    }
}
