//! Concurrent-session correctness analysis.
//!
//! mcptest is not a load tester (see `docs/what-is-mcptest.md`); this checks a
//! correctness property that generic HTTP load tools cannot: that a server stays
//! correct when several sessions run at once. Given a single-session baseline
//! result and the outcome of running the same probe in N concurrent sessions, it
//! reports three failure families:
//!
//! - liveness: a session that did not complete (hang, disconnect, or panic),
//! - determinism: a session whose result diverged from the baseline, the
//!   signature of cross-talk or shared-state corruption under concurrency,
//! - isolation: a marker value that leaked from one session into another.
//!
//! The analysis is pure and deterministic; the live harness opens the sessions
//! and feeds their outcomes in. Keeping it pure means the negative cases (a
//! server that bleeds or corrupts) are unit-tested with synthetic outcomes,
//! without needing a deliberately broken server fixture.

use serde::Serialize;

/// The outcome of running the probe in one concurrent session.
#[derive(Debug, Clone)]
pub struct SessionOutcome {
    /// Zero-based session index.
    pub index: usize,
    /// Whether the session completed its probe. `false` means it hung,
    /// disconnected, or errored before producing a result.
    pub completed: bool,
    /// Canonical probe result (for example a digest of the `tools/list`
    /// response), present only when the session completed.
    pub result: Option<String>,
    /// Indices of other sessions whose unique marker leaked into this session's
    /// traffic. Empty when no marker probe is configured or nothing leaked.
    pub foreign_markers: Vec<usize>,
}

impl SessionOutcome {
    /// A completed session with the given result and no observed leakage.
    pub fn completed(index: usize, result: impl Into<String>) -> Self {
        SessionOutcome {
            index,
            completed: true,
            result: Some(result.into()),
            foreign_markers: Vec::new(),
        }
    }

    /// A session that never produced a result.
    pub fn incomplete(index: usize) -> Self {
        SessionOutcome {
            index,
            completed: false,
            result: None,
            foreign_markers: Vec::new(),
        }
    }
}

/// One isolation breach: a foreign session's marker seen in another session.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Leak {
    /// The session whose traffic carried a foreign marker.
    pub session: usize,
    /// The session the leaked marker belongs to.
    pub from: usize,
}

/// The concurrency verdict over all sessions.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ConcurrencyReport {
    /// Number of concurrent sessions analyzed.
    pub sessions: usize,
    /// Sessions that did not complete (liveness failures), sorted.
    pub incomplete: Vec<usize>,
    /// Sessions whose result diverged from the single-session baseline, sorted.
    pub diverged: Vec<usize>,
    /// Cross-session marker leaks (isolation breaches).
    pub leaks: Vec<Leak>,
}

impl ConcurrencyReport {
    /// True when every session completed, matched the baseline, and stayed
    /// isolated.
    pub fn passed(&self) -> bool {
        self.incomplete.is_empty() && self.diverged.is_empty() && self.leaks.is_empty()
    }
}

/// Analyze concurrent-session outcomes against a single-session baseline result.
pub fn analyze(baseline: &str, outcomes: &[SessionOutcome]) -> ConcurrencyReport {
    let mut report = ConcurrencyReport {
        sessions: outcomes.len(),
        ..Default::default()
    };
    for o in outcomes {
        if !o.completed {
            report.incomplete.push(o.index);
            // An incomplete session has no result to compare; do not also flag
            // it as diverged.
            continue;
        }
        if o.result.as_deref() != Some(baseline) {
            report.diverged.push(o.index);
        }
        for &from in &o.foreign_markers {
            report.leaks.push(Leak {
                session: o.index,
                from,
            });
        }
    }
    report.incomplete.sort_unstable();
    report.diverged.sort_unstable();
    report
}

/// Render the report as a human-friendly summary.
pub fn render(report: &ConcurrencyReport) -> String {
    let mut s = format!(
        "Concurrent-session correctness: {} session(s)\n",
        report.sessions
    );
    if report.passed() {
        s.push_str("  PASS: all sessions completed, matched the baseline, and stayed isolated\n");
        return s;
    }
    if !report.incomplete.is_empty() {
        s.push_str(&format!(
            "  liveness: {} session(s) did not complete: {:?}\n",
            report.incomplete.len(),
            report.incomplete
        ));
    }
    if !report.diverged.is_empty() {
        s.push_str(&format!(
            "  determinism: {} session(s) diverged from the baseline: {:?}\n",
            report.diverged.len(),
            report.diverged
        ));
    }
    if !report.leaks.is_empty() {
        s.push_str(&format!(
            "  isolation: {} cross-session leak(s) detected\n",
            report.leaks.len()
        ));
        for leak in &report.leaks {
            s.push_str(&format!(
                "    session {} saw session {}'s marker\n",
                leak.session, leak.from
            ));
        }
    }
    s
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn clean_concurrent_run_passes() {
        let outcomes = vec![
            SessionOutcome::completed(0, "catalog-v1"),
            SessionOutcome::completed(1, "catalog-v1"),
            SessionOutcome::completed(2, "catalog-v1"),
        ];
        let report = analyze("catalog-v1", &outcomes);
        assert!(report.passed());
        assert_eq!(report.sessions, 3);
    }

    #[test]
    fn an_incomplete_session_is_a_liveness_failure() {
        let outcomes = vec![
            SessionOutcome::completed(0, "catalog-v1"),
            SessionOutcome::incomplete(1),
        ];
        let report = analyze("catalog-v1", &outcomes);
        assert!(!report.passed());
        assert_eq!(report.incomplete, vec![1]);
        // An incomplete session is not double-counted as diverged.
        assert!(report.diverged.is_empty());
    }

    #[test]
    fn a_diverged_result_is_a_determinism_failure() {
        let outcomes = vec![
            SessionOutcome::completed(0, "catalog-v1"),
            SessionOutcome::completed(1, "catalog-CORRUPT"),
        ];
        let report = analyze("catalog-v1", &outcomes);
        assert!(!report.passed());
        assert_eq!(report.diverged, vec![1]);
    }

    #[test]
    fn a_foreign_marker_is_an_isolation_breach() {
        let mut bled = SessionOutcome::completed(1, "catalog-v1");
        bled.foreign_markers = vec![0];
        let outcomes = vec![SessionOutcome::completed(0, "catalog-v1"), bled];
        let report = analyze("catalog-v1", &outcomes);
        assert!(!report.passed());
        assert_eq!(
            report.leaks,
            vec![Leak {
                session: 1,
                from: 0
            }]
        );
    }

    #[test]
    fn render_states_pass_and_each_failure_family() {
        let clean = analyze("x", &[SessionOutcome::completed(0, "x")]);
        assert!(render(&clean).contains("PASS"));

        let mut bled = SessionOutcome::completed(2, "y");
        bled.foreign_markers = vec![0];
        let bad = analyze(
            "x",
            &[
                SessionOutcome::incomplete(1),
                SessionOutcome::completed(0, "y"),
                bled,
            ],
        );
        let text = render(&bad);
        assert!(text.contains("liveness"));
        assert!(text.contains("determinism"));
        assert!(text.contains("isolation"));
    }
}
