use std::{cmp::Ordering, collections::HashMap, error::Error, fmt, iter::Peekable, str::Chars};

use reqwest::{header::HeaderMap, StatusCode};
use serde_json::Value;

use crate::parser::context;

use super::ExecutionArtifact;

pub(crate) const INTERNAL_SSE_EVENT_HEADER: &str = "x-hen-sse-event";
pub(crate) const INTERNAL_SSE_ID_HEADER: &str = "x-hen-sse-id";
pub(crate) const INTERNAL_WS_KIND_HEADER: &str = "x-hen-ws-kind";

#[derive(Debug, Clone)]
pub struct ResponseCapture {
    pub source: CaptureSource,
    pub target: CaptureTarget,
    pub variable: String,
    pub default: Option<String>,
    pub raw_path: String,
}

#[derive(Debug, Clone)]
pub enum CaptureSource {
    Current,
    Dependency(String),
}

#[derive(Debug, Clone)]
pub enum CaptureTarget {
    Body(Vec<BodyAccessor>),
    Json(DecodedJsonTarget),
    Header(HeaderAccessor),
    Status(StatusAccessor),
    Sse(SseAccessor),
    Ws(WsAccessor),
}

#[derive(Debug, Clone)]
pub struct DecodedJsonTarget {
    pub source: Box<CaptureTarget>,
    pub source_raw_path: String,
    pub path: Vec<BodyAccessor>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum BodyAccessor {
    Key(String),
    Index(usize),
    Filter(FilterPredicate),
}

#[derive(Debug, Clone, PartialEq)]
pub struct FilterPredicate {
    pub clauses: Vec<FilterClause>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct FilterClause {
    pub path: Vec<FilterPathAccessor>,
    pub operator: FilterOperator,
    pub expected: FilterExpectedValue,
}

#[derive(Debug, Clone, PartialEq)]
pub enum FilterExpectedValue {
    Literal(Value),
    Variable(String),
}

#[derive(Debug, Clone, PartialEq)]
pub enum FilterPathAccessor {
    Key(String),
    Index(usize),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterOperator {
    Eq,
    Ne,
}

#[derive(Debug, Clone)]
pub struct HeaderAccessor {
    pub name: String,
    pub case_sensitive: bool,
}

#[derive(Debug, Clone)]
pub enum StatusAccessor {
    Code,
    Text,
    Family,
}

#[derive(Debug, Clone, Copy)]
pub enum SseAccessor {
    Event,
    Id,
}

#[derive(Debug, Clone, Copy)]
pub enum WsAccessor {
    Kind,
}

#[derive(Debug)]
pub struct CaptureError(pub String);

#[derive(Debug, Clone, PartialEq)]
pub(crate) enum CaptureValue {
    Missing,
    Json(Value),
    RawText(String),
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ResolvedAssertionCaptureValue {
    pub value: CaptureValue,
    pub resolved_path: Option<String>,
}

#[derive(Debug, Clone, PartialEq)]
struct ResolvedJsonNode<'a> {
    value: &'a Value,
    resolved_path: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum BodyResolutionError {
    Missing { resolved_path: Option<String> },
    AmbiguousFilter { predicate: String, matches: usize },
    UnknownFilterVariable(String),
}

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

impl Error for CaptureError {}

impl ResponseCapture {
    pub fn parse(raw: &str, context: &HashMap<String, String>) -> Result<Self, CaptureError> {
        let trimmed = raw.trim();
        if !trimmed.starts_with('&') {
            return Err(CaptureError("capture must begin with '&'".into()));
        }

        let remainder = trimmed.trim_start_matches('&').trim_start();
        let (source, remainder) = parse_capture_source(remainder)?;
        let (path_part_raw, remainder_raw) = remainder
            .split_once("->")
            .ok_or_else(|| CaptureError("missing capture assignment '->'".into()))?;
        let path_part = path_part_raw.trim();
        if path_part.is_empty() {
            return Err(CaptureError("missing capture path".into()));
        }
        let remainder = remainder_raw.trim();

        let (variable_part, default_part) = match remainder.split_once(":=") {
            Some((lhs, rhs)) => (lhs.trim(), Some(rhs.trim())),
            None => (remainder, None),
        };

        let variable_part = variable_part.trim();
        if !variable_part.starts_with('$') {
            return Err(CaptureError(
                "capture assignment must target a $variable".into(),
            ));
        }

        let variable = variable_part.trim_start_matches('$').trim().to_string();
        if variable.is_empty() {
            return Err(CaptureError("capture variable name cannot be empty".into()));
        }

        let default = default_part.map(|d| context::inject_from_variable(d, context));

        let target = CaptureTarget::parse(path_part)?;

        Ok(ResponseCapture {
            source,
            target,
            variable,
            default,
            raw_path: path_part.to_string(),
        })
    }

    pub fn extract_from_snapshot(
        &self,
        snapshot: &ResponseSnapshot,
        env: &HashMap<String, String>,
    ) -> Result<Option<(String, String)>, CaptureError> {
        let value = match resolve_capture_value(&self.target, &self.raw_path, snapshot, env) {
            Ok(val) => Some(val),
            Err(err) => match &self.default {
                Some(default) => Some(default.clone()),
                None => {
                    return Err(CaptureError(format!(
                        "{}",
                        err.0
                            .replace("assertion", &format!("capture into ${}", self.variable))
                    )));
                }
            },
        };

        match value {
            Some(val) => Ok(Some((self.variable.clone(), val))),
            None => Ok(None),
        }
    }

