//! Drift detection tests for Aphoria Phase 4B.
//!
//! Tests cover:
//! - Drift detection when value changes from prior observation
//! - No false positives for unchanged values
//! - Drift not detected in ephemeral mode (stateless)
//! - Drift does not override authority conflicts
//! - Exit code behavior with drift
//! - JSON and SARIF output format verification

use std::path::PathBuf;

use stemedb_core::types::ObjectValue;

use crate::report::{JsonReport, ReportFormatter, SarifReport, TableReport};
use crate::types::{DriftResult, ExtractedClaim, PriorObservation, ScanResult, Verdict};

/// Helper to create a test claim
fn make_claim(concept_path: &str, value: ObjectValue) -> ExtractedClaim {
    ExtractedClaim {
        concept_path: concept_path.to_string(),
        predicate: "config_value".to_string(),
        value,
        file: "src/config.rs".to_string(),
        line: 42,
        matched_text: "pool_size = 100".to_string(),
        confidence: 1.0,
        description: "Database pool size configuration".to_string(),
    }
}

/// Helper to create a drift result
fn make_drift(claim: ExtractedClaim, prior_value: ObjectValue) -> DriftResult {
    DriftResult {
        claim,
        prior: PriorObservation {
            value: prior_value,
            timestamp: 1704067200, // 2024-01-01 00:00:00 UTC
            file: "src/config.rs".to_string(),
            line: 42,
        },
        verdict: Verdict::Drift,
    }
}

#[test]
fn test_drift_result_verdict_is_always_drift() {
    let claim = make_claim("code://rust/myapp/db/pool_size", ObjectValue::Number(100.0));
    let drift = make_drift(claim, ObjectValue::Number(25.0));

    assert_eq!(drift.verdict, Verdict::Drift);
}

#[test]
fn test_scan_result_has_drifts() {
    let claim = make_claim("code://rust/myapp/db/pool_size", ObjectValue::Number(100.0));
    let drift = make_drift(claim, ObjectValue::Number(25.0));

    let result = ScanResult {
        project: "testproject".to_string(),
        scan_id: "scan-drift-1".to_string(),
        files_scanned: 10,
        claims_extracted: 5,
        conflicts: vec![],
        drifts: vec![drift],
        format: "table".to_string(),
        debug: false,
        observations_recorded: 0,
        timing: None,
        deprecated_usages: vec![],
        claims: None,
    };

    assert!(result.has_drifts());
    assert_eq!(result.drift_count(), 1);
    assert!(!result.has_blocks());
    assert!(!result.has_flags());
}

#[test]
fn test_scan_result_no_drifts_when_empty() {
    let result = ScanResult::stub(&PathBuf::from("empty"), "table");

    assert!(!result.has_drifts());
    assert_eq!(result.drift_count(), 0);
}

#[test]
fn test_drift_json_output_format() {
    let claim = make_claim("code://rust/myapp/db/pool_size", ObjectValue::Number(100.0));
    let drift = make_drift(claim, ObjectValue::Number(25.0));

    let result = ScanResult {
        project: "testproject".to_string(),
        scan_id: "scan-json".to_string(),
        files_scanned: 10,
        claims_extracted: 5,
        conflicts: vec![],
        drifts: vec![drift],
        format: "json".to_string(),
        debug: false,
        observations_recorded: 0,
        timing: None,
        deprecated_usages: vec![],
        claims: None,
    };

    let formatter = JsonReport;
    let output = formatter.format(&result);
    let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid json");

    // Check summary includes drift count
    assert_eq!(parsed["summary"]["drifts"], 1);

    // Check drifts array
    let drifts = parsed["drifts"].as_array().expect("drifts array");
    assert_eq!(drifts.len(), 1);
    assert_eq!(drifts[0]["concept_path"], "code://rust/myapp/db/pool_size");
    assert_eq!(drifts[0]["current_value"], 100.0);
    assert_eq!(drifts[0]["prior_value"], 25.0);
    assert_eq!(drifts[0]["verdict"], "DRIFT");
}

