//! Tests for predicate alias persistence and index normalization (Phase 6.5.3).
//!
//! These tests verify:
//! 1. Predicate aliases are persisted to storage during policy import
//! 2. Predicate aliases are loaded from storage on restart
//! 3. Index normalization uses persisted aliases for conflict detection

use crate::*;

/// Test that predicate aliases are persisted during policy import.
#[tokio::test]
async fn test_predicate_alias_persistence_during_import() {
    let temp_dir = tempfile::Builder::new()
        .prefix("aphoria_alias_persist")
        .tempdir()
        .expect("create temp dir");

    // Create .aphoria directory for the agent key
    let aphoria_dir = temp_dir.path().join(".aphoria");
    std::fs::create_dir_all(&aphoria_dir).expect("create .aphoria dir");

    // Create a Trust Pack with predicate aliases
    let signing_key = crate::bridge::load_or_generate_key(temp_dir.path()).expect("load key");

    // Create a simple assertion
    let claim = ExtractedClaim {
        concept_path: "rfc://test/tls/cert".to_string(),
        predicate: "required".to_string(), // Note: uses alias, not canonical
        value: stemedb_core::types::ObjectValue::Boolean(true),
        file: "policy".to_string(),
        line: 0,
        matched_text: "TLS required".to_string(),
        confidence: 1.0,
        description: "TLS must be required".to_string(),
    };

    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    let assertion = crate::bridge::claim_to_assertion(&claim, &signing_key, timestamp);

    // Create predicate alias: enabled ↔ required
    let predicate_aliases = vec![crate::policy::PackPredicateAliasSet {
        canonical: "enabled".to_string(),
        aliases: vec!["required".to_string(), "mandatory".to_string()],
    }];

    let pack = crate::policy::TrustPack::new_with_predicate_aliases(
        "Alias Test Pack".to_string(),
        "1.0.0".to_string(),
        vec![assertion],
        vec![],
        predicate_aliases,
        &signing_key,
        None,
        None,
    )
    .expect("create pack");

    // Save the pack
    let pack_path = temp_dir.path().join("alias_test.pack");
    pack.save(&pack_path).expect("save pack");

    // Set up config
    let mut config = AphoriaConfig::default();
    config.episteme.data_dir = temp_dir.path().join(".aphoria").join("db");

    // Import the policy (this should persist the aliases)
    let import_stats =
        crate::policy_ops::import_policy(pack_path.clone(), &config).await.expect("import policy");

    assert_eq!(import_stats.predicate_aliases_imported, 1, "Should import 1 alias set");

    // Open LocalEpisteme and verify aliases are loaded
    let episteme =
        crate::episteme::LocalEpisteme::open(&config, temp_dir.path()).await.expect("open");

    // Check in-memory aliases
    let aliases = episteme.predicate_aliases();
    assert_eq!(aliases.len(), 1, "Should have 1 alias set in memory");
    assert_eq!(aliases[0].canonical, "enabled");
    assert!(aliases[0].aliases.contains(&"required".to_string()));
    assert!(aliases[0].aliases.contains(&"mandatory".to_string()));
}

/// Test that predicate aliases survive a restart (persistence verification).
#[tokio::test]
async fn test_predicate_alias_survives_restart() {
    let temp_dir = tempfile::Builder::new()
        .prefix("aphoria_alias_restart")
        .tempdir()
        .expect("create temp dir");

    // Create .aphoria directory for the agent key
    let aphoria_dir = temp_dir.path().join(".aphoria");
    std::fs::create_dir_all(&aphoria_dir).expect("create .aphoria dir");

    // Create a Trust Pack with predicate aliases
    let signing_key = crate::bridge::load_or_generate_key(temp_dir.path()).expect("load key");
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    let claim = ExtractedClaim {
        concept_path: "rfc://test/jwt/signing".to_string(),
        predicate: "permitted".to_string(),
        value: stemedb_core::types::ObjectValue::Boolean(true),
        file: "policy".to_string(),
        line: 0,
        matched_text: "JWT permitted".to_string(),
        confidence: 1.0,
        description: "JWT signing permitted".to_string(),
    };
    let assertion = crate::bridge::claim_to_assertion(&claim, &signing_key, timestamp);

    let predicate_aliases = vec![crate::policy::PackPredicateAliasSet {
        canonical: "allowed".to_string(),
        aliases: vec!["permitted".to_string(), "approved".to_string()],
    }];

    let pack = crate::policy::TrustPack::new_with_predicate_aliases(
        "Restart Test Pack".to_string(),
        "1.0.0".to_string(),
        vec![assertion],
        vec![],
        predicate_aliases,
        &signing_key,
        None,
        None,
    )
    .expect("create pack");

    let pack_path = temp_dir.path().join("restart_test.pack");
    pack.save(&pack_path).expect("save pack");

    let mut config = AphoriaConfig::default();
    config.episteme.data_dir = temp_dir.path().join(".aphoria").join("db");

    // Import the policy
    crate::policy_ops::import_policy(pack_path.clone(), &config).await.expect("import policy");

    // Simulate restart: open a new LocalEpisteme instance
    // This should load aliases from storage
    let episteme =
        crate::episteme::LocalEpisteme::open(&config, temp_dir.path()).await.expect("reopen");

    // Verify aliases were loaded from storage
    let aliases = episteme.predicate_aliases();
    assert_eq!(aliases.len(), 1, "Should have 1 alias set after restart");
    assert_eq!(aliases[0].canonical, "allowed");
    assert!(aliases[0].aliases.contains(&"permitted".to_string()));
    assert!(aliases[0].aliases.contains(&"approved".to_string()));
}

