//! Handler for the `aphoria claims promote` command.
//!
//! Promotion raises a claim to a higher authority tier (lower tier number)
//! by creating a new claim that supersedes the original. The original claim
//! is marked Deprecated in the TOML file.
//!
//! The invariant protected here: claim data is never destroyed. The original
//! remains in the file with a Deprecated status; the new claim links back via
//! `supersedes` and carries a provenance trail.

use aphoria::claims_file::ClaimsFile;
use aphoria::{validate_promotion, ClaimStatus, PromotionRequest, PromotionResult};

/// Return the current UNIX timestamp in whole seconds.
///
/// Used to generate unique new claim IDs without external dependencies.
/// Returns 0 if the system clock is not available (e.g. before UNIX epoch).
fn timestamp_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

/// Format the current date as an ISO 8601 string (UTC).
fn date_str_now() -> String {
    chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
}

/// Execute a promotion: validate, load, supersede with higher tier, save.
///
/// Validates the request, loads the claim from the TOML file, creates a new
/// claim at the target tier that supersedes the original, marks the original
/// as Deprecated, and saves both changes.
///
/// Returns `PromotionResult` on success or soft validation failure.
/// Returns `Err(AphoriaError)` only for I/O or unexpected errors that
/// prevent the operation from completing (cannot read/write TOML file,
/// current directory unavailable, etc.).
pub fn execute_promotion(
    request: PromotionRequest,
    project_root: &std::path::Path,
) -> Result<PromotionResult, aphoria::AphoriaError> {
    let claims_path = ClaimsFile::default_path(project_root);
    let mut claims_file = ClaimsFile::load(&claims_path)?;

    // Find the claim by ID.
    let existing = claims_file.claims.iter().find(|c| c.id == request.claim_id).cloned();

    let existing = match existing {
        Some(c) => c,
        None => {
            return Ok(PromotionResult {
                original_claim_id: request.claim_id.clone(),
                new_claim_id: String::new(),
                previous_tier: String::new(),
                new_tier: String::new(),
                success: false,
                error: Some(format!("claim '{}' not found", request.claim_id)),
            });
        }
    };

    // Validate before touching any state.
    let is_deprecated = existing.status == ClaimStatus::Deprecated;
    if let Err(e) = validate_promotion(&request, &existing.authority_tier, is_deprecated) {
        return Ok(PromotionResult {
            original_claim_id: request.claim_id.clone(),
            new_claim_id: String::new(),
            previous_tier: existing.authority_tier.clone(),
            new_tier: request.target_tier.clone(),
            success: false,
            error: Some(e.to_string()),
        });
    }

    let previous_tier = existing.authority_tier.clone();
    let now = date_str_now();

    // Build the promoted provenance trail.
    let new_provenance = format!(
        "{} | Promoted to {} by {} on {}: {}",
        existing.provenance, request.target_tier, request.promoted_by, now, request.reason
    );

    // Merge evidence: existing + new, deduplicated, preserving order.
    let mut merged_evidence = existing.evidence.clone();
    for item in &request.evidence {
        if !merged_evidence.contains(item) {
            merged_evidence.push(item.clone());
        }
    }

    let new_id = format!("{}-promoted-{}", request.claim_id, timestamp_secs());

    let new_claim = aphoria::AuthoredClaim {
        id: new_id.clone(),
        concept_path: existing.concept_path.clone(),
        predicate: existing.predicate.clone(),
        value: existing.value.clone(),
        comparison: existing.comparison.clone(),
        provenance: new_provenance,
        invariant: existing.invariant.clone(),
        consequence: existing.consequence.clone(),
        authority_tier: request.target_tier.clone(),
        evidence: merged_evidence,
        category: existing.category.clone(),
        status: ClaimStatus::Active,
        supersedes: Some(existing.id.clone()),
        created_by: request.promoted_by.clone(),
        created_at: now.clone(),
        updated_at: None,
    };

    // Mark the original as Deprecated (in-place — ClaimsFile is a flat mutable TOML file).
    for c in claims_file.claims.iter_mut() {
        if c.id == request.claim_id {
            c.status = ClaimStatus::Deprecated;
            c.updated_at = Some(now);
            break;
        }
    }

    // Append the new claim and persist.
    claims_file.claims.push(new_claim);
    claims_file.save(&claims_path)?;

    Ok(PromotionResult {
        original_claim_id: request.claim_id,
        new_claim_id: new_id,
        previous_tier,
        new_tier: request.target_tier,
        success: true,
        error: None,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use aphoria::{AuthoredClaim, AuthoredValue, ClaimStatus};
    use tempfile::TempDir;

    /// Build a minimal active claim for test fixtures.
    fn sample_claim(id: &str, tier: &str) -> AuthoredClaim {
        AuthoredClaim {
            id: id.to_string(),
            concept_path: format!("test/{id}"),
            predicate: "behavior".to_string(),
            value: AuthoredValue::Bool(true),
            comparison: Default::default(),
            provenance: "Test analysis by engineer".to_string(),
            invariant: "System MUST behave correctly".to_string(),
            consequence: "Incorrect behavior causes failures".to_string(),
            authority_tier: tier.to_string(),
            evidence: vec!["ADR-001".to_string()],
            category: "architecture".to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "tester".to_string(),
            created_at: "2026-02-01T00:00:00Z".to_string(),
            updated_at: None,
        }
    }

    fn setup_claims_file(dir: &TempDir, claims: Vec<AuthoredClaim>) -> ClaimsFile {
        let path = ClaimsFile::default_path(dir.path());
        let mut file = ClaimsFile::new();
        for c in claims {
            file.claims.push(c);
        }
        file.save(&path).expect("save claims file");
        file
    }

    fn valid_request(claim_id: &str, target_tier: &str) -> PromotionRequest {
        PromotionRequest {
            claim_id: claim_id.to_string(),
            target_tier: target_tier.to_string(),
            evidence: vec!["new-study-reference".to_string()],
            reason: "Adopted as org-wide standard after review".to_string(),
            promoted_by: "jml".to_string(),
        }
    }

    #[test]
    fn test_happy_path_promotes_claim() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "community")]);

        let req = valid_request("claim-001", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute");

        assert!(result.success, "expected success, got error: {:?}", result.error);
        assert_eq!(result.original_claim_id, "claim-001");
        assert!(!result.new_claim_id.is_empty(), "new_claim_id must be set");
        assert_eq!(result.previous_tier, "community");
        assert_eq!(result.new_tier, "expert");
        assert!(result.error.is_none());
    }

    #[test]
    fn test_original_marked_deprecated_after_promotion() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "community")]);

        let req = valid_request("claim-001", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute");
        assert!(result.success);

        // Reload the file and verify the original is Deprecated.
        let path = ClaimsFile::default_path(dir.path());
        let loaded = ClaimsFile::load(&path).expect("reload");

        let original = loaded.find_by_id("claim-001").expect("find original");
        assert_eq!(original.status, ClaimStatus::Deprecated);
        assert!(original.updated_at.is_some(), "updated_at must be set on deprecation");
    }

    #[test]
    fn test_new_claim_persisted_with_correct_fields() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "community")]);

        let req = valid_request("claim-001", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute");
        assert!(result.success);

        let path = ClaimsFile::default_path(dir.path());
        let loaded = ClaimsFile::load(&path).expect("reload");

        let new_claim = loaded.find_by_id(&result.new_claim_id).expect("find new claim");
        assert_eq!(new_claim.authority_tier, "expert");
        assert_eq!(new_claim.status, ClaimStatus::Active);
        assert_eq!(new_claim.supersedes.as_deref(), Some("claim-001"));
        assert_eq!(new_claim.created_by, "jml");
        // New claim must have inherited the concept path.
        assert_eq!(new_claim.concept_path, "test/claim-001");
    }

    #[test]
    fn test_evidence_is_merged_and_deduplicated() {
        let dir = TempDir::new().expect("temp dir");
        let mut claim = sample_claim("claim-001", "community");
        // Original has "ADR-001"; request adds "ADR-001" (dup) and "new-study".
        claim.evidence = vec!["ADR-001".to_string()];
        setup_claims_file(&dir, vec![claim]);

        let mut req = valid_request("claim-001", "expert");
        req.evidence = vec!["ADR-001".to_string(), "new-study".to_string()];

        let result = execute_promotion(req, dir.path()).expect("execute");
        assert!(result.success);

        let path = ClaimsFile::default_path(dir.path());
        let loaded = ClaimsFile::load(&path).expect("reload");
        let new_claim = loaded.find_by_id(&result.new_claim_id).expect("find");

        // "ADR-001" must appear exactly once; "new-study" must be present.
        assert_eq!(new_claim.evidence.iter().filter(|e| e.as_str() == "ADR-001").count(), 1);
        assert!(new_claim.evidence.contains(&"new-study".to_string()));
    }

    #[test]
    fn test_provenance_contains_promotion_trail() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "community")]);

        let req = valid_request("claim-001", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute");
        assert!(result.success);

        let path = ClaimsFile::default_path(dir.path());
        let loaded = ClaimsFile::load(&path).expect("reload");
        let new_claim = loaded.find_by_id(&result.new_claim_id).expect("find");

        assert!(
            new_claim.provenance.contains("Promoted to expert"),
            "provenance must record target tier: {}",
            new_claim.provenance
        );
        assert!(
            new_claim.provenance.contains("jml"),
            "provenance must record promoter: {}",
            new_claim.provenance
        );
        assert!(
            new_claim.provenance.contains("Adopted as org-wide standard after review"),
            "provenance must contain reason: {}",
            new_claim.provenance
        );
    }

    #[test]
    fn test_claim_not_found_returns_soft_failure() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![]);

        let req = valid_request("nonexistent-claim", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute (I/O must not fail)");

        assert!(!result.success);
        assert!(result.error.is_some());
        let msg = result.error.unwrap();
        assert!(msg.contains("nonexistent-claim"), "error must name the missing claim: {msg}");
    }

    #[test]
    fn test_deprecated_claim_returns_validation_failure() {
        let dir = TempDir::new().expect("temp dir");
        let mut claim = sample_claim("claim-001", "community");
        claim.status = ClaimStatus::Deprecated;
        setup_claims_file(&dir, vec![claim]);

        let req = valid_request("claim-001", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute");

        assert!(!result.success);
        let msg = result.error.unwrap();
        assert!(msg.contains("deprecated"), "error must mention deprecated status: {msg}");
    }

    #[test]
    fn test_same_tier_returns_validation_failure() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "expert")]);

        let req = valid_request("claim-001", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute");

        assert!(!result.success);
        let msg = result.error.unwrap();
        assert!(
            msg.contains("lower authority") || msg.contains("equal"),
            "error must describe tier constraint: {msg}"
        );
    }

    #[test]
    fn test_lower_authority_tier_returns_validation_failure() {
        let dir = TempDir::new().expect("temp dir");
        // Claim is already at "expert" (tier 3); requesting "community" (tier 4) is a demotion.
        setup_claims_file(&dir, vec![sample_claim("claim-001", "expert")]);

        let req = valid_request("claim-001", "community");
        let result = execute_promotion(req, dir.path()).expect("execute");

        assert!(!result.success);
        assert!(result.error.is_some());
    }

    #[test]
    fn test_missing_evidence_returns_validation_failure() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "community")]);

        let mut req = valid_request("claim-001", "expert");
        req.evidence = vec![];
        let result = execute_promotion(req, dir.path()).expect("execute");

        assert!(!result.success);
        let msg = result.error.unwrap();
        assert!(msg.contains("evidence"), "error must mention evidence: {msg}");
    }

    #[test]
    fn test_missing_reason_returns_validation_failure() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "community")]);

        let mut req = valid_request("claim-001", "expert");
        req.reason = "   ".to_string();
        let result = execute_promotion(req, dir.path()).expect("execute");

        assert!(!result.success);
        let msg = result.error.unwrap();
        assert!(msg.contains("reason"), "error must mention reason: {msg}");
    }

    #[test]
    fn test_anecdotal_to_regulatory_is_valid() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "anecdotal")]);

        let req = valid_request("claim-001", "regulatory");
        let result = execute_promotion(req, dir.path()).expect("execute");

        assert!(result.success);
        assert_eq!(result.previous_tier, "anecdotal");
        assert_eq!(result.new_tier, "regulatory");
    }

    #[test]
    fn test_file_has_exactly_two_entries_after_promotion() {
        let dir = TempDir::new().expect("temp dir");
        setup_claims_file(&dir, vec![sample_claim("claim-001", "community")]);

        let req = valid_request("claim-001", "expert");
        let result = execute_promotion(req, dir.path()).expect("execute");
        assert!(result.success);

        let path = ClaimsFile::default_path(dir.path());
        let loaded = ClaimsFile::load(&path).expect("reload");
        // Original (now Deprecated) + new promoted claim.
        assert_eq!(
            loaded.len(),
            2,
            "file must contain exactly 2 entries: original (deprecated) + promoted"
        );
    }
}
