//! Bridge between Observation and Episteme Assertion.
//!
//! Converts claims extracted from source code into Episteme assertions
//! that can be ingested into the knowledge graph.
//!
//! ## Git Commit Tracking
//!
//! Git commit hashes are captured at **assertion-creation time** (during ingestion),
//! not when claims are authored in TOML files. This avoids the "double-commit problem":
//!
//! 1. User authors claim in `.aphoria/claims.toml` (no git hash)
//! 2. User commits the TOML file → creates commit A
//! 3. Aphoria ingests claims → captures commit A's hash in assertions
//!
//! If we stored git hashes in TOML, the act of committing the TOML would invalidate
//! its own stored hash, creating an infinite loop. By capturing at ingestion time,
//! the hash in `source_metadata` reflects the actual code state when the claim was
//! converted to an assertion.

use blake3::Hasher;
use ed25519_dalek::{Signer, SigningKey};
use stemedb_core::types::{
    Assertion, Hash, HlcTimestamp, LifecycleStage, SignatureEntry, SourceClass,
};
use tracing::instrument;

use crate::types::authored_claim::ComparisonMode;
use crate::types::{parse_authority_tier, AuthoredClaim, AuthoredValue, ClaimStatus, Observation};

/// Convert an Observation to an Episteme Assertion.
///
/// The assertion is signed with the provided keypair and timestamped.
/// Uses `SourceClass::Expert` (Tier 3) for code-extracted claims.
#[instrument(skip(signing_key), fields(concept_path = %claim.concept_path, predicate = %claim.predicate))]
pub fn claim_to_assertion(
    claim: &Observation,
    signing_key: &SigningKey,
    timestamp: u64,
    git_commit: Option<&str>,
) -> Assertion {
    claim_to_assertion_with_tier(claim, signing_key, timestamp, SourceClass::Expert, git_commit)
}

/// Map observation confidence to appropriate tier.
///
/// Observations (extracted patterns) are assigned tiers based on confidence:
/// - High confidence (≥0.9): Tier 4 (Community, weight 0.3)
/// - Low confidence (<0.9): Tier 5 (Anecdotal, weight 0.1)
///
/// This is different from authored claims which use explicit authority tiers.
pub fn observation_to_tier(confidence: f32) -> SourceClass {
    if confidence >= 0.9 {
        SourceClass::Community // Tier 4
    } else {
        SourceClass::Anecdotal // Tier 5
    }
}

/// Convert an observation (Observation) to an Episteme Assertion.
///
/// Observations are pattern matches from extractors. Unlike authored claims,
/// they lack provenance and consequences. The tier is determined by confidence:
/// - High confidence (≥0.9) → Tier 4 (Community, 0.3 weight)
/// - Low confidence (<0.9) → Tier 5 (Anecdotal, 0.1 weight)
///
/// This replaces the fixed Tier 3 mapping previously used for code extractions.
#[instrument(skip(signing_key), fields(concept_path = %claim.concept_path, predicate = %claim.predicate, confidence = %claim.confidence))]
pub fn observation_to_assertion(
    claim: &Observation,
    signing_key: &SigningKey,
    timestamp: u64,
    git_commit: Option<&str>,
) -> Assertion {
    let tier = observation_to_tier(claim.confidence);
    claim_to_assertion_with_tier(claim, signing_key, timestamp, tier, git_commit)
}

/// Convert an Observation to a Tier 4 (Community) observation.
///
/// **Deprecated:** Use `observation_to_assertion()` which maps confidence to tier.
///
/// Used for claims that have no authority conflict — these become "project memory"
/// that persists across commits and enables future drift detection.
///
/// Observations are lower-weight assertions (Tier 4, 0.3 authority weight) that
/// record what the code actually does without making authoritative claims.
#[deprecated(
    since = "0.9.0",
    note = "Use observation_to_assertion() for confidence-based tier mapping"
)]
#[instrument(skip(signing_key), fields(concept_path = %claim.concept_path, predicate = %claim.predicate))]
pub fn claim_to_observation(
    claim: &Observation,
    signing_key: &SigningKey,
    timestamp: u64,
) -> Assertion {
    claim_to_assertion_with_tier(claim, signing_key, timestamp, SourceClass::Community, None)
}