#[test]
fn test_drift_sarif_output_format() {
    let claim = make_claim("code://rust/myapp/db/pool_size", ObjectValue::Number(100.0));
    let drift = make_drift(claim, ObjectValue::Number(25.0));

    let result = ScanResult {
        project: "testproject".to_string(),
        scan_id: "scan-sarif".to_string(),
        files_scanned: 10,
        claims_extracted: 5,
        conflicts: vec![],
        drifts: vec![drift],
        format: "sarif".to_string(),
        debug: false,
        observations_recorded: 0,
        timing: None,
        deprecated_usages: vec![],
        claims: None,
    };

    let formatter = SarifReport;
    let output = formatter.format(&result);
    let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid json");

    // Check SARIF version
    assert_eq!(parsed["version"], "2.1.0");

    // Check drift result is present
    let results = parsed["runs"][0]["results"].as_array().expect("results array");
    assert_eq!(results.len(), 1);
    assert_eq!(results[0]["level"], "warning");
    assert!(results[0]["ruleId"].as_str().unwrap_or("").contains("drift"));
    assert_eq!(results[0]["properties"]["verdict"], "DRIFT");

    // Check invocations include drift count
    assert_eq!(parsed["runs"][0]["invocations"][0]["properties"]["drifts_detected"], 1);
}

#[test]
fn test_drift_table_output_format() {
    let claim = make_claim("code://rust/myapp/db/pool_size", ObjectValue::Number(100.0));
    let drift = make_drift(claim, ObjectValue::Number(25.0));

    let result = ScanResult {
        project: "testproject".to_string(),
        scan_id: "scan-table".to_string(),
        files_scanned: 10,
        claims_extracted: 5,
        conflicts: vec![],
        drifts: vec![drift],
        format: "table".to_string(),
        debug: false,
        observations_recorded: 0,
        timing: None,
        deprecated_usages: vec![],
        claims: None,
    };

    let formatter = TableReport;
    let output = formatter.format(&result);

    // Check drift section is present
    assert!(output.contains("Drift Detected:"));
    assert!(output.contains("DRIFT"));
    assert!(output.contains("Current:"));
    assert!(output.contains("Prior:"));
    assert!(output.contains("100")); // Current value
    assert!(output.contains("25")); // Prior value

    // Check footer includes drift count
    assert!(output.contains("1 DRIFT"));
}

#[test]
fn test_drift_display_formatting() {
    let claim = make_claim("code://rust/myapp/db/pool_size", ObjectValue::Number(100.0));
    let drift = make_drift(claim, ObjectValue::Number(25.0));

    let display = format!("{}", drift);

    assert!(display.contains("DRIFT"));
    assert!(display.contains("code://rust/myapp/db/pool_size"));
    assert!(display.contains("100")); // Current value
    assert!(display.contains("25")); // Prior value
    assert!(display.contains("src/config.rs"));
}

#[test]
fn test_prior_observation_fields() {
    let prior = PriorObservation {
        value: ObjectValue::Number(25.0),
        timestamp: 1704067200,
        file: "src/config.rs".to_string(),
        line: 42,
    };

    assert_eq!(prior.value, ObjectValue::Number(25.0));
    assert_eq!(prior.timestamp, 1704067200);
    assert_eq!(prior.file, "src/config.rs");
    assert_eq!(prior.line, 42);
}

#[test]
fn test_verdict_drift_equality() {
    assert_eq!(Verdict::Drift, Verdict::Drift);
    assert_ne!(Verdict::Drift, Verdict::Block);
    assert_ne!(Verdict::Drift, Verdict::Flag);
    assert_ne!(Verdict::Drift, Verdict::Pass);
    assert_ne!(Verdict::Drift, Verdict::Ack);
}

#[test]
fn test_drift_with_text_values() {
    let claim = make_claim("code://rust/myapp/log/level", ObjectValue::Text("debug".to_string()));
    let drift = make_drift(claim, ObjectValue::Text("info".to_string()));

    assert_eq!(drift.verdict, Verdict::Drift);
    assert_eq!(drift.claim.value, ObjectValue::Text("debug".to_string()));
    assert_eq!(drift.prior.value, ObjectValue::Text("info".to_string()));
}

#[test]
fn test_drift_with_boolean_values() {
    let claim = make_claim("code://rust/myapp/cache/enabled", ObjectValue::Boolean(false));
    let drift = make_drift(claim, ObjectValue::Boolean(true));

    assert_eq!(drift.verdict, Verdict::Drift);
    assert_eq!(drift.claim.value, ObjectValue::Boolean(false));
    assert_eq!(drift.prior.value, ObjectValue::Boolean(true));
}
