//! Integration tests for the verification engine.
//!
//! Tests the full verify pipeline: claims + observations → verdicts.

use crate::claims_file::ClaimsFile;
use crate::types::authored_claim::{AuthoredClaim, AuthoredValue, ClaimStatus, ComparisonMode};
use crate::types::Observation;
use crate::verify::{verify_claims, AuditVerdict};
use stemedb_core::types::ObjectValue;

fn make_claim(
    id: &str,
    concept_path: &str,
    predicate: &str,
    value: AuthoredValue,
    comparison: ComparisonMode,
) -> AuthoredClaim {
    AuthoredClaim {
        id: id.to_string(),
        concept_path: concept_path.to_string(),
        predicate: predicate.to_string(),
        value,
        comparison,
        provenance: "test".to_string(),
        invariant: "test invariant".to_string(),
        consequence: "test consequence".to_string(),
        authority_tier: "expert".to_string(),
        evidence: vec![],
        category: "test".to_string(),
        status: ClaimStatus::Active,
        supersedes: None,
        created_by: "tester".to_string(),
        created_at: "2026-02-08T12:00:00Z".to_string(),
        updated_at: None,
    }
}

fn make_obs(concept_path: &str, predicate: &str, value: ObjectValue) -> Observation {
    Observation {
        concept_path: concept_path.to_string(),
        predicate: predicate.to_string(),
        value,
        file: "src/test.rs".to_string(),
        line: 42,
        matched_text: "test match".to_string(),
        confidence: 1.0,
        description: "test".to_string(),
    }
}

#[test]
fn test_full_verify_mixed_verdicts() {
    let claims = vec![
        // This should PASS — observation matches
        make_claim(
            "pass-claim",
            "project/tls/cert_verification",
            "enabled",
            AuthoredValue::Bool(true),
            ComparisonMode::Equals,
        ),
        // This should CONFLICT — value differs
        make_claim(
            "conflict-claim",
            "project/config/timeout",
            "value",
            AuthoredValue::Number(30.0),
            ComparisonMode::Equals,
        ),
        // This should be MISSING — no observation
        make_claim(
            "missing-claim",
            "project/auth/mfa",
            "enabled",
            AuthoredValue::Bool(true),
            ComparisonMode::Present,
        ),
    ];

    let observations = vec![
        make_obs(
            "code://rust/project/tls/cert_verification",
            "enabled",
            ObjectValue::Boolean(true),
        ),
        make_obs("code://rust/project/config/timeout", "value", ObjectValue::Number(60.0)),
        // An unclaimed observation
        make_obs(
            "code://rust/project/cors/allow_origin",
            "origin",
            ObjectValue::Text("*".to_string()),
        ),
    ];

    let report = verify_claims(&claims, &observations);

    assert_eq!(report.summary.total_claims, 3);
    assert_eq!(report.summary.pass, 1);
    assert_eq!(report.summary.conflict, 1);
    assert_eq!(report.summary.missing, 1);
    assert_eq!(report.summary.unclaimed, 1);

    // Verify individual results
    let pass = report
        .results
        .iter()
        .find(|r| r.claim.as_ref().map(|c| c.id.as_str()) == Some("pass-claim"));
    assert_eq!(pass.map(|r| &r.verdict), Some(&AuditVerdict::Pass));

    let conflict = report
        .results
        .iter()
        .find(|r| r.claim.as_ref().map(|c| c.id.as_str()) == Some("conflict-claim"));
    assert_eq!(conflict.map(|r| &r.verdict), Some(&AuditVerdict::Conflict));

    let missing = report
        .results
        .iter()
        .find(|r| r.claim.as_ref().map(|c| c.id.as_str()) == Some("missing-claim"));
    assert_eq!(missing.map(|r| &r.verdict), Some(&AuditVerdict::Missing));
}