/// Internal helper to create assertions with a specific source class.
fn claim_to_assertion_with_tier(
    claim: &Observation,
    signing_key: &SigningKey,
    timestamp: u64,
    source_class: SourceClass,
    git_commit: Option<&str>,
) -> Assertion {
    // Build source metadata
    let mut metadata = serde_json::json!({
        "file": claim.file,
        "line": claim.line,
        "matched_text": claim.matched_text,
        "scan_tool": "aphoria",
        "scan_version": env!("CARGO_PKG_VERSION"),
    });

    // Add git commit if available
    if let Some(hash) = git_commit {
        metadata["git_commit"] = serde_json::json!(hash);
    }

    let source_metadata = metadata;

    // Compute source hash from file:line:matched_text
    let source_hash = compute_source_hash(&claim.file, claim.line, &claim.matched_text);

    // Create signature (version 1: signs subject:predicate)
    let message = format!("{}:{}", claim.concept_path, claim.predicate);
    let signature = signing_key.sign(message.as_bytes());
    let verifying_key = signing_key.verifying_key();

    let signature_entry = SignatureEntry {
        agent_id: verifying_key.to_bytes(),
        signature: signature.to_bytes(),
        timestamp,
        version: 1,
    };

    Assertion {
        subject: claim.concept_path.clone(),
        predicate: claim.predicate.clone(),
        object: claim.value.clone(),
        parent_hash: None,
        source_hash,
        source_class,
        visual_hash: None,
        epoch: None,
        source_metadata: serde_json::to_vec(&source_metadata).ok(),
        narrative: None,
        lifecycle: LifecycleStage::Approved,
        signatures: vec![signature_entry],
        confidence: claim.confidence,
        timestamp,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    }
}

/// Convert an `AuthoredClaim` to an Episteme Assertion.
///
/// Unlike extractor-produced assertions, authored claims carry full provenance:
/// - `source_class` is derived from the claim's `authority_tier` field
/// - `source_metadata` includes provenance, invariant, consequence, and evidence
/// - `parent_hash` is computed from the `supersedes` field if present
/// - `lifecycle` is `Approved` (authored claims are already reviewed)
#[instrument(skip(signing_key), fields(id = %claim.id, concept_path = %claim.concept_path))]
pub fn authored_claim_to_assertion(
    claim: &AuthoredClaim,
    signing_key: &SigningKey,
    timestamp: u64,
    git_commit: Option<&str>,
) -> Result<Assertion, crate::AphoriaError> {
    let source_class = parse_authority_tier(&claim.authority_tier)?;

    let mut metadata = serde_json::json!({
        "authored": true,
        "claim_id": claim.id,
        "provenance": claim.provenance,
        "invariant": claim.invariant,
        "consequence": claim.consequence,
        "evidence": claim.evidence,
        "category": claim.category,
        "comparison": claim.comparison.to_string(),
        "status": claim.status.to_string(),
        "created_by": claim.created_by,
        "created_at": claim.created_at,
        "tool": "aphoria",
        "tool_version": env!("CARGO_PKG_VERSION"),
    });

    // Add git commit if available
    if let Some(hash) = git_commit {
        metadata["git_commit"] = serde_json::json!(hash);
    }

    // Add supersedes if present
    if let Some(ref supersedes) = claim.supersedes {
        metadata["supersedes"] = serde_json::json!(supersedes);
    }

    // Add updated_at if present
    if let Some(ref updated_at) = claim.updated_at {
        metadata["updated_at"] = serde_json::json!(updated_at);
    }

    let source_metadata = metadata;

    // Source hash from claim ID (stable, deterministic)
    let source_hash = compute_authored_claim_hash(&claim.id);

    // Compute parent hash from superseded claim ID if present
    let parent_hash = claim.supersedes.as_ref().map(|sid| compute_authored_claim_hash(sid));

    // Map ClaimStatus → LifecycleStage
    let lifecycle = claim_status_to_lifecycle(&claim.status);

    // Sign subject:predicate
    let message = format!("{}:{}", claim.concept_path, claim.predicate);
    let signature = signing_key.sign(message.as_bytes());
    let verifying_key = signing_key.verifying_key();

    let signature_entry = SignatureEntry {
        agent_id: verifying_key.to_bytes(),
        signature: signature.to_bytes(),
        timestamp,
        version: 1,
    };

    Ok(Assertion {
        subject: claim.concept_path.clone(),
        predicate: claim.predicate.clone(),
        object: claim.value.to_object_value(),
        parent_hash,
        source_hash,
        source_class,
        visual_hash: None,
        epoch: None,
        source_metadata: serde_json::to_vec(&source_metadata).ok(),
        narrative: None,
        lifecycle,
        signatures: vec![signature_entry],
        confidence: 1.0, // Authored claims have full confidence
        timestamp,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    })
}

/// Map `ClaimStatus` to `LifecycleStage`.
fn claim_status_to_lifecycle(status: &ClaimStatus) -> LifecycleStage {
    match status {
        ClaimStatus::Draft => LifecycleStage::Proposed,
        ClaimStatus::Active => LifecycleStage::Approved,
        ClaimStatus::Deprecated => LifecycleStage::Deprecated,
        ClaimStatus::Superseded => LifecycleStage::Deprecated,
    }
}

/// Map `LifecycleStage` back to `ClaimStatus`.
///
/// Uses `has_parent` to distinguish `Superseded` from `Deprecated`
/// (both map to `LifecycleStage::Deprecated`).
fn lifecycle_to_claim_status(lifecycle: LifecycleStage, has_parent: bool) -> ClaimStatus {
    match lifecycle {
        LifecycleStage::Proposed => ClaimStatus::Draft,
        LifecycleStage::UnderReview => ClaimStatus::Draft,
        LifecycleStage::Approved => ClaimStatus::Active,
        LifecycleStage::Deprecated => {
            if has_parent {
                ClaimStatus::Superseded
            } else {
                ClaimStatus::Deprecated
            }
        }
        LifecycleStage::Rejected => ClaimStatus::Deprecated,
    }
}