    pub fn required_dependency(&self) -> Option<&str> {
        match &self.source {
            CaptureSource::Dependency(name) => Some(name.as_str()),
            CaptureSource::Current => None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct ResponseSnapshot {
    pub status: StatusCode,
    pub headers: HeaderMap,
    pub body: String,
    pub json: Option<Value>,
}

impl Default for ResponseSnapshot {
    fn default() -> Self {
        Self {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::new(),
            json: None,
        }
    }
}

impl From<&ExecutionArtifact> for ResponseSnapshot {
    fn from(value: &ExecutionArtifact) -> Self {
        Self {
            status: value.status,
            headers: value.headers.clone(),
            body: value.body.clone(),
            json: value.json.clone(),
        }
    }
}

impl CaptureTarget {
    pub fn parse(input: &str) -> Result<Self, CaptureError> {
        let trimmed = input.trim();
        if trimmed.starts_with("json(") {
            return parse_decoded_json_target(trimmed);
        }

        let mut chars = input.chars().peekable();
        let mut buffer = String::new();

        while let Some(ch) = chars.peek() {
            if ch.is_whitespace() {
                chars.next();
            } else {
                break;
            }
        }

        while let Some(ch) = chars.peek() {
            if *ch == '.' || ch.is_whitespace() {
                break;
            }
            buffer.push(*ch);
            chars.next();
        }

        let head = buffer.trim();
        let remaining: String = chars.collect();
        let remaining = remaining.trim();

        match head {
            "body" => {
                let segments = parse_body_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Body(segments))
            }
            "graphql" => {
                let segments = parse_body_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Body(segments))
            }
            "header" => {
                let accessor = parse_header_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Header(accessor))
            }
            "status" => {
                let accessor = parse_status_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Status(accessor))
            }
            "sse" => {
                let accessor = parse_sse_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Sse(accessor))
            }
            "ws" => {
                let accessor = parse_ws_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Ws(accessor))
            }
            other => Err(CaptureError(format!(
                "unsupported capture target '{}'. expected body, graphql, header, status, sse, ws, or json(...)",
                other
            ))),
        }
    }
}

pub fn parse_capture_operand(
    raw: &str,
) -> Result<(CaptureSource, CaptureTarget, String), CaptureError> {
    let trimmed = raw.trim();
    if !trimmed.starts_with('&') {
        return Err(CaptureError("capture must begin with '&'".into()));
    }

    let remainder = trimmed.trim_start_matches('&').trim_start();
    let (source, remainder) = parse_capture_source(remainder)?;
    let path = remainder.trim();
    if path.is_empty() {
        return Err(CaptureError(
            "capture operand requires a target path".into(),
        ));
    }

    let target = CaptureTarget::parse(path)?;
    Ok((source, target, path.to_string()))
}

pub fn resolve_capture_value(
    target: &CaptureTarget,
    raw_path: &str,
    snapshot: &ResponseSnapshot,
    env: &HashMap<String, String>,
) -> Result<String, CaptureError> {
    match target {
        CaptureTarget::Body(segments) => {
            if segments.is_empty() {
                Ok(snapshot.body.clone())
            } else if let Some(json) = snapshot.json.as_ref() {
                extract_json_value(json, segments, env)
                    .map_err(|err| format_body_resolution_error(raw_path, err))
            } else {
                Err(CaptureError(format!(
                    "Failed to resolve body path '{}': response body is not valid JSON",
                    raw_path
                )))
            }
        }
        CaptureTarget::Json(target) => {
            let json = decode_json_target(target, snapshot, env)?;

            if target.path.is_empty() {
                Ok(render_json_capture_value(&json))
            } else {
                extract_json_value(&json, &target.path, env)
                    .map_err(|err| format_body_resolution_error(raw_path, err))
            }
        }
        CaptureTarget::Header(accessor) => extract_header_value(&snapshot.headers, accessor)
            .map(|value| value.to_string())
            .ok_or_else(|| {
                CaptureError(format!(
                    "Failed to resolve header '{}' in assertion",
                    accessor.name
                ))
            }),
        CaptureTarget::Status(accessor) => {
            let status_str = match accessor {
                StatusAccessor::Code => snapshot.status.as_u16().to_string(),
                StatusAccessor::Text => {
                    snapshot.status.canonical_reason().unwrap_or("").to_string()
                }
                StatusAccessor::Family => format!("{}xx", snapshot.status.as_u16() / 100),
            };
            Ok(status_str)
        }
        CaptureTarget::Sse(accessor) => extract_sse_value(&snapshot.headers, *accessor)
            .ok_or_else(|| {
                CaptureError(format!(
                    "Failed to resolve SSE field '{}' in assertion",
                    sse_accessor_name(*accessor)
                ))
            }),
        CaptureTarget::Ws(accessor) => extract_ws_value(&snapshot.headers, *accessor)
            .ok_or_else(|| {
                CaptureError(format!(
                    "Failed to resolve WebSocket field '{}' in assertion",
                    ws_accessor_name(*accessor)
                ))
            }),
    }
}

pub(crate) fn resolve_capture_value_with_path_for_assertion(
    target: &CaptureTarget,
    raw_path: &str,
    snapshot: &ResponseSnapshot,
    env: &HashMap<String, String>,
) -> Result<ResolvedAssertionCaptureValue, CaptureError> {
    match target {
        CaptureTarget::Body(segments) => {
            if segments.is_empty() {
                if let Some(json) = snapshot.json.as_ref() {
                    Ok(ResolvedAssertionCaptureValue {
                        value: CaptureValue::Json(json.clone()),
                        resolved_path: Some(raw_path.to_string()),
                    })
                } else {
                    Ok(ResolvedAssertionCaptureValue {
                        value: CaptureValue::RawText(snapshot.body.clone()),
                        resolved_path: Some(raw_path.to_string()),
                    })
                }
            } else if let Some(json) = snapshot.json.as_ref() {
                let root_path = if raw_path.trim_start().starts_with("graphql") {
                    "graphql"
                } else {
                    "body"
                };

                match extract_json_node(json, segments, env, root_path) {
                    Ok(node) => Ok(ResolvedAssertionCaptureValue {
                        value: CaptureValue::Json(node.value.clone()),
                        resolved_path: node
                            .resolved_path
                            .or_else(|| Some(raw_path.to_string())),
                    }),
                    Err(BodyResolutionError::Missing { resolved_path }) => {
                        Ok(ResolvedAssertionCaptureValue {
                            value: CaptureValue::Missing,
                            resolved_path: resolved_path.or_else(|| Some(raw_path.to_string())),
                        })
                    }
                    Err(err) => Err(format_body_resolution_error(raw_path, err)),
                }
            } else {
                Err(CaptureError(format!(
                    "Failed to resolve body path '{}': response body is not valid JSON",
                    raw_path
                )))
            }
        }
        CaptureTarget::Json(target) => {
            let json = decode_json_target(target, snapshot, env)?;

            if target.path.is_empty() {
                Ok(ResolvedAssertionCaptureValue {
                    value: CaptureValue::Json(json),
                    resolved_path: Some(raw_path.to_string()),
                })
            } else {
                let decoded_root = format!("json({})", target.source_raw_path);
                match extract_json_node(&json, &target.path, env, &decoded_root) {
                    Ok(node) => Ok(ResolvedAssertionCaptureValue {
                        value: CaptureValue::Json(node.value.clone()),
                        resolved_path: node
                            .resolved_path
                            .or_else(|| Some(raw_path.to_string())),
                    }),
                    Err(BodyResolutionError::Missing { resolved_path }) => {
                        Ok(ResolvedAssertionCaptureValue {
                            value: CaptureValue::Missing,
                            resolved_path: resolved_path.or_else(|| Some(raw_path.to_string())),
                        })
                    }
                    Err(err) => Err(format_body_resolution_error(raw_path, err)),
                }
            }
        }
        CaptureTarget::Header(accessor) => Ok(ResolvedAssertionCaptureValue {
            value: extract_header_value(&snapshot.headers, accessor)
                .map(|value| CaptureValue::Json(Value::String(value.to_string())))
                .unwrap_or(CaptureValue::Missing),
            resolved_path: Some(raw_path.to_string()),
        }),
        CaptureTarget::Status(accessor) => Ok(ResolvedAssertionCaptureValue {
            value: match accessor {
                StatusAccessor::Code => CaptureValue::Json(Value::from(snapshot.status.as_u16())),
                StatusAccessor::Text => CaptureValue::Json(Value::String(
                    snapshot.status.canonical_reason().unwrap_or("").to_string(),
                )),
                StatusAccessor::Family => CaptureValue::Json(Value::String(format!(
                    "{}xx",
                    snapshot.status.as_u16() / 100
                ))),
            },
            resolved_path: Some(raw_path.to_string()),
        }),
        CaptureTarget::Sse(accessor) => Ok(ResolvedAssertionCaptureValue {
            value: extract_sse_value(&snapshot.headers, *accessor)
                .map(|value| CaptureValue::Json(Value::String(value)))
                .unwrap_or(CaptureValue::Missing),
            resolved_path: Some(raw_path.to_string()),
        }),
        CaptureTarget::Ws(accessor) => Ok(ResolvedAssertionCaptureValue {
            value: extract_ws_value(&snapshot.headers, *accessor)
                .map(|value| CaptureValue::Json(Value::String(value)))
                .unwrap_or(CaptureValue::Missing),
            resolved_path: Some(raw_path.to_string()),
        }),
    }
}

fn parse_body_path(segments: &str) -> Result<Vec<BodyAccessor>, CaptureError> {
    if segments.is_empty() {
        return Ok(vec![]);
    }

    let raw_segments = split_path_segments(segments)?;
    let mut accessors: Vec<BodyAccessor> = Vec::new();

    for seg in raw_segments {
        if seg.is_empty() {
            continue;
        }

        let mut chars = seg.chars().peekable();
        let mut name = String::new();

        while let Some(ch) = chars.peek() {
            if *ch == '[' {
                break;
            }
            name.push(*ch);
            chars.next();
        }

        let trimmed_name = name.trim();
        if !trimmed_name.is_empty() {
            accessors.push(BodyAccessor::Key(trimmed_name.to_string()));
        }

        while let Some(ch) = chars.next() {
            if ch == '[' {
                let bracket_content = consume_bracket_content(&mut chars)?;
                let trimmed_content = bracket_content.trim();

                if let Some(predicate_source) = trimmed_content.strip_prefix('?') {
                    accessors.push(BodyAccessor::Filter(parse_filter_predicate(
                        predicate_source,
                    )?));
                    continue;
                }

                accessors.push(BodyAccessor::Index(parse_array_index(trimmed_content)?));
            } else if !ch.is_whitespace() {
                return Err(CaptureError(
                    "unexpected character in body path segment".into(),
                ));
            }
        }
    }

    Ok(accessors)
}

fn parse_header_path(segment: &str) -> Result<HeaderAccessor, CaptureError> {
    let raw_segments = split_path_segments(segment)?;
    let name_segment = raw_segments
        .first()
        .ok_or_else(|| CaptureError("header capture requires a header name".into()))?;

    let trimmed = name_segment.trim();
    if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
        let name = trimmed[1..trimmed.len() - 1].to_string();
        Ok(HeaderAccessor {
            name,
            case_sensitive: true,
        })
    } else {
        Ok(HeaderAccessor {
            name: trimmed.to_string(),
            case_sensitive: false,
        })
    }
}

