//! Coverage analysis for a suite run.
//!
//! Coverage answers a question users naturally ask: "is my test suite
//! actually comprehensive?" The runner records observations through a
//! [`tracker::CoverageTracker`], then folds them against the declared
//! server surface into a [`report::CoverageReport`] that the reporters
//! and threshold gate consume.
//!
//! Four dimensions are tracked:
//!
//! - Tool coverage (which declared tools were called).
//! - Pairwise argument coverage (which 2-way argument combinations were
//!   exercised; pairwise depth caps the combinatorial explosion at
//!   `n*(n-1)/2` per tool).
//! - Error-path coverage (which named error fixtures fired).
//! - Transport coverage (stdio + streamable HTTP + legacy SSE).
//!
//! ## Counting decision
//!
//! Snapshot tests count toward coverage. Every assertion against a tool
//! registers as a call. Without this, a server author with a snapshot
//! suite would see "0% coverage" even though their tests are running.
//! Decision documented.
//!
//! ## CLI flag coordination
//!
//! The `mcptest run` subcommand will accept `--coverage` and
//! `--coverage-threshold tools=80,args=60` flags. The flag wiring lives
//! in the CLI crate; this module exposes [`threshold::Thresholds::parse`]
//! so the CLI is a thin wrapper. The standalone `mcptest coverage
//! --suite tests/ --output coverage.html` subcommand will wrap
//! [`report::CoverageReport::render_html`].

pub mod report;
pub mod suggest;
pub mod threshold;
pub mod tracker;

pub use report::{
    build, ArgCombination, CoverageReport, ErrorPathCoverageEntry, ToolCoverageEntry,
    TransportCoverageEntry,
};
pub use suggest::{
    build_suggestions, SuggestionContext, SuggestionItem, SuggestionKind, Suggestions,
    SUGGESTION_MARKER,
};
pub use threshold::{ParseError, Threshold, ThresholdKind, Thresholds, UnmetThreshold};
pub use tracker::{CoverageTracker, Observation};

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

    #[test]
    fn end_to_end_run_records_and_reports() {
        let mut tracker = CoverageTracker::new();
        tracker.record(&Observation {
            tool: "create_issue".into(),
            args: json!({"title": "x", "public": true}),
            error_path: None,
            transport: "stdio".into(),
        });
        tracker.record(&Observation {
            tool: "create_issue".into(),
            args: json!({}),
            error_path: Some("invalid_title".into()),
            transport: "stdio".into(),
        });
        let declared_tools = vec!["create_issue".to_owned(), "list_issues".to_owned()];
        let declared_errs = vec!["invalid_title".to_owned()];
        let declared_transports = vec!["stdio".to_owned(), "http".to_owned()];
        let report = tracker.into_report(
            &declared_tools,
            &declared_errs,
            &declared_transports,
            Some(4),
        );
        assert_eq!(report.tool_coverage_pct, 50.0);
        assert_eq!(report.error_path_coverage_pct, 100.0);
        assert_eq!(report.transport_coverage_pct, 50.0);
        assert!(report.tools[0].called);
        assert!(!report.tools[1].called);

        let thresholds = Thresholds::parse("tools=80,error_paths=100").unwrap();
        let result = thresholds.evaluate(&report);
        // Tools threshold fails (50% vs 80%); error_paths passes.
        let unmet = result.unwrap_err();
        assert_eq!(unmet.len(), 1);
        assert_eq!(unmet[0].threshold.kind, ThresholdKind::Tools);
    }
}