/// Map a `SourceClass` back to the authority tier string.
fn source_class_to_tier_string(source_class: SourceClass) -> String {
    match source_class {
        SourceClass::Regulatory => "regulatory".to_string(),
        SourceClass::Clinical => "clinical".to_string(),
        SourceClass::Observational => "observational".to_string(),
        SourceClass::TeamPolicy => "team_policy".to_string(),
        SourceClass::Expert => "expert".to_string(),
        SourceClass::Community => "community".to_string(),
        SourceClass::Anecdotal => "anecdotal".to_string(),
    }
}

/// Convert an Episteme Assertion back to an `AuthoredClaim`.
///
/// Recovers all fields from `source_metadata` JSON. Returns an error if the
/// assertion was not created from an authored claim (missing `authored: true`
/// in metadata).
pub fn assertion_to_authored_claim(
    assertion: &Assertion,
) -> Result<AuthoredClaim, crate::AphoriaError> {
    let metadata_bytes = assertion.source_metadata.as_ref().ok_or_else(|| {
        crate::AphoriaError::Claims("Assertion has no source_metadata".to_string())
    })?;

    let metadata: serde_json::Value = serde_json::from_slice(metadata_bytes).map_err(|e| {
        crate::AphoriaError::Claims(format!("Failed to parse source_metadata JSON: {e}"))
    })?;

    // Verify this is an authored claim assertion
    if metadata.get("authored") != Some(&serde_json::Value::Bool(true)) {
        return Err(crate::AphoriaError::Claims(
            "Assertion is not from an authored claim (missing authored: true)".to_string(),
        ));
    }

    let claim_id = metadata["claim_id"].as_str().unwrap_or("").to_string();

    let provenance = metadata["provenance"].as_str().unwrap_or("").to_string();

    let invariant = metadata["invariant"].as_str().unwrap_or("").to_string();

    let consequence = metadata["consequence"].as_str().unwrap_or("").to_string();

    let evidence: Vec<String> = metadata["evidence"]
        .as_array()
        .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
        .unwrap_or_default();

    let category = metadata["category"].as_str().unwrap_or("").to_string();

    let comparison = metadata
        .get("comparison")
        .and_then(|v| v.as_str())
        .map(|s| match s {
            "equals" => ComparisonMode::Equals,
            "not_equals" => ComparisonMode::NotEquals,
            "present" => ComparisonMode::Present,
            "absent" => ComparisonMode::Absent,
            "contains" => ComparisonMode::Contains,
            "not_contains" => ComparisonMode::NotContains,
            _ => ComparisonMode::default(),
        })
        .unwrap_or_default();

    // Recover status from metadata, falling back to lifecycle mapping
    let status = metadata
        .get("status")
        .and_then(|v| v.as_str())
        .and_then(|s| ClaimStatus::parse(s).ok())
        .unwrap_or_else(|| {
            lifecycle_to_claim_status(assertion.lifecycle, assertion.parent_hash.is_some())
        });

    let created_by = metadata["created_by"].as_str().unwrap_or("").to_string();

    let created_at = metadata["created_at"].as_str().unwrap_or("").to_string();

    let updated_at = metadata.get("updated_at").and_then(|v| v.as_str()).map(|s| s.to_string());

    let supersedes = metadata.get("supersedes").and_then(|v| v.as_str()).map(|s| s.to_string());

    // Convert ObjectValue back to AuthoredValue
    let value = match &assertion.object {
        stemedb_core::types::ObjectValue::Boolean(b) => AuthoredValue::Bool(*b),
        stemedb_core::types::ObjectValue::Number(n) => AuthoredValue::Number(*n),
        stemedb_core::types::ObjectValue::Text(s) => AuthoredValue::Text(s.clone()),
        stemedb_core::types::ObjectValue::Reference(r) => AuthoredValue::Text(r.clone()),
    };

    // Map source_class back to authority tier string
    let authority_tier = source_class_to_tier_string(assertion.source_class);

    Ok(AuthoredClaim {
        id: claim_id,
        concept_path: assertion.subject.clone(),
        predicate: assertion.predicate.clone(),
        value,
        comparison,
        provenance,
        invariant,
        consequence,
        authority_tier,
        evidence,
        category,
        status,
        supersedes,
        created_by,
        created_at,
        updated_at,
    })
}

/// Compute a deterministic hash from an authored claim ID.
fn compute_authored_claim_hash(claim_id: &str) -> Hash {
    let mut hasher = Hasher::new();
    hasher.update(b"authored-claim:");
    hasher.update(claim_id.as_bytes());
    *hasher.finalize().as_bytes()
}