fn parse_status_path(segment: &str) -> Result<StatusAccessor, CaptureError> {
    let raw_segments = split_path_segments(segment)?;
    let field = raw_segments
        .first()
        .map(|s| s.trim().to_string())
        .unwrap_or_default();

    if field.is_empty() {
        return Ok(StatusAccessor::Code);
    }

    match field.as_str() {
        "code" => Ok(StatusAccessor::Code),
        "text" => Ok(StatusAccessor::Text),
        "family" => Ok(StatusAccessor::Family),
        other => Err(CaptureError(format!(
            "unsupported status field '{}'. expected code, text, or family",
            other
        ))),
    }
}

fn parse_sse_path(segment: &str) -> Result<SseAccessor, CaptureError> {
    let raw_segments = split_path_segments(segment)?;
    let field = raw_segments
        .first()
        .map(|s| s.trim().to_string())
        .unwrap_or_default();

    match field.as_str() {
        "event" => Ok(SseAccessor::Event),
        "id" => Ok(SseAccessor::Id),
        other => Err(CaptureError(format!(
            "unsupported sse field '{}'. expected event or id",
            other
        ))),
    }
}

fn parse_ws_path(segment: &str) -> Result<WsAccessor, CaptureError> {
    let raw_segments = split_path_segments(segment)?;
    let field = raw_segments
        .first()
        .map(|s| s.trim().to_string())
        .unwrap_or_default();

    match field.as_str() {
        "kind" => Ok(WsAccessor::Kind),
        other => Err(CaptureError(format!(
            "unsupported ws field '{}'. expected kind",
            other
        ))),
    }
}

