//! The per-mode matching engine: one private function per [`MatchMode`]
//! lineup, dispatched from `super::validate_trace`.
//!
//! Splitting the engine out of the type definitions keeps each file tied
//! to one concern: `expected` owns the vocabulary (args, calls, modes),
//! this file owns the algorithms that line a reference up against a
//! recorded trace.

use serde_json::Value;

use super::expected::{ExpectedCall, TraceMismatch, TraceValidation};
use crate::Result;

/// One-for-one positional match: lengths must be equal and every position
/// must match.
pub(super) fn validate_exact_sequence(
    actual: &[Value],
    expected: &[ExpectedCall],
) -> Result<TraceValidation> {
    let mut mismatches = Vec::new();
    for (i, expected_call) in expected.iter().enumerate() {
        match actual.get(i) {
            None => mismatches.push(TraceMismatch {
                expected_index: i,
                actual_index: None,
                diffs: Vec::new(),
                note: format!(
                    "trace ended after {} call(s); expected a `{}` call at position {i}",
                    actual.len(),
                    expected_call.name
                ),
            }),
            Some(actual_call) => {
                if let Some(diffs) = expected_call.match_call(actual_call)? {
                    mismatches.push(TraceMismatch {
                        expected_index: i,
                        actual_index: Some(i),
                        note: format!(
                            "call at position {i} did not match `{}`",
                            expected_call.name
                        ),
                        diffs,
                    });
                }
            }
        }
    }
    // An exact sequence also fails when the trace made extra calls past
    // the expected plan, because the call plan is the contract.
    if actual.len() > expected.len() {
        mismatches.push(TraceMismatch {
            expected_index: expected.len(),
            actual_index: Some(expected.len()),
            diffs: Vec::new(),
            note: format!(
                "trace made {} extra call(s) past the {} expected (strict mode)",
                actual.len() - expected.len(),
                expected.len()
            ),
        });
    }
    Ok(finish(mismatches))
}

/// In-order containment: walk the recorded calls once, advancing the
/// expected cursor whenever the current expected call matches. Extra
/// recorded calls between matches are allowed.
pub(super) fn validate_subsequence(
    actual: &[Value],
    expected: &[ExpectedCall],
) -> Result<TraceValidation> {
    let mut expected_idx = 0usize;
    let mut last_match_at: Option<usize> = None;
    for (actual_idx, actual_call) in actual.iter().enumerate() {
        if expected_idx >= expected.len() {
            break;
        }
        // A schema problem must surface even while scanning for a match,
        // so probe the expectation and propagate the error.
        if expected[expected_idx].match_call(actual_call)?.is_none() {
            last_match_at = Some(actual_idx);
            expected_idx += 1;
        }
    }
    if expected_idx == expected.len() {
        return Ok(TraceValidation::pass());
    }
    // The first unmatched expected call is the reportable failure: every
    // call before it was found in order, this one never was.
    let search_start = last_match_at.map(|i| i + 1).unwrap_or(0);
    let unmatched = &expected[expected_idx];
    let mismatch = TraceMismatch {
        expected_index: expected_idx,
        actual_index: None,
        diffs: Vec::new(),
        note: format!(
            "expected call `{}` not found in order among recorded calls (searched from index {search_start})",
            unmatched.name
        ),
    };
    Ok(finish(vec![mismatch]))
}

/// Unordered "every reference call present" check, shared by
/// `MatchMode::Unordered` and `MatchMode::Superset` (both require all
/// reference calls and tolerate extras; they differ only in intent, not
/// in pass/fail outcome). Each reference call is matched to a distinct,
/// not-yet-consumed recorded call, in any order.
pub(super) fn validate_all_present(
    actual: &[Value],
    expected: &[ExpectedCall],
) -> Result<TraceValidation> {
    let mut consumed = vec![false; actual.len()];
    let mut mismatches = Vec::new();
    for (i, expected_call) in expected.iter().enumerate() {
        let mut matched = false;
        for (j, actual_call) in actual.iter().enumerate() {
            if consumed[j] {
                continue;
            }
            // Propagate a malformed-schema error rather than treating it
            // as a non-match.
            if expected_call.match_call(actual_call)?.is_none() {
                consumed[j] = true;
                matched = true;
                break;
            }
        }
        if !matched {
            mismatches.push(TraceMismatch {
                expected_index: i,
                actual_index: None,
                diffs: Vec::new(),
                note: format!(
                    "reference call `{}` was not found anywhere in the recorded trace",
                    expected_call.name
                ),
            });
        }
    }
    Ok(finish(mismatches))
}

/// Subset (over-calling detector): every recorded call must be allowed by
/// some not-yet-consumed reference call. A recorded call with no matching
/// reference is a wasted / over-call and fails the trace. The reference
/// calls need not all be used; the trace may legitimately call fewer.
pub(super) fn validate_subset(
    actual: &[Value],
    expected: &[ExpectedCall],
) -> Result<TraceValidation> {
    let mut consumed = vec![false; expected.len()];
    let mut mismatches = Vec::new();
    for (i, actual_call) in actual.iter().enumerate() {
        let mut allowed = false;
        for (j, expected_call) in expected.iter().enumerate() {
            if consumed[j] {
                continue;
            }
            if expected_call.match_call(actual_call)?.is_none() {
                consumed[j] = true;
                allowed = true;
                break;
            }
        }
        if !allowed {
            let actual_name = actual_call
                .get("name")
                .and_then(Value::as_str)
                .unwrap_or("<none>");
            mismatches.push(TraceMismatch {
                expected_index: expected.len(),
                actual_index: Some(i),
                diffs: Vec::new(),
                note: format!(
                    "recorded call `{actual_name}` at index {i} is an over-call: not allowed by the reference set (subset mode)"
                ),
            });
        }
    }
    Ok(finish(mismatches))
}

/// Fold a mismatch list into a [`TraceValidation`].
fn finish(mismatches: Vec<TraceMismatch>) -> TraceValidation {
    TraceValidation {
        passed: mismatches.is_empty(),
        mismatches,
    }
}