/// Compute the content hash of an assertion for deduplication.
#[allow(dead_code)]
pub fn compute_assertion_hash(assertion: &Assertion) -> Hash {
    let mut hasher = Hasher::new();
    hasher.update(assertion.subject.as_bytes());
    hasher.update(assertion.predicate.as_bytes());
    hasher.update(format!("{:?}", assertion.object).as_bytes());
    hasher.update(&assertion.source_hash);
    hasher.update(&[assertion.source_class.tier()]);
    *hasher.finalize().as_bytes()
}

/// Compute the source hash from file location and matched text.
fn compute_source_hash(file: &str, line: usize, matched_text: &str) -> Hash {
    let mut hasher = Hasher::new();
    hasher.update(file.as_bytes());
    hasher.update(&line.to_le_bytes());
    hasher.update(matched_text.as_bytes());
    *hasher.finalize().as_bytes()
}

/// Generate a new Ed25519 signing key for an Aphoria agent.
pub fn generate_signing_key() -> SigningKey {
    use rand::rngs::OsRng;
    SigningKey::generate(&mut OsRng)
}

/// Load or generate the project's signing key.
///
/// The key is stored at `.aphoria/agent.key` in the project root.
pub fn load_or_generate_key(project_root: &std::path::Path) -> std::io::Result<SigningKey> {
    let aphoria_dir = project_root.join(".aphoria");
    let key_path = aphoria_dir.join("agent.key");

    if key_path.exists() {
        load_key_from_file(&key_path)
    } else {
        // Generate new key
        let key = generate_signing_key();
        std::fs::create_dir_all(&aphoria_dir)?;
        std::fs::write(&key_path, key.to_bytes())?;
        Ok(key)
    }
}