#[test]
fn test_claims_file_roundtrip_with_comparison() {
    let temp_dir = tempfile::TempDir::new().expect("create temp dir");
    let path = temp_dir.path().join("claims.toml");

    let mut file = ClaimsFile::new();
    file.add(make_claim(
        "absent-claim",
        "core/imports/tokio",
        "imported",
        AuthoredValue::Bool(true),
        ComparisonMode::Absent,
    ));
    file.add(make_claim(
        "present-claim",
        "project/tls/cert",
        "enabled",
        AuthoredValue::Bool(true),
        ComparisonMode::Present,
    ));

    file.save(&path).expect("save");

    let loaded = ClaimsFile::load(&path).expect("load");
    assert_eq!(loaded.len(), 2);
    assert_eq!(loaded.claims[0].comparison, ComparisonMode::Absent);
    assert_eq!(loaded.claims[1].comparison, ComparisonMode::Present);
}

#[test]
fn test_verifiable_predicates_coverage() {
    use crate::config::AphoriaConfig;
    use crate::extractors::ExtractorRegistry;

    let config = AphoriaConfig::default();
    let registry = ExtractorRegistry::new(&config);

    // Count extractors that declare verifiable predicates
    let declaring_count =
        registry.extractors().iter().filter(|e| !e.verifiable_predicates().is_empty()).count();

    // We implemented verifiable_predicates() on 10 extractors;
    // self_audit is opt-in so 9 are present in default config
    assert!(
        declaring_count >= 9,
        "Expected at least 9 extractors with verifiable_predicates(), got {declaring_count}"
    );

    // Verify specific extractors declare the right predicates
    let tls_verify = registry.extractors().iter().find(|e| e.name() == "tls_verify");
    assert!(tls_verify.is_some());
    let preds = tls_verify.map(|e| e.verifiable_predicates()).unwrap_or_default();
    assert!(preds.contains(&("tls/cert_verification", "enabled")));

    // Verify wildcard pattern for import_graph
    let import_graph = registry.extractors().iter().find(|e| e.name() == "import_graph");
    assert!(import_graph.is_some());
    let preds = import_graph.map(|e| e.verifiable_predicates()).unwrap_or_default();
    assert!(preds.contains(&("imports/*", "imported")));
}

#[test]
fn test_extractor_claim_map_with_dogfood_claims() {
    use crate::config::AphoriaConfig;
    use crate::extractors::ExtractorRegistry;
    use crate::verify::compute_extractor_claim_map;

    let config = AphoriaConfig::default();
    let registry = ExtractorRegistry::new(&config);

    // Claims matching our dogfood claims.toml
    let claims = vec![
        make_claim(
            "aphoria-tls-verify-001",
            "aphoria/tls/cert_verification",
            "enabled",
            AuthoredValue::Bool(false),
            ComparisonMode::Absent,
        ),
        make_claim(
            "aphoria-no-md5-001",
            "aphoria/crypto/hashing/algorithm",
            "algorithm",
            AuthoredValue::Text("md5".to_string()),
            ComparisonMode::NotEquals,
        ),
        make_claim(
            "aphoria-no-wildcard-cors-001",
            "aphoria/cors/allow_origin",
            "config_value",
            AuthoredValue::Text("*".to_string()),
            ComparisonMode::Absent,
        ),
    ];

    let map = compute_extractor_claim_map(&claims, registry.extractors());

    // All 3 claims should have covering extractors
    for mapping in &map.claim_mappings {
        assert!(
            !mapping.covering_extractors.is_empty(),
            "Claim {} should have a covering extractor",
            mapping.claim_id
        );
    }
}

#[test]
fn test_absent_mode_integration() {
    // "core MUST NOT import tokio" — claim says tokio import should be absent
    let claims = vec![make_claim(
        "no-tokio",
        "core/imports/tokio",
        "imported",
        AuthoredValue::Bool(true),
        ComparisonMode::Absent,
    )];

    // No tokio import found — PASS
    let report = verify_claims(&claims, &[]);
    assert_eq!(report.summary.pass, 1);

    // Tokio import found — CONFLICT
    let obs =
        vec![make_obs("code://rust/core/imports/tokio", "imported", ObjectValue::Boolean(true))];
    let report = verify_claims(&claims, &obs);
    assert_eq!(report.summary.conflict, 1);
}
