//! Markdown rendering for authored claims.
//!
//! Generates `claims-explained.md` style output, grouping claims by category
//! and rendering full provenance details.

use crate::types::authored_claim::{
    format_authority_tier, parse_authority_tier, AuthoredClaim, ClaimStatus,
};

/// Render all claims as a markdown document grouped by category.
pub fn render_claims_markdown(claims: &[AuthoredClaim], project_name: &str) -> String {
    let mut out = String::new();
    out.push_str(&format!("# Claims for {project_name}\n\n"));

    // Group by category
    let mut categories: std::collections::BTreeMap<String, Vec<&AuthoredClaim>> =
        std::collections::BTreeMap::new();
    for claim in claims {
        categories.entry(claim.category.clone()).or_default().push(claim);
    }

    if categories.is_empty() {
        out.push_str("No claims authored yet.\n");
        return out;
    }

    for (category, cat_claims) in &categories {
        let active_count = cat_claims.iter().filter(|c| c.status == ClaimStatus::Active).count();
        let title = capitalize(category);
        out.push_str(&format!(
            "## {title} Claims ({active_count} active, {} total)\n\n",
            cat_claims.len()
        ));

        for claim in cat_claims {
            render_single_claim(&mut out, claim);
            out.push('\n');
        }
    }

    out
}

/// Render a single claim as a markdown section.
pub fn render_single_claim(out: &mut String, claim: &AuthoredClaim) {
    let status_badge = match claim.status {
        ClaimStatus::Active => "",
        ClaimStatus::Deprecated => " [DEPRECATED]",
        ClaimStatus::Superseded => " [SUPERSEDED]",
    };

    out.push_str(&format!("### {}: {}{status_badge}\n", claim.id, claim.invariant));
    out.push_str(&format!("- **Concept:** `{}`\n", claim.concept_path));
    out.push_str(&format!("- **Predicate:** `{}` = `{}`\n", claim.predicate, claim.value));
    out.push_str(&format!("- **Invariant:** {}\n", claim.invariant));
    out.push_str(&format!("- **Consequence:** {}\n", claim.consequence));
    out.push_str(&format!("- **Provenance:** {}\n", claim.provenance));

    let tier_display = parse_authority_tier(&claim.authority_tier)
        .map(format_authority_tier)
        .unwrap_or_else(|_| {
            tracing::warn!(
                claim_id = %claim.id,
                raw_tier = %claim.authority_tier,
                "Failed to parse authority tier, using raw value"
            );
            claim.authority_tier.clone()
        });
    out.push_str(&format!("- **Authority:** {tier_display}\n"));

    if !claim.evidence.is_empty() {
        out.push_str(&format!("- **Evidence:** {}\n", claim.evidence.join(", ")));
    }

    out.push_str(&format!("- **Status:** {}\n", claim.status));
    out.push_str(&format!("- **Author:** {} ({})\n", claim.created_by, claim.created_at));

    if let Some(ref supersedes) = claim.supersedes {
        out.push_str(&format!("- **Supersedes:** {supersedes}\n"));
    }

    if let Some(ref updated) = claim.updated_at {
        out.push_str(&format!("- **Updated:** {updated}\n"));
    }
}

/// Render a single claim as JSON wrapped in a structured envelope.
pub fn render_claim_json(
    claim: &AuthoredClaim,
    project_name: &str,
) -> Result<String, serde_json::Error> {
    let envelope = serde_json::json!({
        "type": "claim_detail",
        "project": project_name,
        "claim": claim
    });
    serde_json::to_string_pretty(&envelope)
}

/// Render all claims as JSON wrapped in a structured envelope.
pub fn render_claims_json(
    claims: &[AuthoredClaim],
    project_name: &str,
) -> Result<String, serde_json::Error> {
    let envelope = serde_json::json!({
        "type": "claims_explain",
        "project": project_name,
        "total": claims.len(),
        "claims": claims
    });
    serde_json::to_string_pretty(&envelope)
}

fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::authored_claim::AuthoredValue;

    fn sample_claim(id: &str, category: &str) -> AuthoredClaim {
        AuthoredClaim {
            id: id.to_string(),
            concept_path: "test/concept".to_string(),
            predicate: "test_pred".to_string(),
            value: AuthoredValue::Text("test_value".to_string()),
            comparison: Default::default(),
            provenance: "Test provenance".to_string(),
            invariant: "Test invariant MUST hold".to_string(),
            consequence: "Bad things happen".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec!["ADR-001".to_string()],
            category: category.to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "tester".to_string(),
            created_at: "2026-02-08T12:00:00Z".to_string(),
            updated_at: None,
        }
    }

    #[test]
    fn test_render_empty() {
        let md = render_claims_markdown(&[], "test-project");
        assert!(md.contains("No claims authored yet"));
    }

    #[test]
    fn test_render_groups_by_category() {
        let claims = vec![
            sample_claim("safety-001", "safety"),
            sample_claim("arch-001", "architecture"),
            sample_claim("safety-002", "safety"),
        ];
        let md = render_claims_markdown(&claims, "test-project");
        assert!(md.contains("## Architecture Claims (1 active, 1 total)"));
        assert!(md.contains("## Safety Claims (2 active, 2 total)"));
    }

    #[test]
    fn test_render_single_claim_fields() {
        let claim = sample_claim("test-001", "safety");
        let mut out = String::new();
        render_single_claim(&mut out, &claim);

        assert!(out.contains("### test-001:"));
        assert!(out.contains("**Concept:** `test/concept`"));
        assert!(out.contains("**Predicate:** `test_pred` = `test_value`"));
        assert!(out.contains("**Invariant:**"));
        assert!(out.contains("**Consequence:**"));
        assert!(out.contains("**Provenance:**"));
        assert!(out.contains("Expert (Tier 3)"));
        assert!(out.contains("**Evidence:** ADR-001"));
    }

    #[test]
    fn test_deprecated_badge() {
        let mut claim = sample_claim("dep-001", "safety");
        claim.status = ClaimStatus::Deprecated;
        let mut out = String::new();
        render_single_claim(&mut out, &claim);
        assert!(out.contains("[DEPRECATED]"));
    }

    #[test]
    fn test_render_json() {
        let claim = sample_claim("test-001", "safety");
        let json = render_claim_json(&claim, "test-project").expect("json");
        assert!(json.contains("\"type\": \"claim_detail\""));
        assert!(json.contains("\"project\": \"test-project\""));
        assert!(json.contains("\"id\": \"test-001\""));
    }

    #[test]
    fn test_render_claims_json_envelope() {
        let claims = vec![sample_claim("c-001", "arch"), sample_claim("c-002", "safety")];
        let json = render_claims_json(&claims, "my-project").expect("json");
        assert!(json.contains("\"type\": \"claims_explain\""));
        assert!(json.contains("\"total\": 2"));
        assert!(json.contains("\"project\": \"my-project\""));
    }
}
