//! Runner helper for attaching jury consensus to test results.
//!
//! The matcher dispatch the  runner will own is not in this crate
//! yet. This module ships the smallest helper the runner needs once that
//! lands: pass an `llm-judge` match result through the W7 jury engine
//! and attach a `JuryConsensus` to either a [`TestResult`] or the
//! nested [`TestStep`] depending on whether the judge ran inside a
//! step. Keeping the wiring here, next to [`super::step::time_step`],
//! means the runner does not have to repeat the attach-to-step versus
//! attach-to-test branch at every matcher call site.

use crate::eval::jury::engine::{evaluate_with_jury, ConsensusMethod, JurorVote};
use crate::report::{TestResult, TestStep};

/// Attach a `JuryConsensus` built from `votes` to `target`. The
/// caller picks the consensus method (majority versus unanimous). The
/// runner uses this when an `llm-judge` matcher decides a test.
pub fn attach_to_test(target: &mut TestResult, votes: &[JurorVote], method: ConsensusMethod) {
    target.consensus = Some(evaluate_with_jury(votes, method));
}

/// Attach a `JuryConsensus` to a [`TestStep`]. Used when an
/// `llm-judge` matcher ran inside one step of a stepwise test (the
/// step-scoped attachment point the  timing model already
/// supports).
pub fn attach_to_step(target: &mut TestStep, votes: &[JurorVote], method: ConsensusMethod) {
    target.consensus = Some(evaluate_with_jury(votes, method));
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::eval::jury::engine::Vote;
    use crate::eval::provider::ProviderId;
    use crate::report::{Report, Verdict};

    fn passing_vote(model: &str) -> JurorVote {
        JurorVote {
            provider: ProviderId::Anthropic,
            model: model.into(),
            latency_ms: 100,
            cost_usd: Some(0.0001),
            tokens: None,
            weight: 1.0,
            result: Ok(Vote {
                verdict: Verdict::Pass,
                score: Some(0.9),
                reasoning: Some("rubric match".into()),
            }),
        }
    }

    #[test]
    fn attach_to_test_populates_consensus_field() {
        let mut r = Report::sample();
        let votes = vec![passing_vote("a"), passing_vote("b")];
        attach_to_test(&mut r.tests[0], &votes, ConsensusMethod::Majority);
        let consensus = r.tests[0].consensus.as_ref().expect("attached");
        assert_eq!(consensus.verdict, Verdict::Pass);
        assert_eq!(consensus.jurors.len(), 2);
    }

    #[test]
    fn attach_to_step_populates_consensus_field() {
        let mut r = Report::sample_stepwise();
        let votes = vec![passing_vote("a"), passing_vote("b")];
        attach_to_step(&mut r.tests[0].steps[0], &votes, ConsensusMethod::Unanimous);
        let consensus = r.tests[0].steps[0].consensus.as_ref().expect("attached");
        assert_eq!(consensus.verdict, Verdict::Pass);
    }

    #[test]
    fn attach_to_test_rolls_up_total_cost() {
        let mut r = Report::sample();
        let votes = vec![passing_vote("a"), passing_vote("b"), passing_vote("c")];
        attach_to_test(&mut r.tests[0], &votes, ConsensusMethod::Majority);
        let consensus = r.tests[0].consensus.as_ref().unwrap();
        let total = consensus.total_cost_usd.unwrap();
        assert!((total - 0.0003).abs() < 1e-9);
    }
}
