//! Jury bias mitigations corpus tests.
//!
//! Loads `tests/fixtures/jury-bias/corpus.yaml` and exercises each
//! pure-math mitigation against the hand-crafted fixtures. The fixtures
//! are designed so an LLM provider is not required to assert the
//! mitigation behaviour.

use std::path::PathBuf;

use serde::Deserialize;

use mcptest_core::eval::jury::bias::{
    apply_position_swap, calibrate_rubric, detect_same_family, verbosity_warning, Anchor,
    BiasMitigations,
};

#[derive(Debug, Deserialize)]
struct Entry {
    id: String,
    bias: String,
    #[allow(dead_code)]
    description: String,
    response_a: Option<String>,
    response_b: Option<String>,
    forward_score: Option<f64>,
    reverse_score: Option<f64>,
    anchors: Option<Vec<AnchorEntry>>,
    juror_model: Option<String>,
    generator_model: Option<String>,
    tokens_a: Option<usize>,
    tokens_b: Option<usize>,
    expected_signal: String,
}

#[derive(Debug, Deserialize)]
struct AnchorEntry {
    response: String,
    expected_score: f64,
    observed_score: f64,
}

#[derive(Debug, Deserialize)]
struct Corpus {
    entries: Vec<Entry>,
}

fn corpus_path() -> PathBuf {
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    manifest
        .join("..")
        .join("..")
        .join("tests")
        .join("fixtures")
        .join("jury-bias")
        .join("corpus.yaml")
}

fn load() -> Corpus {
    let path = corpus_path();
    let raw =
        std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
    serde_yaml::from_str(&raw).expect("corpus.yaml parses as Corpus")
}

#[test]
fn corpus_has_exactly_ten_entries() {
    let corpus = load();
    assert_eq!(
        corpus.entries.len(),
        10,
        " corpus must hold 10 fixtures; found {}",
        corpus.entries.len()
    );
}

#[test]
fn corpus_covers_every_bias_type() {
    let corpus = load();
    let mut seen: Vec<String> = corpus.entries.iter().map(|e| e.bias.clone()).collect();
    seen.sort();
    seen.dedup();
    seen.sort();
    let mut expected = vec![
        "calibration".to_string(),
        "cot".to_string(),
        "position".to_string(),
        "self_preference".to_string(),
        "verbosity".to_string(),
    ];
    expected.sort();
    assert_eq!(seen, expected);
}

#[test]
fn position_fixtures_match_swap_helper_output() {
    let corpus = load();
    for entry in corpus.entries.iter().filter(|e| e.bias == "position") {
        let a = entry.response_a.as_deref().expect("response_a present");
        let b = entry.response_b.as_deref().expect("response_b present");
        let forward = entry.forward_score.expect("forward_score present");
        let reverse = entry.reverse_score.expect("reverse_score present");
        let mut call_count = 0usize;
        let score = apply_position_swap(a, b, |_left, _right| {
            call_count += 1;
            if call_count == 1 {
                forward
            } else {
                reverse
            }
        });
        let drift_detected = score.drift > 1e-9;
        match entry.expected_signal.as_str() {
            "position_drift_detected" => {
                assert!(
                    drift_detected,
                    "{} expected drift; got mean={} drift={}",
                    entry.id, score.mean, score.drift
                );
            }
            "no_drift" => {
                assert!(
                    !drift_detected,
                    "{} expected no drift; got drift={}",
                    entry.id, score.drift
                );
            }
            other => panic!("{} has unexpected signal '{other}'", entry.id),
        }
    }
}

#[test]
fn calibration_fixtures_flag_miscalibrated_jurors() {
    let corpus = load();
    for entry in corpus.entries.iter().filter(|e| e.bias == "calibration") {
        let anchors_data = entry.anchors.as_ref().expect("anchors present");
        let anchors: Vec<Anchor> = anchors_data
            .iter()
            .map(|a| Anchor {
                response: a.response.clone(),
                expected_score: a.expected_score,
            })
            .collect();
        let result = calibrate_rubric(&entry.id, &anchors, 0.2, |response| {
            anchors_data
                .iter()
                .find(|a| a.response == response)
                .map(|a| a.observed_score)
                .unwrap_or(0.0)
        });
        match entry.expected_signal.as_str() {
            "miscalibrated" => assert!(result.miscalibrated, "{} should be flagged", entry.id),
            "calibrated" => assert!(!result.miscalibrated, "{} should not be flagged", entry.id),
            other => panic!("{} has unexpected signal '{other}'", entry.id),
        }
    }
}

#[test]
fn self_preference_fixtures_match_family_detector() {
    let corpus = load();
    for entry in corpus
        .entries
        .iter()
        .filter(|e| e.bias == "self_preference")
    {
        let juror = entry.juror_model.as_deref().expect("juror_model present");
        let generator = entry
            .generator_model
            .as_deref()
            .expect("generator_model present");
        let same = detect_same_family(juror, generator);
        match entry.expected_signal.as_str() {
            "same_family" => assert!(same, "{} expected same family", entry.id),
            "cross_family" => assert!(!same, "{} expected cross family", entry.id),
            other => panic!("{} has unexpected signal '{other}'", entry.id),
        }
    }
}

#[test]
fn verbosity_fixtures_match_warning_helper() {
    let corpus = load();
    for entry in corpus.entries.iter().filter(|e| e.bias == "verbosity") {
        let a = entry.tokens_a.expect("tokens_a present");
        let b = entry.tokens_b.expect("tokens_b present");
        let warning = verbosity_warning(a, b, 2.0);
        match entry.expected_signal.as_str() {
            "verbosity_warning" => assert!(warning.is_some(), "{} expected warning", entry.id),
            "no_verbosity_warning" => {
                assert!(warning.is_none(), "{} expected no warning", entry.id)
            }
            other => panic!("{} has unexpected signal '{other}'", entry.id),
        }
    }
}

#[test]
fn cot_fixtures_pair_with_mitigations_flag() {
    let corpus = load();
    for entry in corpus.entries.iter().filter(|e| e.bias == "cot") {
        let mut mitigations = BiasMitigations::default();
        match entry.expected_signal.as_str() {
            "cot_enabled" => {
                mitigations.chain_of_thought = true;
                assert!(mitigations.chain_of_thought);
            }
            "cot_disabled" => {
                mitigations.chain_of_thought = false;
                assert!(!mitigations.chain_of_thought);
            }
            other => panic!("{} has unexpected signal '{other}'", entry.id),
        }
    }
}

#[test]
fn family_detection_covers_all_four_known_families() {
    assert!(detect_same_family("claude-opus-4-7", "claude-haiku-4"));
    assert!(detect_same_family("gpt-5", "gpt-4o"));
    assert!(detect_same_family("gemini-2.0-pro", "gemini-1.5-flash"));
    assert!(detect_same_family("mistral-large", "mixtral-8x22b"));
}

#[test]
fn verbosity_warning_handles_exact_threshold() {
    let warning = verbosity_warning(100, 200, 2.0);
    assert!(warning.is_some(), "verbosity at threshold should warn");
}
