//! Observations display formatter for debugging extractor alignment.
//!
//! This formatter shows all observations created during scan with their concept paths,
//! making extractor alignment issues transparent. Used with `--show-observations` flag.

use crate::report::object_value_display;
use crate::types::ScanResult;
use crate::verify::tail_path;

/// Format observations with concept path alignment analysis.
///
/// Shows all observations created during scan plus analysis of which claims
/// they match (or don't match). Helps debug why extractors aren't detecting violations.
pub fn format_observations(result: &ScanResult) -> String {
    let mut output = String::new();

    // Section 1: List all observations
    output.push_str(&format!("\nObservations Created ({} total):\n\n", result.observations.len()));

    if result.observations.is_empty() {
        output.push_str("  (No observations created - check if extractors matched any code)\n\n");
        return output;
    }

    for (idx, obs) in result.observations.iter().enumerate() {
        output.push_str(&format!(
            "  {}. {} :: {} = {}\n",
            idx + 1,
            obs.concept_path,
            obs.predicate,
            object_value_display(&obs.value),
        ));
        output.push_str(&format!("     File: {}:{}\n", obs.file, obs.line));

        // Show matched text (first line only)
        let matched_text = obs.matched_text.lines().next().unwrap_or("");
        if !matched_text.is_empty() {
            output.push_str(&format!("     Match: {}\n", matched_text));
        }

        output.push_str(&format!("     Confidence: {:.2}\n", obs.confidence));
        output.push('\n');
    }

    // Section 2: Claim matching analysis (if verify report exists)
    if let Some(verify) = &result.verify {
        output.push_str("\nClaim Matching Analysis:\n\n");

        // Check which claims have observations
        for claim_result in &verify.results {
            let claim = match &claim_result.claim {
                Some(c) => c,
                None => continue,
            };

            let concept_path = &claim.concept_path;
            let tail = tail_path(concept_path).unwrap_or_else(|| concept_path.to_string());

            // Find observations with matching tail path
            let matching_obs: Vec<_> = result
                .observations
                .iter()
                .filter(|obs| {
                    tail_path(&obs.concept_path).unwrap_or_else(|| obs.concept_path.clone()) == tail
                })
                .collect();

            if !matching_obs.is_empty() {
                output.push_str(&format!(
                    "  ✅ {} → matches {} (tail: {})\n",
                    matching_obs[0].concept_path, claim.id, tail,
                ));
            } else {
                output.push_str(&format!("  ❌ {} → NO MATCH\n", concept_path));
                output.push_str(&format!(
                    "     Expected concept_path in observations: {}\n",
                    concept_path
                ));
                output.push_str(&format!("     Tail-path needed: {}\n", tail));
                output.push_str("     Issue: No extractor produced this concept_path\n");
                output.push('\n');
            }
        }
    } else {
        output.push_str("\nNote: Use `aphoria verify run` to see claim matching analysis.\n");
    }

    output
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::Observation;
    use crate::verify::{AuditVerdict, VerifyReport, VerifyResult, VerifySummary};
    use crate::AuthoredClaim;
    use std::path::PathBuf;
    use stemedb_core::types::ObjectValue;

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

    fn make_claim(id: &str, concept_path: &str) -> AuthoredClaim {
        use crate::types::authored_claim::{AuthoredValue, ClaimStatus, ComparisonMode};

        AuthoredClaim {
            id: id.to_string(),
            concept_path: concept_path.to_string(),
            predicate: "bounded".to_string(),
            value: AuthoredValue::Bool(false),
            comparison: ComparisonMode::Equals,
            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: "test".to_string(),
            created_at: "2024-01-01T00:00:00Z".to_string(),
            updated_at: None,
        }
    }

    #[test]
    fn test_format_empty_observations() {
        let result = ScanResult::stub(&PathBuf::from("."), "table");
        let output = format_observations(&result);
        assert!(output.contains("No observations created"));
    }

    #[test]
    fn test_format_observations_without_verify() {
        let mut result = ScanResult::stub(&PathBuf::from("."), "table");
        result.observations = vec![make_observation("msgqueue/queue/max_size", "bounded", false)];

        let output = format_observations(&result);
        assert!(output.contains("msgqueue/queue/max_size"));
        assert!(output.contains("bounded"));
        assert!(output.contains("test.rs:42"));
        assert!(output.contains("Use `aphoria verify run`"));
    }

    #[test]
    fn test_format_observations_with_matching_claims() {
        let mut result = ScanResult::stub(&PathBuf::from("."), "table");
        result.observations = vec![make_observation("msgqueue/queue/max_size", "bounded", false)];

        let verify = VerifyReport {
            results: vec![VerifyResult {
                claim: Some(make_claim("msg-001", "msgqueue/queue/max_size")),
                verdict: AuditVerdict::Pass,
                matching_observations: vec![],
                explanation: String::new(),
            }],
            summary: VerifySummary::default(),
        };
        result.verify = Some(verify);

        let output = format_observations(&result);
        assert!(output.contains("✅"));
        assert!(output.contains("msg-001"));
        assert!(output.contains("queue/max_size")); // tail path
    }

    #[test]
    fn test_format_observations_with_non_matching_claims() {
        let mut result = ScanResult::stub(&PathBuf::from("."), "table");
        // Create an observation that doesn't match the claim's tail path
        result.observations = vec![make_observation("different/path/item", "pred", true)];

        let verify = VerifyReport {
            results: vec![VerifyResult {
                claim: Some(make_claim("msg-001", "msgqueue/queue/max_size")),
                verdict: AuditVerdict::Missing,
                matching_observations: vec![],
                explanation: String::new(),
            }],
            summary: VerifySummary::default(),
        };
        result.verify = Some(verify);

        let output = format_observations(&result);
        assert!(output.contains("❌"));
        assert!(output.contains("NO MATCH"));
        assert!(output.contains("msgqueue/queue/max_size"));
        assert!(output.contains("No extractor produced this concept_path"));
    }

    #[test]
    fn test_format_observations_with_scheme_in_concept_path() {
        let mut result = ScanResult::stub(&PathBuf::from("."), "table");
        result.observations =
            vec![make_observation("code://rust/myapp/tls/cert_verification", "enabled", true)];

        let output = format_observations(&result);
        assert!(output.contains("code://rust/myapp/tls/cert_verification"));
        assert!(output.contains("enabled"));
    }

    #[test]
    fn test_format_observations_multiple_observations() {
        let mut result = ScanResult::stub(&PathBuf::from("."), "table");
        result.observations = vec![
            make_observation("path1/item1", "pred1", true),
            make_observation("path2/item2", "pred2", false),
            make_observation("path3/item3", "pred3", true),
        ];

        let output = format_observations(&result);
        assert!(output.contains("3 total"));
        assert!(output.contains("1. path1/item1"));
        assert!(output.contains("2. path2/item2"));
        assert!(output.contains("3. path3/item3"));
    }

    #[test]
    fn test_format_observations_with_empty_matched_text() {
        let mut obs = make_observation("test/path", "pred", true);
        obs.matched_text = String::new();

        let mut result = ScanResult::stub(&PathBuf::from("."), "table");
        result.observations = vec![obs];

        let output = format_observations(&result);
        // Should not crash, should skip the "Match:" line
        assert!(output.contains("test/path"));
        assert!(!output.contains("Match:"));
    }
}