fn split_path_segments(input: &str) -> Result<Vec<String>, CaptureError> {
    if input.is_empty() {
        return Ok(vec![]);
    }

    let mut segments = Vec::new();
    let mut current = String::new();
    let mut chars = input.chars().peekable();
    let mut in_single = false;
    let mut in_double = false;
    let mut bracket_depth = 0usize;

    while let Some(ch) = chars.next() {
        match ch {
            '"' => {
                current.push(ch);
                if !in_single {
                    in_double = !in_double;
                }
            }
            '\'' => {
                current.push(ch);
                if !in_double {
                    in_single = !in_single;
                }
            }
            '[' if !in_single && !in_double => {
                bracket_depth += 1;
                current.push(ch);
            }
            ']' if !in_single && !in_double => {
                if bracket_depth == 0 {
                    return Err(CaptureError("unmatched ']' in body path".into()));
                }
                bracket_depth -= 1;
                current.push(ch);
            }
            '.' if !in_single && !in_double && bracket_depth == 0 => {
                segments.push(current.trim().to_string());
                current.clear();
            }
            _ => current.push(ch),
        }
    }

    if in_single || in_double {
        return Err(CaptureError("unterminated quoted segment".into()));
    }

    if bracket_depth != 0 {
        return Err(CaptureError("unmatched '[' in body path".into()));
    }

    if !current.trim().is_empty() {
        segments.push(current.trim().to_string());
    }

    Ok(segments)
}

fn extract_json_node<'a>(
    json: &'a Value,
    segments: &[BodyAccessor],
    env: &HashMap<String, String>,
    root_path: &str,
) -> Result<ResolvedJsonNode<'a>, BodyResolutionError> {
    let mut current = json;
    let mut current_path = root_path.to_string();
    let mut used_filter = false;

    for segment in segments {
        match segment {
            BodyAccessor::Key(key) => match current {
                Value::Object(map) => {
                    let next_path = join_body_object_path(&current_path, key);
                    current = map.get(key).ok_or_else(|| BodyResolutionError::Missing {
                        resolved_path: used_filter.then_some(next_path.clone()),
                    })?;
                    current_path = next_path;
                }
                _ => {
                    return Err(BodyResolutionError::Missing {
                        resolved_path: used_filter
                            .then_some(join_body_object_path(&current_path, key)),
                    })
                }
            },
            BodyAccessor::Index(index) => match current {
                Value::Array(list) => {
                    let next_path = join_body_array_path(&current_path, *index);
                    current = list.get(*index).ok_or_else(|| BodyResolutionError::Missing {
                        resolved_path: used_filter.then_some(next_path.clone()),
                    })?;
                    current_path = next_path;
                }
                _ => {
                    return Err(BodyResolutionError::Missing {
                        resolved_path: used_filter
                            .then_some(join_body_array_path(&current_path, *index)),
                    })
                }
            },
            BodyAccessor::Filter(predicate) => match current {
                Value::Array(list) => {
                    let mut matches = Vec::new();

                    for (index, item) in list.iter().enumerate() {
                        if predicate.matches(item, env)? {
                            matches.push((index, item));
                        }
                    }

                    match matches.as_slice() {
                        [(index, single)] => {
                            current = single;
                            current_path = join_body_array_path(&current_path, *index);
                            used_filter = true;
                        }
                        [] => {
                            return Err(BodyResolutionError::Missing {
                                resolved_path: None,
                            })
                        }
                        _ => {
                            return Err(BodyResolutionError::AmbiguousFilter {
                                predicate: predicate.to_string(),
                                matches: matches.len(),
                            })
                        }
                    }
                }
                _ => {
                    return Err(BodyResolutionError::Missing {
                        resolved_path: used_filter.then_some(current_path.clone()),
                    })
                }
            },
        }
    }

    Ok(ResolvedJsonNode {
        value: current,
        resolved_path: used_filter.then_some(current_path),
    })
}

fn join_body_object_path(base: &str, key: &str) -> String {
    if base.is_empty() {
        key.to_string()
    } else {
        format!("{}.{}", base, key)
    }
}

fn join_body_array_path(base: &str, index: usize) -> String {
    format!("{}[{}]", base, index)
}

fn extract_json_value(
    json: &Value,
    segments: &[BodyAccessor],
    env: &HashMap<String, String>,
) -> Result<String, BodyResolutionError> {
    let current = extract_json_node(json, segments, env, "body")?.value;

    match current {
        Value::Null => Err(BodyResolutionError::Missing {
            resolved_path: None,
        }),
        Value::Bool(b) => Ok(b.to_string()),
        Value::Number(num) => Ok(num.to_string()),
        Value::String(s) => Ok(s.clone()),
        _ => Ok(current.to_string()),
    }
}

impl FilterPredicate {
    fn matches(
        &self,
        candidate: &Value,
        env: &HashMap<String, String>,
    ) -> Result<bool, BodyResolutionError> {
        for clause in &self.clauses {
            if !clause.matches(candidate, env)? {
                return Ok(false);
            }
        }

        Ok(true)
    }
}

impl FilterClause {
    fn matches(
        &self,
        candidate: &Value,
        env: &HashMap<String, String>,
    ) -> Result<bool, BodyResolutionError> {
        let Some(actual) = resolve_filter_path(candidate, &self.path) else {
            return Ok(false);
        };

        let matched = filter_values_equal(actual, &self.expected, env)?;

        Ok(match self.operator {
            FilterOperator::Eq => matched,
            FilterOperator::Ne => !matched,
        })
    }
}

impl fmt::Display for FilterPredicate {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let rendered = self
            .clauses
            .iter()
            .map(|clause| clause.to_string())
            .collect::<Vec<_>>()
            .join(" && ");
        write!(f, "{}", rendered)
    }
}

