//! Integration corpus for the coverage module.
//!
//! These tests live outside the crate to exercise the public API in the
//! same shape the CLI will use: tracker -> report -> threshold.

use std::collections::{BTreeMap, BTreeSet};

use mcptest_core::coverage::{
    build, ArgCombination, CoverageReport, CoverageTracker, Observation, ThresholdKind, Thresholds,
};
use serde_json::json;

fn tools_named(names: &[&str]) -> Vec<String> {
    names.iter().map(|s| (*s).to_owned()).collect()
}

#[test]
fn empty_tracker_yields_zero_tool_coverage_when_tools_declared() {
    let tracker = CoverageTracker::new();
    let report = tracker.into_report(&tools_named(&["a", "b", "c"]), &[], &[], Some(4));
    assert_eq!(report.tools.len(), 3);
    assert_eq!(report.tool_coverage_pct, 0.0);
    for entry in &report.tools {
        assert!(!entry.called);
    }
}

#[test]
fn snapshot_calls_count_toward_coverage_per_wor_361_decision() {
    let mut tracker = CoverageTracker::new();
    // Two snapshot-driven assertions against the same tool.
    tracker.record(&Observation {
        tool: "search".into(),
        args: json!({"q": "abc"}),
        error_path: None,
        transport: "stdio".into(),
    });
    tracker.record(&Observation {
        tool: "search".into(),
        args: json!({"q": "abc"}),
        error_path: None,
        transport: "stdio".into(),
    });
    let report = tracker.into_report(&tools_named(&["search"]), &[], &[], Some(1));
    assert!(report.tools[0].called);
    assert_eq!(report.tools[0].call_count, 2);
    assert_eq!(report.tool_coverage_pct, 100.0);
}

#[test]
fn pairwise_arg_coverage_caps_at_n_choose_2_per_tool() {
    let mut tracker = CoverageTracker::new();
    // 4 args -> 6 pairwise combinations.
    tracker.record(&Observation {
        tool: "compose".into(),
        args: json!({"a": true, "b": false, "c": 1, "d": 0}),
        error_path: None,
        transport: "stdio".into(),
    });
    let combos_seen = tracker.arg_combinations_seen_total();
    assert_eq!(combos_seen, 6, "4 args produce 6 pairs");
}

#[test]
fn error_paths_roll_up_into_per_tool_breakdown() {
    let mut tracker = CoverageTracker::new();
    tracker.record(&Observation {
        tool: "create_issue".into(),
        args: json!({}),
        error_path: Some("blank_title".into()),
        transport: "stdio".into(),
    });
    let report = tracker.into_report(
        &tools_named(&["create_issue"]),
        &tools_named(&["blank_title", "rate_limit"]),
        &tools_named(&["stdio"]),
        Some(1),
    );
    assert_eq!(report.error_path_coverage_pct, 50.0);
    assert_eq!(
        report.tools[0].error_paths_seen,
        vec!["blank_title".to_owned()]
    );
}

#[test]
fn coverage_report_round_trips_via_json() {
    let report = CoverageReport::sample();
    let s = serde_json::to_string_pretty(&report).unwrap();
    let back: CoverageReport = serde_json::from_str(&s).unwrap();
    assert_eq!(back, report);
}

#[test]
fn thresholds_evaluate_reports_each_unmet_dimension() {
    let mut report = CoverageReport::sample();
    report.tool_coverage_pct = 60.0;
    report.arg_coverage_pct = 10.0;
    report.error_path_coverage_pct = 100.0;
    report.transport_coverage_pct = 100.0;
    let t = Thresholds::parse("tools=80,args=50,error_paths=100,transports=100").unwrap();
    let unmet = t.evaluate(&report).unwrap_err();
    assert_eq!(unmet.len(), 2);
    let kinds: Vec<_> = unmet.iter().map(|u| u.threshold.kind).collect();
    assert!(kinds.contains(&ThresholdKind::Tools));
    assert!(kinds.contains(&ThresholdKind::Args));
}

#[test]
fn thresholds_parse_rejects_unknown_keys_with_clear_message() {
    let err = Thresholds::parse("zebras=99").unwrap_err();
    let msg = err.to_string();
    assert!(msg.contains("zebras"));
}

#[test]
fn build_preserves_declared_tool_order_in_output() {
    let declared = tools_named(&["c", "a", "b"]);
    let mut calls = BTreeMap::new();
    calls.insert("b".into(), 1usize);
    let report = build(
        &declared,
        &[],
        &[],
        &calls,
        &BTreeMap::new(),
        &BTreeMap::new(),
        &BTreeSet::new(),
        &BTreeSet::new(),
        0,
        0,
    );
    assert_eq!(report.tools[0].name, "c");
    assert_eq!(report.tools[1].name, "a");
    assert_eq!(report.tools[2].name, "b");
    assert!(report.tools[2].called);
}

#[test]
fn arg_combination_canonical_form_dedups_pair_order() {
    let a = ArgCombination::from_pairs(vec![("y".into(), "1".into()), ("x".into(), "2".into())]);
    let b = ArgCombination::from_pairs(vec![("x".into(), "2".into()), ("y".into(), "1".into())]);
    assert_eq!(a, b);
}

#[test]
fn coverage_report_renders_html_with_uncovered_class() {
    let report = CoverageReport::sample();
    let html = report.render_html();
    assert!(html.contains("class=\"uncovered\""));
    assert!(html.contains("create_issue"));
}
