//! Integration corpus for the verdict cache.
//!
//! Lives outside `src/` to keep the unit tests focused on the SHA-256 key
//! and TTL math. The cases here exercise the full file-backed cache: opt-in
//! gate, key sensitivity to each of the five input fields, TTL expiry, and
//! the get-then-put round trip.

use std::time::{Duration, SystemTime};

use mcptest_core::eval::{VerdictCache, VerdictCacheConfig, VerdictKey, VerdictRecord};

fn key<'a>(
    juror: &'a str,
    template: &'a str,
    criteria: &'a str,
    prompt: &'a str,
    response: &'a str,
) -> VerdictKey<'a> {
    VerdictKey {
        juror_model: juror,
        juror_prompt_template_version: template,
        criteria,
        prompt,
        response,
    }
}

#[test]
fn opt_in_gate_blocks_writes_when_disabled() {
    let dir = tempfile::tempdir().unwrap();
    let mut cache = VerdictCache::new(VerdictCacheConfig {
        root: dir.path().to_path_buf(),
        default_ttl_secs: 60,
        enabled: false,
    });
    let k = key("m", "v1", "c", "p", "r");
    let rec = VerdictRecord::new("pass", 60, SystemTime::now());
    cache.put(&k, &rec).unwrap();
    let entries: Vec<_> = std::fs::read_dir(dir.path()).unwrap().flatten().collect();
    assert!(entries.is_empty(), "disabled cache must not write to disk");
}

#[test]
fn opt_in_gate_blocks_reads_when_disabled() {
    let dir = tempfile::tempdir().unwrap();
    let mut enabled = VerdictCache::enabled_at(dir.path());
    let k = key("m", "v1", "c", "p", "r");
    enabled
        .put(&k, &VerdictRecord::new("pass", 60, SystemTime::now()))
        .unwrap();
    let mut disabled = VerdictCache::new(VerdictCacheConfig {
        root: dir.path().to_path_buf(),
        default_ttl_secs: 60,
        enabled: false,
    });
    assert!(disabled.get(&k).unwrap().is_none());
}

#[test]
fn each_of_five_fields_changes_the_key() {
    let baseline = key("openai/gpt-5", "v1", "{\"x\":1}", "Q", "A");
    let permutations = vec![
        key("anthropic/claude-3-5", "v1", "{\"x\":1}", "Q", "A"),
        key("openai/gpt-5", "v2", "{\"x\":1}", "Q", "A"),
        key("openai/gpt-5", "v1", "{\"x\":2}", "Q", "A"),
        key("openai/gpt-5", "v1", "{\"x\":1}", "different", "A"),
        key("openai/gpt-5", "v1", "{\"x\":1}", "Q", "different"),
    ];
    let baseline_fp = baseline.fingerprint();
    for other in permutations {
        assert_ne!(baseline_fp, other.fingerprint());
    }
}

#[test]
fn hit_then_miss_after_ttl_expires() {
    let dir = tempfile::tempdir().unwrap();
    let mut cache = VerdictCache::enabled_at(dir.path());
    let k = key("m", "v1", "c", "p", "r");
    // Store an entry whose TTL elapsed one second in the past.
    let stored_at = SystemTime::now()
        .checked_sub(Duration::from_secs(120))
        .unwrap()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    let stale = VerdictRecord {
        verdict: "stale".into(),
        score: Some(0.1),
        rationale: None,
        stored_at,
        ttl_secs: 60,
    };
    cache.put(&k, &stale).unwrap();
    let fetched = cache.get(&k).unwrap();
    assert!(fetched.is_none(), "expired record must miss");
}

#[test]
fn hit_within_ttl_returns_stored_record() {
    let dir = tempfile::tempdir().unwrap();
    let mut cache = VerdictCache::enabled_at(dir.path());
    let k = key("m", "v1", "c", "p", "r");
    let fresh = VerdictRecord::new("pass", 3600, SystemTime::now()).with_score(0.95);
    cache.put(&k, &fresh).unwrap();
    let got = cache.get(&k).unwrap().unwrap();
    assert_eq!(got.verdict, "pass");
    assert_eq!(got.score, Some(0.95));
}

#[test]
fn stats_track_hits_and_misses_independently() {
    let dir = tempfile::tempdir().unwrap();
    let mut cache = VerdictCache::enabled_at(dir.path());
    let k = key("m", "v1", "c", "p", "r");
    assert!(cache.get(&k).unwrap().is_none()); // miss 1
    cache
        .put(&k, &VerdictRecord::new("pass", 60, SystemTime::now()))
        .unwrap();
    assert!(cache.get(&k).unwrap().is_some()); // hit 1
    assert!(cache.get(&k).unwrap().is_some()); // hit 2
    let stats = cache.stats();
    assert_eq!(stats.hits, 2);
    assert_eq!(stats.misses, 1);
    assert_eq!(stats.stores, 1);
}

#[test]
fn separate_namespace_from_test_result_cache() {
    // The two caches must not share a directory. The default verdict root
    // sits under `.../verdicts`; the test-result root sits under
    // `.../runs`. We assert the default verdict root path includes
    // `verdicts` so a future change that accidentally points them at the
    // same root trips this test.
    let root = mcptest_core::eval::default_cache_root();
    assert!(root.to_string_lossy().contains("verdicts"));
    assert!(!root.to_string_lossy().contains("runs"));
}