impl fmt::Display for FilterClause {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} {} {}",
            render_filter_path(&self.path),
            self.operator.as_str(),
            render_filter_value(&self.expected)
        )
    }
}

impl FilterOperator {
    fn as_str(&self) -> &'static str {
        match self {
            Self::Eq => "==",
            Self::Ne => "!=",
        }
    }
}

fn parse_filter_predicate(source: &str) -> Result<FilterPredicate, CaptureError> {
    let trimmed = source.trim();
    if trimmed.is_empty() {
        return Err(CaptureError("filter predicate cannot be empty".into()));
    }

    let clauses = split_filter_clauses(trimmed)?
        .into_iter()
        .map(|clause| parse_filter_clause(clause.as_str()))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(FilterPredicate { clauses })
}

fn split_filter_clauses(input: &str) -> Result<Vec<String>, CaptureError> {
    let mut clauses = Vec::new();
    let mut current = String::new();
    let mut in_single = false;
    let mut in_double = false;
    let mut bracket_depth = 0usize;
    let chars = input.chars().collect::<Vec<_>>();
    let mut idx = 0usize;

    while idx < chars.len() {
        let ch = chars[idx];
        match ch {
            '\'' if !in_double => {
                in_single = !in_single;
                current.push(ch);
            }
            '"' if !in_single => {
                in_double = !in_double;
                current.push(ch);
            }
            '[' if !in_single && !in_double => {
                bracket_depth += 1;
                current.push(ch);
            }
            ']' if !in_single && !in_double => {
                if bracket_depth == 0 {
                    return Err(CaptureError("unmatched ']' in filter predicate".into()));
                }
                bracket_depth -= 1;
                current.push(ch);
            }
            '|' if !in_single && !in_double && idx + 1 < chars.len() && chars[idx + 1] == '|' => {
                return Err(CaptureError(
                    "filter predicates do not support '||' in v1".into(),
                ));
            }
            '(' | ')' if !in_single && !in_double => {
                return Err(CaptureError(
                    "filter predicates do not support parentheses in v1".into(),
                ));
            }
            '&'
                if !in_single
                    && !in_double
                    && bracket_depth == 0
                    && idx + 1 < chars.len()
                    && chars[idx + 1] == '&' =>
            {
                if current.trim().is_empty() {
                    return Err(CaptureError("filter predicate clause cannot be empty".into()));
                }
                clauses.push(current.trim().to_string());
                current.clear();
                idx += 1;
            }
            _ => current.push(ch),
        }
        idx += 1;
    }

    if in_single || in_double {
        return Err(CaptureError(
            "unterminated quoted string in filter predicate".into(),
        ));
    }

    if bracket_depth != 0 {
        return Err(CaptureError("unmatched '[' in filter predicate".into()));
    }

    if current.trim().is_empty() {
        return Err(CaptureError("filter predicate clause cannot be empty".into()));
    }

    clauses.push(current.trim().to_string());
    Ok(clauses)
}

fn parse_filter_clause(input: &str) -> Result<FilterClause, CaptureError> {
    let (path, operator, expected) = split_filter_clause_expression(input)?;

    Ok(FilterClause {
        path: parse_filter_path(path)?,
        operator,
        expected: parse_filter_value(expected)?,
    })
}

fn split_filter_clause_expression(
    input: &str,
) -> Result<(&str, FilterOperator, &str), CaptureError> {
    let chars = input.chars().collect::<Vec<_>>();
    let mut in_single = false;
    let mut in_double = false;
    let mut bracket_depth = 0usize;
    let mut idx = 0usize;

    while idx + 1 < chars.len() {
        let ch = chars[idx];
        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 => {
                if bracket_depth == 0 {
                    return Err(CaptureError("unmatched ']' in filter clause".into()));
                }
                bracket_depth -= 1;
            }
            _ => {}
        }

        if !in_single && !in_double && bracket_depth == 0 {
            let operator = match (chars[idx], chars[idx + 1]) {
                ('=', '=') => Some(FilterOperator::Eq),
                ('!', '=') => Some(FilterOperator::Ne),
                ('~', '=') => {
                    return Err(CaptureError(
                        "filter predicates do not support '~=' in v1".into(),
                    ))
                }
                _ => None,
            };

            if let Some(operator) = operator {
                let left = input[..idx].trim();
                let right = input[idx + 2..].trim();
                if left.is_empty() || right.is_empty() {
                    return Err(CaptureError(
                        "filter predicate must contain both a path and a value".into(),
                    ));
                }
                return Ok((left, operator, right));
            }
        }

        idx += 1;
    }

    Err(CaptureError(
        "filter predicates must use '==' or '!=' in v1".into(),
    ))
}

fn parse_filter_path(path: &str) -> Result<Vec<FilterPathAccessor>, CaptureError> {
    let raw_segments = split_path_segments(path.trim())?;
    let mut accessors = Vec::new();

    for seg in raw_segments {
        if seg.is_empty() {
            continue;
        }

        let mut chars = seg.chars().peekable();
        let mut name = String::new();

        while let Some(ch) = chars.peek() {
            if *ch == '[' {
                break;
            }
            name.push(*ch);
            chars.next();
        }

        let trimmed_name = name.trim();
        if !trimmed_name.is_empty() {
            accessors.push(FilterPathAccessor::Key(trimmed_name.to_string()));
        }

        while let Some(ch) = chars.next() {
            if ch == '[' {
                let bracket_content = consume_bracket_content(&mut chars)?;
                let trimmed_content = bracket_content.trim();
                if trimmed_content.starts_with('?') {
                    return Err(CaptureError(
                        "nested filter predicates are not supported in v1".into(),
                    ));
                }
                accessors.push(FilterPathAccessor::Index(parse_array_index(trimmed_content)?));
            } else if !ch.is_whitespace() {
                return Err(CaptureError(
                    "unexpected character in filter path".into(),
                ));
            }
        }
    }

    if accessors.is_empty() {
        return Err(CaptureError("filter path cannot be empty".into()));
    }

    Ok(accessors)
}