/// Test that merge semantics work when importing multiple packs with overlapping aliases.
#[tokio::test]
async fn test_predicate_alias_merge_from_multiple_packs() {
    let temp_dir =
        tempfile::Builder::new().prefix("aphoria_alias_merge").tempdir().expect("create temp dir");

    let aphoria_dir = temp_dir.path().join(".aphoria");
    std::fs::create_dir_all(&aphoria_dir).expect("create .aphoria dir");

    let signing_key = crate::bridge::load_or_generate_key(temp_dir.path()).expect("load key");
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    let mut config = AphoriaConfig::default();
    config.episteme.data_dir = temp_dir.path().join(".aphoria").join("db");

    // Pack 1: enabled ↔ required
    let claim1 = ExtractedClaim {
        concept_path: "rfc://test/tls/version".to_string(),
        predicate: "required".to_string(),
        value: stemedb_core::types::ObjectValue::Text("1.3".to_string()),
        file: "policy".to_string(),
        line: 0,
        matched_text: "TLS 1.3 required".to_string(),
        confidence: 1.0,
        description: "TLS 1.3 required".to_string(),
    };
    let assertion1 = crate::bridge::claim_to_assertion(&claim1, &signing_key, timestamp);

    let pack1 = crate::policy::TrustPack::new_with_predicate_aliases(
        "Pack 1".to_string(),
        "1.0.0".to_string(),
        vec![assertion1],
        vec![],
        vec![crate::policy::PackPredicateAliasSet {
            canonical: "enabled".to_string(),
            aliases: vec!["required".to_string()],
        }],
        &signing_key,
        None,
        None,
    )
    .expect("create pack 1");

    let pack1_path = temp_dir.path().join("pack1.pack");
    pack1.save(&pack1_path).expect("save pack 1");

    // Pack 2: enabled ↔ mandatory (same canonical, different alias)
    let claim2 = ExtractedClaim {
        concept_path: "rfc://test/tls/ciphers".to_string(),
        predicate: "mandatory".to_string(),
        value: stemedb_core::types::ObjectValue::Text("AES-GCM".to_string()),
        file: "policy".to_string(),
        line: 0,
        matched_text: "AES-GCM mandatory".to_string(),
        confidence: 1.0,
        description: "AES-GCM mandatory".to_string(),
    };
    let assertion2 = crate::bridge::claim_to_assertion(&claim2, &signing_key, timestamp);

    let pack2 = crate::policy::TrustPack::new_with_predicate_aliases(
        "Pack 2".to_string(),
        "1.0.0".to_string(),
        vec![assertion2],
        vec![],
        vec![crate::policy::PackPredicateAliasSet {
            canonical: "enabled".to_string(),
            aliases: vec!["mandatory".to_string()],
        }],
        &signing_key,
        None,
        None,
    )
    .expect("create pack 2");

    let pack2_path = temp_dir.path().join("pack2.pack");
    pack2.save(&pack2_path).expect("save pack 2");

    // Import both packs
    crate::policy_ops::import_policy(pack1_path, &config).await.expect("import pack 1");
    crate::policy_ops::import_policy(pack2_path, &config).await.expect("import pack 2");

    // Verify merged aliases
    let episteme =
        crate::episteme::LocalEpisteme::open(&config, temp_dir.path()).await.expect("open");

    let aliases = episteme.predicate_aliases();
    assert_eq!(aliases.len(), 1, "Should have 1 merged alias set");

    let enabled_set = aliases.iter().find(|a| a.canonical == "enabled").expect("find enabled");
    assert!(
        enabled_set.aliases.contains(&"required".to_string()),
        "Should have 'required' from pack 1"
    );
    assert!(
        enabled_set.aliases.contains(&"mandatory".to_string()),
        "Should have 'mandatory' from pack 2"
    );
}

