//! The grounded control checker: judge each candidate region for a control, then
//! keep only the verdicts that survive the grounding gate.
//!
//! Generic over [`ControlJudge`] so tests drive it with a deterministic stub —
//! the whole recognize → ground path is then exercised without an LLM. With the
//! real judge, determinism comes from temperature 0 plus the gate.

use compliance_core::control_check::{ground, CandidateRegion, ControlCheckSpec};
use compliance_core::models::Finding;

use super::judge::ControlJudge;

/// Runs a [`ControlJudge`] over candidate regions and grounds the results.
pub struct GroundedControlChecker<J> {
    judge: J,
}

impl<J: ControlJudge> GroundedControlChecker<J> {
    pub fn new(judge: J) -> Self {
        Self { judge }
    }

    /// Judge every candidate region for `spec` and return the grounded findings.
    /// A verdict that doesn't quote real code in its region is dropped by
    /// [`ground`], so nothing fabricated reaches the caller.
    pub async fn check(
        &self,
        spec: &ControlCheckSpec,
        regions: &[CandidateRegion],
        repo_id: &str,
    ) -> Vec<Finding> {
        let mut findings = Vec::new();
        for region in regions {
            let verdict = self.judge.judge(spec, region).await;
            if let Some(finding) = ground(spec, region, &verdict, repo_id) {
                findings.push(finding);
            }
        }
        findings
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use compliance_core::control_check::LlmVerdict;
    use compliance_core::models::finding::Severity;

    /// Deterministic stub: returns a fixed verdict for every region, so the
    /// recognize → ground composition is tested without an LLM.
    struct StubJudge {
        verdict: LlmVerdict,
    }

    impl ControlJudge for StubJudge {
        async fn judge(&self, _spec: &ControlCheckSpec, _region: &CandidateRegion) -> LlmVerdict {
            self.verdict.clone()
        }
    }

    fn spec() -> ControlCheckSpec {
        ControlCheckSpec {
            control_id: "cra-ai-8".into(),
            title: "No default passwords".into(),
            requirement: "No default credentials".into(),
            default_cwe: Some("CWE-798".into()),
            severity: Severity::High,
        }
    }

    fn region(content: &str) -> CandidateRegion {
        CandidateRegion {
            file: "src/auth.py".into(),
            start_line: 1,
            content: content.into(),
        }
    }

    #[tokio::test]
    async fn keeps_grounded_and_drops_ungrounded() {
        let checker = GroundedControlChecker::new(StubJudge {
            verdict: LlmVerdict {
                violates: true,
                snippet: "PASSWORD = \"admin\"".into(),
                cwe: None,
                confidence: 0.9,
            },
        });
        let regions = vec![
            region("x = 1\nPASSWORD = \"admin\"\n"), // quotes real code → grounded
            region("totally unrelated code\n"),      // snippet absent → dropped
        ];
        let findings = checker.check(&spec(), &regions, "repo").await;
        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0].control_refs, vec!["cra-ai-8".to_string()]);
        assert_eq!(findings[0].line_number, Some(2));
    }

    #[tokio::test]
    async fn non_violation_yields_nothing() {
        let checker = GroundedControlChecker::new(StubJudge {
            verdict: LlmVerdict {
                violates: false,
                snippet: String::new(),
                cwe: None,
                confidence: 0.0,
            },
        });
        let findings = checker
            .check(&spec(), &[region("PASSWORD = \"admin\"\n")], "repo")
            .await;
        assert!(findings.is_empty());
    }
}
