//! Offline integration: the distractor-tool scorer folds a recorded selection
//! plus the injected distractor set into the assertable `distractors.*` targets
//! with no model and no network. Mirrors how the runner will score the
//! `distractors:` gate from a recorded run against a padded candidate list.

use mcptest_core::eval::distractors::metrics_object;
use mcptest_core::eval::{
    catalog, degradation_curve, near_duplicates, resolve_distractor_ids, score_selection,
    DistractorSource, DistractorSpec,
};
use mcptest_core::executor::resolve_target;

/// The correct tool ids for the sample task: a single search tool.
fn correct() -> Vec<String> {
    vec!["google.search".to_string()]
}

#[test]
fn catalog_spec_injects_bundled_distractors_and_scores_a_clean_pick() {
    let spec = DistractorSpec::new(4, DistractorSource::Catalog);
    let distractors = resolve_distractor_ids(&spec);
    assert_eq!(distractors.len(), 4);
    assert!(distractors
        .iter()
        .all(|d| catalog().iter().any(|e| e.name == d)));

    // The agent picked the correct tool despite the four bundled distractors.
    let score = score_selection(&["google.search".to_string()], &correct(), &distractors);
    let metrics = metrics_object(&score);
    assert_eq!(
        resolve_target(&metrics, "distractors.accuracy").as_u64(),
        Some(100)
    );
    assert_eq!(
        resolve_target(&metrics, "distractors.chose_distractor").as_u64(),
        Some(0)
    );
}

#[test]
fn near_duplicate_spec_traps_a_collision_pick() {
    let spec = DistractorSpec::new(
        3,
        DistractorSource::NearDuplicate {
            of: vec!["search".to_string()],
        },
    );
    let distractors = resolve_distractor_ids(&spec);
    assert_eq!(distractors, near_duplicates("search")[..3].to_vec());

    // The agent fell for the uppercase near-duplicate instead of the real tool.
    let score = score_selection(&["SEARCH".to_string()], &correct(), &distractors);
    assert_eq!(score.chose_distractor, 1);
    assert_eq!(score.accuracy_percent, 0);
}

#[test]
fn accuracy_degrades_as_distractor_count_grows() {
    let baseline = score_selection(&["google.search".to_string()], &correct(), &[]);
    let noisy = score_selection(
        &["weather_forecast".to_string()],
        &correct(),
        &resolve_distractor_ids(&DistractorSpec::new(6, DistractorSource::Catalog)),
    );
    let curve = degradation_curve(&[noisy, baseline]);
    let counts: Vec<u32> = curve.iter().map(|p| p.distractor_count).collect();
    assert_eq!(counts, vec![0, 6]);
    // Clean baseline scores full; the noisy run that chose a distractor scores 0.
    assert_eq!(curve[0].accuracy_percent, 100);
    assert_eq!(curve[1].accuracy_percent, 0);
}

#[test]
fn metrics_object_is_byte_stable_across_calls() {
    let distractors = resolve_distractor_ids(&DistractorSpec::new(2, DistractorSource::Catalog));
    let a = score_selection(&["google.search".to_string()], &correct(), &distractors);
    let b = score_selection(&["google.search".to_string()], &correct(), &distractors);
    assert_eq!(metrics_object(&a), metrics_object(&b));
}
