//! Jury-accuracy corpus shape validation.
//!
//! Parses `tests/fixtures/jury-accuracy/corpus.yaml` and confirms every
//! entry carries all required fields, every category is one of the five
//! known values, every entry has exactly three human labels, the
//! `gold_label` matches the majority of `human_labels`, and the
//! `disagreement` flag matches a 2-1 split.
//!
//! The actual jury-accuracy benchmark is `#[ignore]` until
//! (ADR 0020 trait) and  (provider impls) land. Once both are in,
//! the ignored test loops over the corpus, calls the jury, and asserts
//! agreement at or above 85%.

use std::collections::BTreeSet;
use std::path::PathBuf;

use serde::Deserialize;

/// On-disk shape of a corpus entry. Mirrors `corpus.yaml`. `response` and
/// `criteria` are not read by the shape tests but are required at
/// deserialize time so a malformed corpus fails fast.
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Entry {
    id: String,
    category: String,
    response: String,
    criteria: String,
    human_labels: Vec<String>,
    gold_label: String,
    disagreement: bool,
}

#[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-accuracy")
        .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")
}

fn majority(labels: &[String]) -> &'static str {
    let passes = labels.iter().filter(|l| l.as_str() == "pass").count();
    let fails = labels.iter().filter(|l| l.as_str() == "fail").count();
    if passes > fails {
        "pass"
    } else {
        "fail"
    }
}

#[test]
fn corpus_loads_and_has_enough_entries() {
    let corpus = load();
    assert!(
        corpus.entries.len() >= 50 && corpus.entries.len() <= 100,
        "corpus has {} entries;  requires 50 to 100",
        corpus.entries.len()
    );
}

#[test]
fn every_entry_has_required_fields() {
    let corpus = load();
    for e in &corpus.entries {
        assert!(!e.id.is_empty(), "entry has empty id");
        assert!(!e.category.is_empty(), "entry {} has empty category", e.id);
        // response may legitimately be empty (see edge-001); criteria must not.
        assert!(!e.criteria.is_empty(), "entry {} has empty criteria", e.id);
        assert!(
            !e.human_labels.is_empty(),
            "entry {} has no human labels",
            e.id
        );
        assert!(
            !e.gold_label.is_empty(),
            "entry {} has empty gold_label",
            e.id
        );
    }
}

#[test]
fn every_entry_has_unique_id() {
    let corpus = load();
    let mut seen = BTreeSet::new();
    for e in &corpus.entries {
        assert!(seen.insert(e.id.clone()), "duplicate id: {}", e.id);
    }
}

#[test]
fn every_category_is_one_of_five() {
    let allowed: BTreeSet<&str> = ["factual", "format", "helpfulness", "code", "edge"]
        .into_iter()
        .collect();
    let corpus = load();
    for e in &corpus.entries {
        assert!(
            allowed.contains(e.category.as_str()),
            "entry {} has unknown category {}",
            e.id,
            e.category
        );
    }
}

#[test]
fn every_entry_has_three_human_labels() {
    let corpus = load();
    for e in &corpus.entries {
        assert_eq!(
            e.human_labels.len(),
            3,
            "entry {} has {} labels;  requires exactly 3",
            e.id,
            e.human_labels.len()
        );
        for label in &e.human_labels {
            assert!(
                label == "pass" || label == "fail",
                "entry {} has invalid label {}",
                e.id,
                label
            );
        }
    }
}

#[test]
fn gold_label_matches_majority_of_human_labels() {
    let corpus = load();
    for e in &corpus.entries {
        let expected = majority(&e.human_labels);
        assert_eq!(
            e.gold_label, expected,
            "entry {}: gold_label {} does not match majority {} of labels {:?}",
            e.id, e.gold_label, expected, e.human_labels
        );
    }
}

#[test]
fn disagreement_flag_matches_two_one_split() {
    let corpus = load();
    for e in &corpus.entries {
        let passes = e
            .human_labels
            .iter()
            .filter(|l| l.as_str() == "pass")
            .count();
        let fails = e.human_labels.len() - passes;
        let is_split = passes != 0 && fails != 0;
        assert_eq!(
            e.disagreement, is_split,
            "entry {}: disagreement={} but labels {:?} split is {}",
            e.id, e.disagreement, e.human_labels, is_split
        );
    }
}

#[test]
fn category_distribution_matches_corpus_spec() {
    //  asks for ~15 factual, ~15 format, ~15 helpfulness,
    // ~15 code, ~10 edge. We pin a band per category so the corpus does
    // not silently skew toward one bucket as entries are added.
    let corpus = load();
    let mut counts: std::collections::BTreeMap<String, usize> = Default::default();
    for e in &corpus.entries {
        *counts.entry(e.category.clone()).or_default() += 1;
    }
    for cat in ["factual", "format", "helpfulness", "code"] {
        let n = counts.get(cat).copied().unwrap_or(0);
        assert!(
            (10..=20).contains(&n),
            "category {cat} has {n} entries; expected 10 to 20"
        );
    }
    let edge = counts.get("edge").copied().unwrap_or(0);
    assert!(
        (5..=15).contains(&edge),
        "category edge has {edge} entries; expected 5 to 15"
    );
}

/// Accuracy benchmark. Disabled until the LLM provider trait (,
/// ADR 0020) and the provider impls land. Once they do,
/// remove `#[ignore]` and wire the body to the actual jury runner.
#[test]
#[ignore = "depends on  trait and  provider impls"]
fn jury_agreement_with_gold_meets_target() {
    let corpus = load();
    // Future shape:
    //   let provider = build_provider_from_env();
    //   let jury = Jury::new(&provider, 3, Consensus::Majority);
    //   let mut agree = 0;
    //   for e in &corpus.entries {
    //       let verdict = jury.evaluate(&e.criteria, &e.response).await;
    //       if verdict.label() == e.gold_label { agree += 1; }
    //   }
    //   let rate = agree as f64 / corpus.entries.len() as f64;
    //   assert!(rate >= 0.85, "jury agreement {rate} below 0.85 target");
    let _ = corpus;
    panic!("jury runner not yet available; see  / ");
}