fn parse_filter_value(input: &str) -> Result<FilterExpectedValue, CaptureError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(CaptureError("filter value cannot be empty".into()));
    }

    if trimmed.starts_with('$') {
        let variable = trimmed.trim_start_matches('$').trim();
        if variable.is_empty() {
            return Err(CaptureError(
                "filter variable reference cannot be empty".into(),
            ));
        }

        return Ok(FilterExpectedValue::Variable(variable.to_string()));
    }

    if trimmed.starts_with('[') || trimmed.starts_with('{') {
        return Err(CaptureError(
            "structural subset matching inside filters is not supported in v1".into(),
        ));
    }

    if trimmed.starts_with('/') {
        return Err(CaptureError(
            "regex matching inside filters is not supported in v1".into(),
        ));
    }

    if (trimmed.starts_with('\'') && trimmed.ends_with('\'')) && trimmed.len() >= 2 {
        return Ok(FilterExpectedValue::Literal(Value::String(
            trimmed[1..trimmed.len() - 1].to_string(),
        )));
    }

    let value = serde_json::from_str::<Value>(trimmed).map_err(|_| {
        CaptureError(
            "filter values must be quoted strings, numbers, booleans, or null".into(),
        )
    })?;

    match value {
        Value::Array(_) | Value::Object(_) => Err(CaptureError(
            "filter values must be scalar in v1".into(),
        )),
        scalar => Ok(FilterExpectedValue::Literal(scalar)),
    }
}

fn resolve_filter_path<'a>(candidate: &'a Value, path: &[FilterPathAccessor]) -> Option<&'a Value> {
    let mut current = candidate;

    for accessor in path {
        match (accessor, current) {
            (FilterPathAccessor::Key(key), Value::Object(map)) => current = map.get(key)?,
            (FilterPathAccessor::Index(index), Value::Array(list)) => current = list.get(*index)?,
            _ => return None,
        }
    }

    Some(current)
}

fn filter_values_equal(
    actual: &Value,
    expected: &FilterExpectedValue,
    env: &HashMap<String, String>,
) -> Result<bool, BodyResolutionError> {
    match expected {
        FilterExpectedValue::Literal(expected) => Ok(match (actual, expected) {
            (Value::Number(lhs), Value::Number(rhs)) => lhs
                .as_f64()
                .zip(rhs.as_f64())
                .map(|(left, right)| left == right)
                .unwrap_or(false),
            _ => actual == expected,
        }),
        FilterExpectedValue::Variable(variable) => {
            let value = env
                .get(variable)
                .ok_or_else(|| BodyResolutionError::UnknownFilterVariable(variable.clone()))?;
            Ok(filter_text_equals_json(value, actual))
        }
    }
}

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

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

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

fn parse_array_index(input: &str) -> Result<usize, CaptureError> {
    if input.trim().is_empty() {
        return Err(CaptureError("array index cannot be empty".into()));
    }

    input
        .trim()
        .parse::<usize>()
        .map_err(|_| CaptureError("array index must be a number".into()))
}

fn consume_bracket_content(chars: &mut Peekable<Chars<'_>>) -> Result<String, CaptureError> {
    let mut content = String::new();
    let mut depth = 1usize;
    let mut in_single = false;
    let mut in_double = false;

    while let Some(ch) = chars.next() {
        match ch {
            '\'' if !in_double => {
                in_single = !in_single;
                content.push(ch);
            }
            '"' if !in_single => {
                in_double = !in_double;
                content.push(ch);
            }
            '[' if !in_single && !in_double => {
                depth += 1;
                content.push(ch);
            }
            ']' if !in_single && !in_double => {
                depth -= 1;
                if depth == 0 {
                    return Ok(content);
                }
                content.push(ch);
            }
            _ => content.push(ch),
        }
    }

    Err(CaptureError("unmatched '[' in body path".into()))
}

fn render_filter_path(path: &[FilterPathAccessor]) -> String {
    let mut rendered = String::new();

    for accessor in path {
        match accessor {
            FilterPathAccessor::Key(key) => {
                if !rendered.is_empty() {
                    rendered.push('.');
                }
                rendered.push_str(key);
            }
            FilterPathAccessor::Index(index) => {
                rendered.push('[');
                rendered.push_str(index.to_string().as_str());
                rendered.push(']');
            }
        }
    }

    rendered
}

fn render_filter_value(value: &FilterExpectedValue) -> String {
    match value {
        FilterExpectedValue::Literal(Value::String(text)) => {
            serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text))
        }
        FilterExpectedValue::Literal(value) => value.to_string(),
        FilterExpectedValue::Variable(variable) => format!("${}", variable),
    }
}

fn format_body_resolution_error(raw_path: &str, error: BodyResolutionError) -> CaptureError {
    match error {
        BodyResolutionError::Missing { .. } => CaptureError(format!(
            "Failed to resolve body path '{}': path not found",
            raw_path
        )),
        BodyResolutionError::AmbiguousFilter { predicate, matches } => CaptureError(format!(
            "Failed to resolve body path '{}': filter '{}' matched {} elements; expected exactly one",
            raw_path, predicate, matches
        )),
        BodyResolutionError::UnknownFilterVariable(variable) => CaptureError(format!(
            "Failed to resolve body path '{}': filter variable '${}' was not found",
            raw_path, variable
        )),
    }
}

fn parse_decoded_json_target(input: &str) -> Result<CaptureTarget, CaptureError> {
    let (source_raw_path, remainder) = split_decoded_json_wrapper(input)?;
    let source_raw_path = source_raw_path.trim();
    if source_raw_path.is_empty() {
        return Err(CaptureError("json() requires a source path".into()));
    }

    let remainder = remainder.trim();
    if !remainder.is_empty() && !remainder.starts_with('.') {
        return Err(CaptureError(
            "json(...) path suffix must begin with '.'".into(),
        ));
    }

    let source = CaptureTarget::parse(source_raw_path)?;
    let path = parse_body_path(remainder.trim_start_matches('.'))?;

    Ok(CaptureTarget::Json(DecodedJsonTarget {
        source: Box::new(source),
        source_raw_path: source_raw_path.to_string(),
        path,
    }))
}