/// Load a signing key from a specific file path.
///
/// Returns an error if the file doesn't exist or contains invalid data.
pub fn load_key_from_file(key_path: &std::path::Path) -> std::io::Result<SigningKey> {
    let key_bytes = std::fs::read(key_path)?;
    if key_bytes.len() == 32 {
        let mut arr = [0u8; 32];
        arr.copy_from_slice(&key_bytes);
        Ok(SigningKey::from_bytes(&arr))
    } else {
        Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("Invalid key file: expected 32 bytes, got {}", key_bytes.len()),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use stemedb_core::types::ObjectValue;

    #[test]
    fn test_claim_to_assertion() {
        let claim = Observation {
            concept_path: "code://rust/myapp/tls/cert_verification".to_string(),
            predicate: "enabled".to_string(),
            value: ObjectValue::Boolean(false),
            file: "src/client.rs".to_string(),
            line: 42,
            matched_text: "danger_accept_invalid_certs(true)".to_string(), // aphoria:ignore - Test fixture
            confidence: 1.0,
            description: "TLS verification disabled".to_string(),
        };

        let key = generate_signing_key();
        let timestamp = 1706832000;

        let assertion = claim_to_assertion(&claim, &key, timestamp, None);

        assert_eq!(assertion.subject, claim.concept_path);
        assert_eq!(assertion.predicate, "enabled");
        assert_eq!(assertion.object, ObjectValue::Boolean(false));
        assert_eq!(assertion.source_class, SourceClass::Expert);
        assert_eq!(assertion.confidence, 1.0);
        assert_eq!(assertion.timestamp, timestamp);
        assert!(!assertion.signatures.is_empty());
    }

    #[test]
    fn test_observation_to_tier_high_confidence() {
        assert_eq!(observation_to_tier(1.0), SourceClass::Community);
        assert_eq!(observation_to_tier(0.95), SourceClass::Community);
        assert_eq!(observation_to_tier(0.9), SourceClass::Community);
        assert_eq!(observation_to_tier(0.9).tier(), 4);
        assert!((observation_to_tier(0.9).authority_weight() - 0.3).abs() < f32::EPSILON);
    }

    #[test]
    fn test_observation_to_tier_low_confidence() {
        assert_eq!(observation_to_tier(0.89), SourceClass::Anecdotal);
        assert_eq!(observation_to_tier(0.5), SourceClass::Anecdotal);
        assert_eq!(observation_to_tier(0.1), SourceClass::Anecdotal);
        assert_eq!(observation_to_tier(0.5).tier(), 5);
        assert!((observation_to_tier(0.5).authority_weight() - 0.1).abs() < f32::EPSILON);
    }

    #[test]
    fn test_observation_to_assertion_high_confidence() {
        let observation = Observation {
            concept_path: "code://rust/myapp/tls/cert_verification".to_string(),
            predicate: "enabled".to_string(),
            value: ObjectValue::Boolean(true),
            file: "src/client.rs".to_string(),
            line: 42,
            matched_text: "verify_certs = true".to_string(),
            confidence: 0.95,
            description: "TLS verification enabled".to_string(),
        };

        let key = generate_signing_key();
        let assertion = observation_to_assertion(&observation, &key, 1706832000, None);

        assert_eq!(assertion.source_class, SourceClass::Community); // Tier 4
        assert_eq!(assertion.source_class.tier(), 4);
        assert!((assertion.source_class.authority_weight() - 0.3).abs() < f32::EPSILON);
        assert_eq!(assertion.confidence, 0.95);
    }

    #[test]
    fn test_observation_to_assertion_low_confidence() {
        let observation = Observation {
            concept_path: "code://rust/myapp/config/timeout".to_string(),
            predicate: "value".to_string(),
            value: ObjectValue::Number(30.0),
            file: "src/config.rs".to_string(),
            line: 15,
            matched_text: "timeout = 30".to_string(),
            confidence: 0.7,
            description: "Timeout configuration".to_string(),
        };

        let key = generate_signing_key();
        let assertion = observation_to_assertion(&observation, &key, 1706832000, None);

        assert_eq!(assertion.source_class, SourceClass::Anecdotal); // Tier 5
        assert_eq!(assertion.source_class.tier(), 5);
        assert!((assertion.source_class.authority_weight() - 0.1).abs() < f32::EPSILON);
        assert_eq!(assertion.confidence, 0.7);
    }

    #[test]
    #[allow(deprecated)]
    fn test_claim_to_observation_sets_tier4() {
        let claim = Observation {
            concept_path: "code://rust/myapp/logging/level".to_string(),
            predicate: "value".to_string(),
            value: ObjectValue::Text("debug".to_string()),
            file: "src/config.rs".to_string(),
            line: 15,
            matched_text: "log_level = \"debug\"".to_string(),
            confidence: 0.9,
            description: "Logging level set to debug".to_string(),
        };

        let key = generate_signing_key();
        let timestamp = 1706832000;

        let observation = claim_to_observation(&claim, &key, timestamp);

        assert_eq!(observation.subject, claim.concept_path);
        assert_eq!(observation.predicate, "value");
        assert_eq!(observation.object, ObjectValue::Text("debug".to_string()));
        assert_eq!(observation.source_class, SourceClass::Community); // Tier 4
        assert_eq!(observation.source_class.tier(), 4);
        assert!((observation.source_class.authority_weight() - 0.3).abs() < f32::EPSILON);
        assert_eq!(observation.confidence, 0.9);
        assert_eq!(observation.timestamp, timestamp);
        assert!(!observation.signatures.is_empty());
    }

    #[test]
    #[allow(deprecated)]
    fn test_claim_to_observation_preserves_metadata() {
        let claim = Observation {
            concept_path: "code://rust/myapp/db/pool_size".to_string(),
            predicate: "value".to_string(),
            value: ObjectValue::Number(10.0),
            file: "src/db/config.rs".to_string(),
            line: 42,
            matched_text: "pool_size = 10".to_string(),
            confidence: 1.0,
            description: "Database pool size".to_string(),
        };

        let key = generate_signing_key();
        let timestamp = 1706832000;

        let observation = claim_to_observation(&claim, &key, timestamp);

        // Verify source metadata is preserved
        let metadata: serde_json::Value =
            serde_json::from_slice(observation.source_metadata.as_ref().expect("metadata"))
                .expect("parse");

        assert_eq!(metadata["file"], "src/db/config.rs");
        assert_eq!(metadata["line"], 42);
        assert_eq!(metadata["matched_text"], "pool_size = 10");
        assert_eq!(metadata["scan_tool"], "aphoria");
    }

    #[test]
    fn test_assertion_hash_deterministic() {
        let claim = Observation {
            concept_path: "code://rust/myapp/tls/cert_verification".to_string(),
            predicate: "enabled".to_string(),
            value: ObjectValue::Boolean(false),
            file: "src/client.rs".to_string(),
            line: 42,
            matched_text: "danger_accept_invalid_certs(true)".to_string(), // aphoria:ignore - Test fixture
            confidence: 1.0,
            description: "TLS verification disabled".to_string(),
        };

        let key = generate_signing_key();
        let assertion1 = claim_to_assertion(&claim, &key, 1000, None);
        let assertion2 = claim_to_assertion(&claim, &key, 1000, None);

        let hash1 = compute_assertion_hash(&assertion1);
        let hash2 = compute_assertion_hash(&assertion2);

        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_load_or_generate_key() {
        let temp_dir = tempfile::tempdir().expect("create temp dir");
        let key1 = load_or_generate_key(temp_dir.path()).expect("generate key");
        let key2 = load_or_generate_key(temp_dir.path()).expect("load key");

        // Same key should be loaded
        assert_eq!(key1.to_bytes(), key2.to_bytes());
    }

    #[test]
    fn test_authored_claim_to_assertion() {
        use crate::types::authored_claim::{AuthoredValue, ClaimStatus};

        let claim = AuthoredClaim {
            id: "wallet-seqcst-001".to_string(),
            concept_path: "maxwell/wallet/atomics/ordering".to_string(),
            predicate: "required_ordering".to_string(),
            value: AuthoredValue::Text("SeqCst".to_string()),
            comparison: ComparisonMode::Equals,
            provenance: "Safety analysis".to_string(),
            invariant: "All wallet atomics MUST use SeqCst".to_string(),
            consequence: "Double-spend race condition".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec!["ADR-003".to_string()],
            category: "safety".to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "jml".to_string(),
            created_at: "2026-02-08T12:00:00Z".to_string(),
            updated_at: None,
        };

        let key = generate_signing_key();
        let assertion =
            authored_claim_to_assertion(&claim, &key, 1706832000, None).expect("convert");

        assert_eq!(assertion.subject, "maxwell/wallet/atomics/ordering");
        assert_eq!(assertion.predicate, "required_ordering");
        assert_eq!(assertion.object, ObjectValue::Text("SeqCst".to_string()));
        assert_eq!(assertion.source_class, SourceClass::Expert);
        assert_eq!(assertion.confidence, 1.0);
        assert!(assertion.parent_hash.is_none());
        assert_eq!(assertion.lifecycle, LifecycleStage::Approved);

        // Verify metadata includes provenance fields + comparison + status
        let metadata: serde_json::Value =
            serde_json::from_slice(assertion.source_metadata.as_ref().expect("metadata"))
                .expect("parse");
        assert_eq!(metadata["authored"], true);
        assert_eq!(metadata["claim_id"], "wallet-seqcst-001");
        assert_eq!(metadata["provenance"], "Safety analysis");
        assert_eq!(metadata["invariant"], "All wallet atomics MUST use SeqCst");
        assert_eq!(metadata["comparison"], "equals");
        assert_eq!(metadata["status"], "active");
    }

    #[test]
    fn test_authored_claim_with_supersedes() {
        use crate::types::authored_claim::{AuthoredValue, ClaimStatus};

        let claim = AuthoredClaim {
            id: "wallet-ordering-v2".to_string(),
            concept_path: "maxwell/wallet/atomics/ordering".to_string(),
            predicate: "required_ordering".to_string(),
            value: AuthoredValue::Text("Acquire".to_string()),
            comparison: Default::default(),
            provenance: "Updated safety analysis".to_string(),
            invariant: "Wallet atomics should use Acquire".to_string(),
            consequence: "Performance degradation".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec![],
            category: "safety".to_string(),
            status: ClaimStatus::Active,
            supersedes: Some("wallet-seqcst-001".to_string()),
            created_by: "jml".to_string(),
            created_at: "2026-02-08T13:00:00Z".to_string(),
            updated_at: None,
        };

        let key = generate_signing_key();
        let assertion =
            authored_claim_to_assertion(&claim, &key, 1706832000, None).expect("convert");

        assert!(assertion.parent_hash.is_some());
    }

    #[test]
    fn test_observation_with_git_commit() {
        let observation = Observation {
            concept_path: "code://rust/myapp/feature/enabled".to_string(),
            predicate: "value".to_string(),
            value: ObjectValue::Boolean(true),
            file: "src/config.rs".to_string(),
            line: 10,
            matched_text: "enabled = true".to_string(),
            confidence: 1.0,
            description: "Feature enabled".to_string(),
        };

        let key = generate_signing_key();
        let git_commit = Some("a3f8c2d1b9e7f6a5d4c3b2a1098765432100abcd");
        let assertion = observation_to_assertion(&observation, &key, 1706832000, git_commit);

        // Verify git_commit is in source_metadata
        let metadata: serde_json::Value =
            serde_json::from_slice(assertion.source_metadata.as_ref().expect("metadata"))
                .expect("parse");

        assert_eq!(metadata["git_commit"], "a3f8c2d1b9e7f6a5d4c3b2a1098765432100abcd");
        assert_eq!(metadata["file"], "src/config.rs");
        assert_eq!(metadata["scan_tool"], "aphoria");
    }

    #[test]
    fn test_observation_without_git_commit() {
        let observation = Observation {
            concept_path: "code://rust/myapp/feature/enabled".to_string(),
            predicate: "value".to_string(),
            value: ObjectValue::Boolean(true),
            file: "src/config.rs".to_string(),
            line: 10,
            matched_text: "enabled = true".to_string(),
            confidence: 1.0,
            description: "Feature enabled".to_string(),
        };

        let key = generate_signing_key();
        let assertion = observation_to_assertion(&observation, &key, 1706832000, None);

        // Verify git_commit is NOT in source_metadata
        let metadata: serde_json::Value =
            serde_json::from_slice(assertion.source_metadata.as_ref().expect("metadata"))
                .expect("parse");

        assert!(metadata.get("git_commit").is_none());
        assert_eq!(metadata["file"], "src/config.rs");
    }

    #[test]
    fn test_authored_claim_with_git_commit() {
        use crate::types::authored_claim::{AuthoredValue, ClaimStatus};

        let claim = AuthoredClaim {
            id: "test-git-001".to_string(),
            concept_path: "maxwell/test/pattern".to_string(),
            predicate: "enabled".to_string(),
            value: AuthoredValue::Bool(true),
            comparison: Default::default(),
            provenance: "Test with git hash".to_string(),
            invariant: "Pattern must be enabled".to_string(),
            consequence: "Test failure".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: "2026-02-08T12:00:00Z".to_string(),
            updated_at: None,
        };

        let key = generate_signing_key();
        let git_commit = Some("abc123def456789");
        let assertion =
            authored_claim_to_assertion(&claim, &key, 1706832000, git_commit).expect("convert");

        // Verify git_commit is in source_metadata
        let metadata: serde_json::Value =
            serde_json::from_slice(assertion.source_metadata.as_ref().expect("metadata"))
                .expect("parse");

        assert_eq!(metadata["git_commit"], "abc123def456789");
        assert_eq!(metadata["authored"], true);
        assert_eq!(metadata["claim_id"], "test-git-001");
    }

    #[test]
    fn test_authored_claim_invalid_tier() {
        use crate::types::authored_claim::{AuthoredValue, ClaimStatus};

        let claim = AuthoredClaim {
            id: "bad-tier".to_string(),
            concept_path: "test/path".to_string(),
            predicate: "test".to_string(),
            value: AuthoredValue::Bool(true),
            comparison: Default::default(),
            provenance: "test".to_string(),
            invariant: "test".to_string(),
            consequence: "test".to_string(),
            authority_tier: "invalid_tier".to_string(),
            evidence: vec![],
            category: "safety".to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "test".to_string(),
            created_at: "2026-02-08T12:00:00Z".to_string(),
            updated_at: None,
        };

        let key = generate_signing_key();
        assert!(authored_claim_to_assertion(&claim, &key, 1706832000, None).is_err());
    }

    #[test]
    fn test_claim_status_to_lifecycle() {
        assert_eq!(claim_status_to_lifecycle(&ClaimStatus::Draft), LifecycleStage::Proposed);
        assert_eq!(claim_status_to_lifecycle(&ClaimStatus::Active), LifecycleStage::Approved);
        assert_eq!(claim_status_to_lifecycle(&ClaimStatus::Deprecated), LifecycleStage::Deprecated);
        assert_eq!(claim_status_to_lifecycle(&ClaimStatus::Superseded), LifecycleStage::Deprecated);
    }

    #[test]
    fn test_lifecycle_to_claim_status() {
        assert_eq!(lifecycle_to_claim_status(LifecycleStage::Proposed, false), ClaimStatus::Draft);
        assert_eq!(
            lifecycle_to_claim_status(LifecycleStage::UnderReview, false),
            ClaimStatus::Draft
        );
        assert_eq!(lifecycle_to_claim_status(LifecycleStage::Approved, false), ClaimStatus::Active);
        assert_eq!(
            lifecycle_to_claim_status(LifecycleStage::Deprecated, false),
            ClaimStatus::Deprecated
        );
        assert_eq!(
            lifecycle_to_claim_status(LifecycleStage::Deprecated, true),
            ClaimStatus::Superseded
        );
        assert_eq!(
            lifecycle_to_claim_status(LifecycleStage::Rejected, false),
            ClaimStatus::Deprecated
        );
    }

    #[test]
    fn test_authored_claim_draft_lifecycle() {
        use crate::types::authored_claim::AuthoredValue;

        let claim = AuthoredClaim {
            id: "draft-001".to_string(),
            concept_path: "test/draft".to_string(),
            predicate: "enabled".to_string(),
            value: AuthoredValue::Bool(true),
            comparison: Default::default(),
            provenance: "test".to_string(),
            invariant: "test".to_string(),
            consequence: "test".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec![],
            category: "test".to_string(),
            status: ClaimStatus::Draft,
            supersedes: None,
            created_by: "test".to_string(),
            created_at: "2026-02-12T00:00:00Z".to_string(),
            updated_at: None,
        };

        let key = generate_signing_key();
        let assertion =
            authored_claim_to_assertion(&claim, &key, 1706832000, None).expect("convert");
        assert_eq!(assertion.lifecycle, LifecycleStage::Proposed);
    }

    #[test]
    fn test_authored_claim_deprecated_lifecycle() {
        use crate::types::authored_claim::AuthoredValue;

        let claim = AuthoredClaim {
            id: "dep-001".to_string(),
            concept_path: "test/deprecated".to_string(),
            predicate: "enabled".to_string(),
            value: AuthoredValue::Bool(true),
            comparison: Default::default(),
            provenance: "test".to_string(),
            invariant: "test".to_string(),
            consequence: "test".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec![],
            category: "test".to_string(),
            status: ClaimStatus::Deprecated,
            supersedes: None,
            created_by: "test".to_string(),
            created_at: "2026-02-12T00:00:00Z".to_string(),
            updated_at: None,
        };

        let key = generate_signing_key();
        let assertion =
            authored_claim_to_assertion(&claim, &key, 1706832000, None).expect("convert");
        assert_eq!(assertion.lifecycle, LifecycleStage::Deprecated);
    }

    #[test]
    fn test_assertion_to_authored_claim_round_trip() {
        use crate::types::authored_claim::AuthoredValue;

        let original = AuthoredClaim {
            id: "round-trip-001".to_string(),
            concept_path: "maxwell/wallet/atomics/ordering".to_string(),
            predicate: "required_ordering".to_string(),
            value: AuthoredValue::Text("SeqCst".to_string()),
            comparison: ComparisonMode::Absent,
            provenance: "Safety analysis".to_string(),
            invariant: "All wallet atomics MUST use SeqCst".to_string(),
            consequence: "Double-spend race condition".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec!["ADR-003".to_string(), "Intel SDM".to_string()],
            category: "safety".to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "jml".to_string(),
            created_at: "2026-02-08T12:00:00Z".to_string(),
            updated_at: Some("2026-02-10T12:00:00Z".to_string()),
        };

        let key = generate_signing_key();
        let assertion =
            authored_claim_to_assertion(&original, &key, 1706832000, None).expect("convert");
        let recovered = assertion_to_authored_claim(&assertion).expect("reverse");

        assert_eq!(recovered.id, original.id);
        assert_eq!(recovered.concept_path, original.concept_path);
        assert_eq!(recovered.predicate, original.predicate);
        assert_eq!(recovered.value, original.value);
        assert_eq!(recovered.comparison, original.comparison);
        assert_eq!(recovered.provenance, original.provenance);
        assert_eq!(recovered.invariant, original.invariant);
        assert_eq!(recovered.consequence, original.consequence);
        assert_eq!(recovered.authority_tier, original.authority_tier);
        assert_eq!(recovered.evidence, original.evidence);
        assert_eq!(recovered.category, original.category);
        assert_eq!(recovered.status, original.status);
        assert_eq!(recovered.supersedes, original.supersedes);
        assert_eq!(recovered.created_by, original.created_by);
        assert_eq!(recovered.created_at, original.created_at);
        assert_eq!(recovered.updated_at, original.updated_at);
    }

    #[test]
    fn test_assertion_to_authored_claim_with_supersedes() {
        use crate::types::authored_claim::AuthoredValue;

        let original = AuthoredClaim {
            id: "v2-001".to_string(),
            concept_path: "test/path".to_string(),
            predicate: "enabled".to_string(),
            value: AuthoredValue::Bool(true),
            comparison: ComparisonMode::Present,
            provenance: "Updated analysis".to_string(),
            invariant: "Must be present".to_string(),
            consequence: "Breaks things".to_string(),
            authority_tier: "community".to_string(),
            evidence: vec![],
            category: "architecture".to_string(),
            status: ClaimStatus::Active,
            supersedes: Some("v1-001".to_string()),
            created_by: "tester".to_string(),
            created_at: "2026-02-12T00:00:00Z".to_string(),
            updated_at: None,
        };

        let key = generate_signing_key();
        let assertion =
            authored_claim_to_assertion(&original, &key, 1706832000, None).expect("convert");
        let recovered = assertion_to_authored_claim(&assertion).expect("reverse");

        assert_eq!(recovered.supersedes, Some("v1-001".to_string()));
        assert_eq!(recovered.authority_tier, "community");
        assert_eq!(recovered.comparison, ComparisonMode::Present);
    }

    #[test]
    fn test_assertion_to_authored_claim_rejects_non_authored() {
        // Create a plain observation assertion (no "authored: true" metadata)
        let observation = Observation {
            concept_path: "code://test".to_string(),
            predicate: "enabled".to_string(),
            value: ObjectValue::Boolean(true),
            file: "test.rs".to_string(),
            line: 1,
            matched_text: "test".to_string(),
            confidence: 1.0,
            description: "test".to_string(),
        };

        let key = generate_signing_key();
        let assertion = claim_to_assertion(&observation, &key, 1706832000, None);

        let result = assertion_to_authored_claim(&assertion);
        assert!(result.is_err());
    }

    #[test]
    fn test_assertion_to_authored_claim_all_statuses() {
        use crate::types::authored_claim::AuthoredValue;

        let key = generate_signing_key();

        for status in [
            ClaimStatus::Draft,
            ClaimStatus::Active,
            ClaimStatus::Deprecated,
            ClaimStatus::Superseded,
        ] {
            let claim = AuthoredClaim {
                id: format!("status-{}", status),
                concept_path: "test/status".to_string(),
                predicate: "check".to_string(),
                value: AuthoredValue::Bool(true),
                comparison: Default::default(),
                provenance: "test".to_string(),
                invariant: "test".to_string(),
                consequence: "test".to_string(),
                authority_tier: "expert".to_string(),
                evidence: vec![],
                category: "test".to_string(),
                status: status.clone(),
                supersedes: if status == ClaimStatus::Superseded {
                    Some("old-001".to_string())
                } else {
                    None
                },
                created_by: "test".to_string(),
                created_at: "2026-02-12T00:00:00Z".to_string(),
                updated_at: None,
            };

            let assertion =
                authored_claim_to_assertion(&claim, &key, 1706832000, None).expect("convert");
            let recovered = assertion_to_authored_claim(&assertion).expect("reverse");
            assert_eq!(recovered.status, status, "Status round-trip failed for {:?}", status);
        }
    }
}