/// Test that index normalization uses persisted aliases for conflict detection.
#[tokio::test]
async fn test_index_normalization_with_persisted_aliases() {
    use crate::episteme::ConceptIndex;

    let temp_dir =
        tempfile::Builder::new().prefix("aphoria_index_norm").tempdir().expect("create temp dir");

    let aphoria_dir = temp_dir.path().join(".aphoria");
    std::fs::create_dir_all(&aphoria_dir).expect("create .aphoria dir");

    let signing_key = crate::bridge::load_or_generate_key(temp_dir.path()).expect("load key");
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    // Create authority assertion with predicate "required"
    let authority_claim = ExtractedClaim {
        concept_path: "rfc://5246/tls/cert".to_string(),
        predicate: "required".to_string(), // Using alias
        value: stemedb_core::types::ObjectValue::Boolean(true),
        file: "authority".to_string(),
        line: 0,
        matched_text: "Cert required".to_string(),
        confidence: 1.0,
        description: "Certificate required".to_string(),
    };
    let authority_assertion =
        crate::bridge::claim_to_assertion(&authority_claim, &signing_key, timestamp);

    // Create predicate aliases: enabled ↔ required
    let predicate_aliases =
        vec![crate::types::PredicateAliasSet::new("enabled", vec!["required", "mandatory"])];

    // Build index WITH alias normalization
    // "required" should be normalized to "enabled" during index build
    let corpus = vec![authority_assertion];
    let index = ConceptIndex::build_with_aliases(&corpus, &predicate_aliases);

    // Look up using "enabled" (canonical) - should find the assertion
    let result_canonical = index.lookup_with_aliases(
        "rfc://5246/tls/cert",
        "enabled", // Using canonical
        &predicate_aliases,
    );
    assert!(
        result_canonical.is_some(),
        "Should find assertion using canonical predicate 'enabled'"
    );

    // Look up using "required" (alias) - should also find the assertion
    let result_alias = index.lookup_with_aliases(
        "rfc://5246/tls/cert",
        "required", // Using alias
        &predicate_aliases,
    );
    assert!(result_alias.is_some(), "Should find assertion using alias predicate 'required'");

    // Look up using "mandatory" (another alias) - should also find
    let result_mandatory =
        index.lookup_with_aliases("rfc://5246/tls/cert", "mandatory", &predicate_aliases);
    assert!(result_mandatory.is_some(), "Should find assertion using alias predicate 'mandatory'");

    // Verify that all three return the same assertion
    let canonical_vec = result_canonical.expect("canonical");
    let alias_vec = result_alias.expect("alias");
    let mandatory_vec = result_mandatory.expect("mandatory");

    assert_eq!(canonical_vec.len(), alias_vec.len());
    assert_eq!(canonical_vec.len(), mandatory_vec.len());
    assert_eq!(canonical_vec.len(), 1);
}

/// Test the asymmetry fix: authority with "required" conflicts with code using "enabled".
#[tokio::test]
async fn test_asymmetric_predicate_conflict_detection() {
    use crate::episteme::ConceptIndex;

    let signing_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    // Authority says: required = true
    let authority_claim = ExtractedClaim {
        concept_path: "rfc://5246/tls/cert_verification".to_string(),
        predicate: "required".to_string(), // Authority uses "required"
        value: stemedb_core::types::ObjectValue::Boolean(true),
        file: "rfc5246".to_string(),
        line: 0,
        matched_text: "MUST verify".to_string(),
        confidence: 1.0,
        description: "Certificate verification required".to_string(),
    };
    let authority_assertion =
        crate::bridge::claim_to_assertion(&authority_claim, &signing_key, timestamp);

    // Code says: enabled = false (should conflict!)
    let code_claim = ExtractedClaim {
        concept_path: "code://myapp/tls/cert_verification".to_string(),
        predicate: "enabled".to_string(), // Code uses "enabled" (canonical)
        value: stemedb_core::types::ObjectValue::Boolean(false),
        file: "config.rs".to_string(),
        line: 42,
        matched_text: "verify_certs: false".to_string(),
        confidence: 0.9,
        description: "Certificate verification disabled".to_string(),
    };

    // Define aliases: enabled ↔ required
    let predicate_aliases =
        vec![crate::types::PredicateAliasSet::new("enabled", vec!["required", "mandatory"])];

    // Build index WITH normalization
    let corpus = vec![authority_assertion];
    let index = ConceptIndex::build_with_aliases(&corpus, &predicate_aliases);

    // Look up the code claim's concept using normalized predicate
    // Even though authority has "required" and code has "enabled",
    // they should match because both normalize to "enabled"
    let lookup_result = index.lookup_with_aliases(
        &code_claim.concept_path,
        &code_claim.predicate,
        &predicate_aliases,
    );

    assert!(
        lookup_result.is_some(),
        "Should find authority assertion when code uses canonical 'enabled'"
    );

    let assertions = lookup_result.expect("assertions");
    assert_eq!(assertions.len(), 1, "Should find exactly 1 authority assertion");

    // Verify the assertion values conflict (true vs false)
    let authority = &assertions[0];
    assert!(
        matches!(&authority.object, stemedb_core::types::ObjectValue::Boolean(true)),
        "Authority should have value=true"
    );
    assert!(
        matches!(&code_claim.value, stemedb_core::types::ObjectValue::Boolean(false)),
        "Code should have value=false"
    );
}