fn split_decoded_json_wrapper(input: &str) -> Result<(&str, &str), CaptureError> {
    let trimmed = input.trim();
    let Some(rest) = trimmed.strip_prefix("json(") else {
        return Err(CaptureError("json() target must begin with 'json('".into()));
    };

    let mut in_single = false;
    let mut in_double = false;
    let mut bracket_depth = 0usize;
    let mut paren_depth = 1usize;

    for (offset, ch) in rest.char_indices() {
        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 => {
                if bracket_depth == 0 {
                    return Err(CaptureError("unmatched ']' in json() target".into()));
                }
                bracket_depth -= 1;
            }
            '(' if !in_single && !in_double && bracket_depth == 0 => paren_depth += 1,
            ')' if !in_single && !in_double && bracket_depth == 0 => {
                paren_depth -= 1;
                if paren_depth == 0 {
                    let inner = &rest[..offset];
                    let remainder = &rest[offset + ch.len_utf8()..];
                    return Ok((inner, remainder));
                }
            }
            _ => {}
        }
    }

    Err(CaptureError("json() target is missing closing ')'".into()))
}

fn decode_json_target(
    target: &DecodedJsonTarget,
    snapshot: &ResponseSnapshot,
    env: &HashMap<String, String>,
) -> Result<Value, CaptureError> {
    let resolved = resolve_capture_value_with_path_for_assertion(
        target.source.as_ref(),
        &target.source_raw_path,
        snapshot,
        env,
    )?;

    match resolved.value {
        CaptureValue::Missing => Err(CaptureError(format!(
            "Failed to decode JSON from '{}': source path not found",
            target.source_raw_path
        ))),
        CaptureValue::Json(Value::String(text)) => decode_json_text(&text, &target.source_raw_path),
        CaptureValue::Json(value) => Ok(value),
        CaptureValue::RawText(text) => decode_json_text(&text, &target.source_raw_path),
    }
}

fn decode_json_text(text: &str, raw_path: &str) -> Result<Value, CaptureError> {
    serde_json::from_str::<Value>(text).map_err(|err| {
        CaptureError(format!(
            "Failed to decode JSON from '{}': {}",
            raw_path, err
        ))
    })
}

fn render_json_capture_value(value: &Value) -> String {
    match value {
        Value::Null => "null".to_string(),
        Value::Bool(boolean) => boolean.to_string(),
        Value::Number(number) => number.to_string(),
        Value::String(text) => text.clone(),
        _ => value.to_string(),
    }
}

fn extract_header_value<'a>(headers: &'a HeaderMap, accessor: &HeaderAccessor) -> Option<&'a str> {
    if accessor.case_sensitive {
        if let Some(value) = headers.iter().find_map(|(name, value)| {
            if name.as_str() == accessor.name {
                value.to_str().ok()
            } else {
                None
            }
        }) {
            return Some(value);
        }
    }

    headers.iter().find_map(|(name, value)| {
        if name.as_str().eq_ignore_ascii_case(&accessor.name) {
            value.to_str().ok()
        } else {
            None
        }
    })
}

fn extract_sse_value(headers: &HeaderMap, accessor: SseAccessor) -> Option<String> {
    let name = match accessor {
        SseAccessor::Event => INTERNAL_SSE_EVENT_HEADER,
        SseAccessor::Id => INTERNAL_SSE_ID_HEADER,
    };

    headers
        .get(name)
        .and_then(|value| value.to_str().ok())
        .map(|value| value.to_string())
}

fn sse_accessor_name(accessor: SseAccessor) -> &'static str {
    match accessor {
        SseAccessor::Event => "event",
        SseAccessor::Id => "id",
    }
}

fn extract_ws_value(headers: &HeaderMap, accessor: WsAccessor) -> Option<String> {
    let name = match accessor {
        WsAccessor::Kind => INTERNAL_WS_KIND_HEADER,
    };

    headers
        .get(name)
        .and_then(|value| value.to_str().ok())
        .map(|value| value.to_string())
}

fn ws_accessor_name(accessor: WsAccessor) -> &'static str {
    match accessor {
        WsAccessor::Kind => "kind",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use reqwest::header::HeaderValue;

    #[test]
    fn parses_body_capture_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& body.token -> $TOKEN", &context).unwrap();

        match &capture.target {
            CaptureTarget::Body(segments) => {
                assert_eq!(segments.len(), 1);
            }
            _ => panic!("expected body capture"),
        }

        let status = StatusCode::OK;
        let headers = HeaderMap::new();
        let body_json = serde_json::json!({ "token": "abc123" });
        let body_text = body_json.to_string();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: Some(body_json.clone()),
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "TOKEN");
        assert_eq!(extracted.1, "abc123");
    }

    #[test]
    fn parses_graphql_capture_alias_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse(
            "& graphql.errors[0].message -> $ERROR_MESSAGE",
            &context,
        )
        .unwrap();

        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"errors":[{"message":"boom"}]}"#),
            json: Some(json!({
                "errors": [
                    { "message": "boom" }
                ]
            })),
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "ERROR_MESSAGE");
        assert_eq!(extracted.1, "boom");
    }

    #[test]
    fn parses_sse_capture_alias_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& sse.event -> $EVENT", &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" })),
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "EVENT");
        assert_eq!(extracted.1, "price");
    }

    #[test]
    fn parses_ws_capture_alias_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& ws.kind -> $KIND", &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" })),
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "KIND");
        assert_eq!(extracted.1, "text");
    }

    #[test]
    fn parses_decoded_json_capture_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse(
            "& json(body.result.content[0].text).items[0].id -> $ITEM_ID",
            &context,
        )
        .unwrap();

        match &capture.target {
            CaptureTarget::Json(target) => {
                assert_eq!(target.source_raw_path, "body.result.content[0].text");
                assert_eq!(target.path.len(), 3);
            }
            _ => panic!("expected decoded json capture"),
        }

        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"result":{"content":[{"text":"{\"items\":[{\"id\":\"abc123\"}]}"}]}}"#,
            ),
            json: Some(json!({
                "result": {
                    "content": [
                        {
                            "text": "{\"items\":[{\"id\":\"abc123\"}]}"
                        }
                    ]
                }
            })),
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "ITEM_ID");
        assert_eq!(extracted.1, "abc123");
    }

    #[test]
    fn decoded_json_capture_reports_invalid_json() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse(
            "& json(body.result.content[0].text).items[0].id -> $ITEM_ID",
            &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 = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap_err();

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

    #[test]
    fn extracts_header_case_sensitive() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& header.\"Content-Type\" -> $CT", &context).unwrap();

        let mut headers = HeaderMap::new();
        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
        let status = StatusCode::OK;
        let body_text = String::from("{}");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: body_json.clone(),
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "CT");
        assert_eq!(extracted.1, "application/json");
    }

    #[test]
    fn uses_default_when_body_path_missing() {
        let mut context = HashMap::new();
        context.insert("fallback".into(), "default".into());
        let capture =
            ResponseCapture::parse("& body.token -> $TOKEN := {{ fallback }}", &context).unwrap();

        let status = StatusCode::OK;
        let headers = HeaderMap::new();
        let body_text = String::from("{}");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: body_json.clone(),
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "TOKEN");
        assert_eq!(extracted.1, "default");
    }

    #[test]
    fn errors_when_body_path_missing_without_default() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& body.token -> $TOKEN", &context).unwrap();

        let status = StatusCode::OK;
        let headers = HeaderMap::new();
        let body_text = String::from("{}");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: body_json.clone(),
        };

        let result = capture.extract_from_snapshot(&snapshot, &HashMap::new());

        assert!(result.is_err());
    }

    #[test]
    fn status_capture_defaults_to_code() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& status -> $STATUS_CODE", &context).unwrap();

        match capture.target {
            CaptureTarget::Status(StatusAccessor::Code) => {}
            _ => panic!("expected status code accessor"),
        }

        let mut headers = HeaderMap::new();
        headers.insert("content-type", HeaderValue::from_static("text/plain"));
        let status = StatusCode::IM_A_TEAPOT;
        let body_text = String::from("short");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text,
            json: body_json,
        };

        let extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();
        assert_eq!(extracted.0, "STATUS_CODE");
        assert_eq!(extracted.1, status.as_u16().to_string());
    }

    #[test]
    fn assertion_resolution_distinguishes_null_from_missing() {
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(r#"{"present":null}"#),
            json: Some(json!({ "present": null })),
        };

        let present_target = CaptureTarget::parse("body.present").unwrap();
        let missing_target = CaptureTarget::parse("body.missing").unwrap();

        assert_eq!(
            resolve_capture_value_with_path_for_assertion(
                &present_target,
                "body.present",
                &snapshot,
                &HashMap::new(),
            )
            .unwrap()
            .value,
            CaptureValue::Json(Value::Null)
        );
        assert_eq!(
            resolve_capture_value_with_path_for_assertion(
                &missing_target,
                "body.missing",
                &snapshot,
                &HashMap::new(),
            )
            .unwrap()
            .value,
            CaptureValue::Missing
        );
    }

    #[test]
    fn assertion_resolution_preserves_json_objects_at_body_root() {
        let body_json = json!({ "service": "hen", "ok": true });
        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: body_json.to_string(),
            json: Some(body_json.clone()),
        };

        let target = CaptureTarget::parse("body").unwrap();

        assert_eq!(
            resolve_capture_value_with_path_for_assertion(
                &target,
                "body",
                &snapshot,
                &HashMap::new(),
            )
            .unwrap()
            .value,
            CaptureValue::Json(body_json)
        );
    }

    #[test]
    fn parses_filtered_body_capture_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse(
            "& body.jobs[? recipient == \"alice@example.com\"].status -> $JOB_STATUS",
            &context,
        )
        .unwrap();

        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 extracted = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap()
            .unwrap();

        assert_eq!(extracted.0, "JOB_STATUS");
        assert_eq!(extracted.1, "succeeded");
    }

    #[test]
    fn parses_filtered_body_capture_with_variable_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse(
            "& body.jobs[? recipient == $RECIPIENT].status -> $JOB_STATUS",
            &context,
        )
        .unwrap();

        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 env = HashMap::from([(String::from("RECIPIENT"), String::from("alice@example.com"))]);

        let extracted = capture.extract_from_snapshot(&snapshot, &env).unwrap().unwrap();

        assert_eq!(extracted.0, "JOB_STATUS");
        assert_eq!(extracted.1, "succeeded");
    }

    #[test]
    fn filtered_body_capture_errors_when_filter_variable_is_missing() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse(
            "& body.jobs[? recipient == $RECIPIENT].status -> $JOB_STATUS",
            &context,
        )
        .unwrap();

        let snapshot = ResponseSnapshot {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::from(
                r#"{"jobs":[{"recipient":"alice@example.com","status":"succeeded"}]}"#,
            ),
            json: Some(json!({
                "jobs": [{ "recipient": "alice@example.com", "status": "succeeded" }]
            })),
        };

        let err = capture
            .extract_from_snapshot(&snapshot, &HashMap::new())
            .unwrap_err();

        assert!(err.0.contains("filter variable '$RECIPIENT' was not found"));
    }
}

fn parse_capture_source(input: &str) -> Result<(CaptureSource, &str), CaptureError> {
    let trimmed = input.trim_start();
    if let Some(rest) = trimmed.strip_prefix('[') {
        let end = rest
            .find(']')
            .ok_or_else(|| CaptureError("unterminated dependency reference".into()))?;
        let name = rest[..end].trim();
        if name.is_empty() {
            return Err(CaptureError("dependency reference cannot be empty".into()));
        }
        let remainder = rest[end + 1..].trim_start();
        let remainder = if remainder.starts_with('.') {
            &remainder[1..]
        } else {
            remainder
        };
        Ok((CaptureSource::Dependency(name.to_string()), remainder))
    } else {
        Ok((CaptureSource::Current, trimmed))
    }
}
