use cannon_core::types::{NormalizedEntity, Decision};
use cannon_core::overrides::OverrideResolver;
use cannon_core::ReconciliationEngine;
use cannon_common::ir::{IdentityPlan, CompiledBlocking, CompiledRule, MatchGraph, SurvivorshipPolicy, DecisionConfig, CompiledReferenceIdentifier, CompiledRuleType, CompiledRelation, ScoringMethod};
use cannon_common::spec::{ReferenceIdType, ConflictStrategy};
use uuid::Uuid;
use std::collections::HashMap;

#[test]
fn test_reference_identifier_short_circuit() {
    let mut data_a = HashMap::new();
    data_a.insert("email".to_string(), "test@example.com".to_string());
    data_a.insert("ssn".to_string(), "123-45-678".to_string());

    let mut data_b = HashMap::new();
    data_b.insert("email".to_string(), "other@example.com".to_string()); // Different email
    data_b.insert("ssn".to_string(), "123-45-678".to_string()); // Same SSN

    let entity_a = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: Uuid::new_v4(),
        external_id: "ext_a".to_string(),
        entity_type: "customer".to_string(),
        data: data_a,
        source_name: "crm".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_b = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: entity_a.tenant_id,
        external_id: "ext_b".to_string(),
        entity_type: "customer".to_string(),
        data: data_b,
        source_name: "erp".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let plan = IdentityPlan {
        entity: "customer".to_string(),
        plan_hash: "hash".to_string(),
        identity_version: "v1".to_string(),
        sources: vec![],
        blocking: CompiledBlocking {
            strategy: cannon_common::spec::BlockingStrategy::Single,
            keys: vec![cannon_common::ir::BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![cannon_common::ir::BlockingTransformation::Lowercase],
            }],
            fallback_sample_rate: None,
            lsh_bands: None,
            lsh_rows: None,
            neighborhood_window: None,
            sort_fields: vec![],
        },
        match_graph: MatchGraph {
            rules: vec![CompiledRule {
                name: "email_exact".to_string(),
                rule_type: cannon_common::ir::CompiledRuleType::Exact {
                    field: "email".to_string(),
                    weight: 1.0,
                    case_insensitive: true,
                    normalize: true,
                    normalizer: None,
                },
            }],
            leaf_count: 1,
        },
        survivorship: SurvivorshipPolicy::default(),
        decision: DecisionConfig {
            match_threshold: 0.9,
            review_threshold: Some(0.7),
            reject_threshold: Some(0.3),
            conflict_strategy: ConflictStrategy::PreferHighConfidence,
            review_webhook: None,
            scoring_method: ScoringMethod::WeightedSum,
            ml_ensemble_config: None,
            custom_scoring_config: None,
            fellegi_sunter: None,
            tie_breaking: vec![],
        },
        reference_identifiers: vec![CompiledReferenceIdentifier {
            name: "ssn".to_string(),
            id_type: ReferenceIdType::Internal,
            authority: None,
            field: "ssn".to_string(),
            match_weight: 1.0,
        }],
        relations: vec![],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    };

    let engine = ReconciliationEngine::from_plan(&plan);
    let resolver = OverrideResolver::new(vec![]);

    let decision = engine.evaluate_pair(&entity_a, &entity_b, &resolver, 1.0);

    assert_eq!(decision.decision, Decision::Merge);
    assert_eq!(decision.confidence, 1.0);
    assert!(decision.matched_on.contains(&"REF_ID:ssn".to_string()));
}

#[test]
fn test_relation_propagation() {
    let household_id_1 = Uuid::new_v4();
    let household_id_2 = Uuid::new_v4();

    let mut data_a = HashMap::new();
    data_a.insert("email".to_string(), "test@example.com".to_string());
    data_a.insert("household_id".to_string(), household_id_1.to_string());

    let mut data_b = HashMap::new();
    data_b.insert("email".to_string(), "test@example.com".to_string());
    data_b.insert("household_id".to_string(), household_id_2.to_string());

    let entity_a = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: Uuid::new_v4(),
        external_id: "ext_a".to_string(),
        entity_type: "customer".to_string(),
        data: data_a,
        source_name: "crm".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_b = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: entity_a.tenant_id,
        external_id: "ext_b".to_string(),
        entity_type: "customer".to_string(),
        data: data_b,
        source_name: "erp".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let plan = IdentityPlan {
        entity: "customer".to_string(),
        plan_hash: "hash".to_string(),
        identity_version: "v1".to_string(),
        sources: vec![],
        blocking: CompiledBlocking {
            strategy: cannon_common::spec::BlockingStrategy::Single,
            keys: vec![cannon_common::ir::BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![cannon_common::ir::BlockingTransformation::Lowercase],
            }],
            fallback_sample_rate: None,
            lsh_bands: None,
            lsh_rows: None,
            neighborhood_window: None,
            sort_fields: vec![],
        },
        match_graph: MatchGraph {
            rules: vec![CompiledRule {
                name: "email_exact".to_string(),
                rule_type: cannon_common::ir::CompiledRuleType::Exact {
                    field: "email".to_string(),
                    weight: 1.0,
                    case_insensitive: true,
                    normalize: true,
                    normalizer: None,
                },
            }],
            leaf_count: 1,
        },
        survivorship: SurvivorshipPolicy::default(),
        decision: DecisionConfig {
            match_threshold: 0.9,
            review_threshold: Some(0.7),
            reject_threshold: Some(0.3),
            conflict_strategy: ConflictStrategy::PreferHighConfidence,
            review_webhook: None,
            scoring_method: ScoringMethod::WeightedSum,
            ml_ensemble_config: None,
            custom_scoring_config: None,
            fellegi_sunter: None,
            tie_breaking: vec![],
        },
        reference_identifiers: vec![],
        relations: vec![cannon_common::ir::CompiledRelation {
            name: "customer_to_household".to_string(),
            from_entity: "customer".to_string(),
            to_entity: "household".to_string(),
            join_key: "household_id".to_string(),
            cardinality: cannon_common::spec::Cardinality::ManyToOne,
            propagate_match: true,
        }],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    };

    let engine = ReconciliationEngine::from_plan(&plan);
    let entities = vec![entity_a, entity_b];
    let resolver = OverrideResolver::new(vec![]);
    
    // 1. Reconcile primary entities
    let decisions = engine.reconcile(&entities, &resolver);
    assert_eq!(decisions.len(), 1);
    assert_eq!(decisions[0].decision, Decision::Merge);

    // 2. Cluster to find matched groups
    let clusters = engine.cluster_decisions(&entities, &decisions);
    assert_eq!(clusters.len(), 1);
    assert_eq!(clusters[0].len(), 2);

    // 3. Propagate relations
    let prop_decisions = engine.propagate_relations(&clusters, &entities, &resolver, &decisions);
    
    assert_eq!(prop_decisions.len(), 1);
    let pd = &prop_decisions[0];
    
    // Check that it links the households
    let pair = if household_id_1 < household_id_2 { (household_id_1, household_id_2) } else { (household_id_2, household_id_1) };
    let pd_pair = if pd.entity_a_id < pd.entity_b_id { (pd.entity_a_id, pd.entity_b_id) } else { (pd.entity_b_id, pd.entity_a_id) };
    
    assert_eq!(pd_pair, pair);
    assert_eq!(pd.decision, Decision::Merge);
    assert!(pd.matched_on[0].contains("RELATION:customer_to_household"));
}

#[test]
fn test_relation_propagation_with_override() {
    let household_id_1 = Uuid::new_v4();
    let household_id_2 = Uuid::new_v4();

    let mut data_a = HashMap::new();
    data_a.insert("email".to_string(), "test@example.com".to_string());
    data_a.insert("household_id".to_string(), household_id_1.to_string());

    let mut data_b = HashMap::new();
    data_b.insert("email".to_string(), "test@example.com".to_string());
    data_b.insert("household_id".to_string(), household_id_2.to_string());

    let entity_a = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: Uuid::new_v4(),
        external_id: "ext_a".to_string(),
        entity_type: "customer".to_string(),
        data: data_a,
        source_name: "crm".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_b = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: entity_a.tenant_id,
        external_id: "ext_b".to_string(),
        entity_type: "customer".to_string(),
        data: data_b,
        source_name: "erp".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let plan = IdentityPlan {
        entity: "customer".to_string(),
        plan_hash: "hash".to_string(),
        identity_version: "v1".to_string(),
        sources: vec![],
        blocking: CompiledBlocking {
            strategy: cannon_common::spec::BlockingStrategy::Single,
            keys: vec![cannon_common::ir::BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![cannon_common::ir::BlockingTransformation::Lowercase],
            }],
            fallback_sample_rate: None,
            lsh_bands: None,
            lsh_rows: None,
            neighborhood_window: None,
            sort_fields: vec![],
        },
        match_graph: MatchGraph {
            rules: vec![CompiledRule {
                name: "email_exact".to_string(),
                rule_type: cannon_common::ir::CompiledRuleType::Exact {
                    field: "email".to_string(),
                    weight: 1.0,
                    case_insensitive: true,
                    normalize: true,
                    normalizer: None,
                },
            }],
            leaf_count: 1,
        },
        survivorship: SurvivorshipPolicy::default(),
        decision: DecisionConfig {
            match_threshold: 0.9,
            review_threshold: Some(0.7),
            reject_threshold: Some(0.3),
            conflict_strategy: cannon_common::spec::ConflictStrategy::PreferHighConfidence,
            review_webhook: None,
            scoring_method: ScoringMethod::WeightedSum,
            ml_ensemble_config: None,
            custom_scoring_config: None,
            fellegi_sunter: None,
            tie_breaking: vec![],
        },
        reference_identifiers: vec![],
        relations: vec![cannon_common::ir::CompiledRelation {
            name: "customer_to_household".to_string(),
            from_entity: "customer".to_string(),
            to_entity: "household".to_string(),
            join_key: "household_id".to_string(),
            cardinality: cannon_common::spec::Cardinality::ManyToOne,
            propagate_match: true,
        }],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    };

    let engine = ReconciliationEngine::from_plan(&plan);
    let entities = vec![entity_a, entity_b];
    
    // Create a SPLIT override for the two households
    let manual_override = cannon_core::overrides::ManualOverride {
        tenant_id: entities[0].tenant_id,
        override_type: cannon_core::overrides::OverrideType::Split,
        entity_a_id: Some(household_id_1),
        entity_b_id: Some(household_id_2),
        canonical_id: None,
        override_data: serde_json::json!({}),
    };
    let resolver = OverrideResolver::new(vec![manual_override]);
    
    // 1. Reconcile primary entities
    let decisions = engine.reconcile(&entities, &resolver);
    
    // 2. Cluster
    let clusters = engine.cluster_decisions(&entities, &decisions);
    
    // 3. Propagate relations - should be skipped due to override
    let prop_decisions = engine.propagate_relations(&clusters, &entities, &resolver, &decisions);
    
    assert_eq!(prop_decisions.len(), 0, "Relation propagation should have been skipped due to override");
}

#[test]
fn test_multi_hop_relation_propagation() {
    let household_id_1 = Uuid::new_v4();
    let household_id_2 = Uuid::new_v4();
    let address_id_1 = Uuid::new_v4();
    let address_id_2 = Uuid::new_v4();

    // Customer A -> Household 1 -> Address 1
    // Customer B -> Household 2 -> Address 2
    // Customer A and B match on email.

    let mut data_a = HashMap::new();
    data_a.insert("email".to_string(), "test@example.com".to_string());
    data_a.insert("household_id".to_string(), household_id_1.to_string());
    data_a.insert("entity_type".to_string(), "customer".to_string());

    let mut data_b = HashMap::new();
    data_b.insert("email".to_string(), "test@example.com".to_string());
    data_b.insert("household_id".to_string(), household_id_2.to_string());
    data_b.insert("entity_type".to_string(), "customer".to_string());

    let mut data_h1 = HashMap::new();
    data_h1.insert("address_id".to_string(), address_id_1.to_string());
    data_h1.insert("entity_type".to_string(), "household".to_string());

    let mut data_h2 = HashMap::new();
    data_h2.insert("address_id".to_string(), address_id_2.to_string());
    data_h2.insert("entity_type".to_string(), "household".to_string());

    let tenant_id = Uuid::new_v4();

    let entity_a = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id,
        external_id: "cust_a".to_string(),
        entity_type: "customer".to_string(),
        data: data_a,
        source_name: "crm".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_b = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id,
        external_id: "cust_b".to_string(),
        entity_type: "customer".to_string(),
        data: data_b,
        source_name: "erp".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_h1 = NormalizedEntity {
        id: household_id_1,
        tenant_id,
        external_id: "h1".to_string(),
        entity_type: "household".to_string(),
        data: data_h1,
        source_name: "crm".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_h2 = NormalizedEntity {
        id: household_id_2,
        tenant_id,
        external_id: "h2".to_string(),
        entity_type: "household".to_string(),
        data: data_h2,
        source_name: "erp".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let plan = IdentityPlan {
        entity: "customer".to_string(),
        plan_hash: "hash".to_string(),
        identity_version: "v1".to_string(),
        sources: vec![],
        blocking: CompiledBlocking {
            strategy: cannon_common::spec::BlockingStrategy::Single,
            keys: vec![cannon_common::ir::BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![cannon_common::ir::BlockingTransformation::Lowercase],
            }],
            fallback_sample_rate: None,
            lsh_bands: None,
            lsh_rows: None,
            neighborhood_window: None,
            sort_fields: vec![],
        },
        match_graph: MatchGraph {
            rules: vec![CompiledRule {
                name: "email_exact".to_string(),
                rule_type: cannon_common::ir::CompiledRuleType::Exact {
                    field: "email".to_string(),
                    weight: 1.0,
                    case_insensitive: true,
                    normalize: true,
                    normalizer: None,
                },
            }],
            leaf_count: 1,
        },
        survivorship: SurvivorshipPolicy::default(),
        decision: DecisionConfig {
            match_threshold: 0.9,
            review_threshold: Some(0.7),
            reject_threshold: Some(0.3),
            conflict_strategy: cannon_common::spec::ConflictStrategy::PreferHighConfidence,
            review_webhook: None,
            scoring_method: ScoringMethod::WeightedSum,
            ml_ensemble_config: None,
            custom_scoring_config: None,
            fellegi_sunter: None,
            tie_breaking: vec![],
        },
        reference_identifiers: vec![],
        relations: vec![
            cannon_common::ir::CompiledRelation {
                name: "customer_to_household".to_string(),
                from_entity: "customer".to_string(),
                to_entity: "household".to_string(),
                join_key: "household_id".to_string(),
                cardinality: cannon_common::spec::Cardinality::ManyToOne,
                propagate_match: true,
            },
            cannon_common::ir::CompiledRelation {
                name: "household_to_address".to_string(),
                from_entity: "household".to_string(),
                to_entity: "address".to_string(),
                join_key: "address_id".to_string(),
                cardinality: cannon_common::spec::Cardinality::ManyToOne,
                propagate_match: true,
            },
        ],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    };

    let engine = ReconciliationEngine::from_plan(&plan);
    let entities = vec![entity_a, entity_b, entity_h1, entity_h2];
    let resolver = OverrideResolver::new(vec![]);
    
    // 1. Reconcile primary entities (customers)
    let decisions = engine.reconcile(&entities, &resolver);
    assert_eq!(decisions.len(), 1);

    // 2. Cluster
    let clusters = engine.cluster_decisions(&entities, &decisions);
    
    // 3. Propagate relations - should take 2 hops to link addresses
    let prop_decisions = engine.propagate_relations(&clusters, &entities, &resolver, &decisions);
    
    // We expect 2 decisions:
    // 1. Household 1 <-> Household 2 (from Customer match)
    // 2. Address 1 <-> Address 2 (from Household match - triggered by hop 2)
    assert_eq!(prop_decisions.len(), 2);
    
    let matched_relations: Vec<String> = prop_decisions.iter().flat_map(|d| d.matched_on.clone()).collect();
    assert!(matched_relations.iter().any(|r| r.contains("RELATION:customer_to_household:hop:0")));
    assert!(matched_relations.iter().any(|r| r.contains("RELATION:household_to_address:hop:1")));
    
    // Verify addresses are linked
    let addr_pair = if address_id_1 < address_id_2 { (address_id_1, address_id_2) } else { (address_id_2, address_id_1) };
    let linked_addr = prop_decisions.iter().any(|d| {
        let pair = if d.entity_a_id < d.entity_b_id { (d.entity_a_id, d.entity_b_id) } else { (d.entity_b_id, d.entity_a_id) };
        pair == addr_pair
    });
    assert!(linked_addr, "Addresses should be linked via multi-hop propagation");
}

#[test]
fn test_transitive_hybrid_propagation() {
    // A matches B (Scoring/Email)
    // B relates to C (Relation)
    // Outcome: A, B, C should all be in the same cluster, and C matches other C-linked entities.
    
    let tenant_id = Uuid::new_v4();
    let household_id_1 = Uuid::new_v4();
    let household_id_2 = Uuid::new_v4();

    let mut data_a = HashMap::new();
    data_a.insert("email".to_string(), "shared@example.com".to_string());
    data_a.insert("household_id".to_string(), household_id_1.to_string());

    let mut data_b = HashMap::new();
    data_b.insert("email".to_string(), "shared@example.com".to_string());
    data_b.insert("household_id".to_string(), household_id_2.to_string());

    let mut data_c1 = HashMap::new();
    data_c1.insert("id".to_string(), household_id_1.to_string());

    let mut data_c2 = HashMap::new();
    data_c2.insert("id".to_string(), household_id_2.to_string());

    let entity_a = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id,
        external_id: "a".to_string(),
        entity_type: "customer".to_string(),
        data: data_a,
        source_name: "s1".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_b = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id,
        external_id: "b".to_string(),
        entity_type: "customer".to_string(),
        data: data_b,
        source_name: "s2".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_c1 = NormalizedEntity {
        id: household_id_1,
        tenant_id,
        external_id: "c1".to_string(),
        entity_type: "household".to_string(),
        data: data_c1,
        source_name: "s3".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_c2 = NormalizedEntity {
        id: household_id_2,
        tenant_id,
        external_id: "c2".to_string(),
        entity_type: "household".to_string(),
        data: data_c2,
        source_name: "s4".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let plan = IdentityPlan {
        entity: "customer".to_string(),
        plan_hash: "hash".to_string(),
        identity_version: "v1".to_string(),
        sources: vec![],
        blocking: CompiledBlocking {
            strategy: cannon_common::spec::BlockingStrategy::Single,
            keys: vec![cannon_common::ir::BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![cannon_common::ir::BlockingTransformation::Lowercase],
            }],
            fallback_sample_rate: None,
            lsh_bands: None,
            lsh_rows: None,
            neighborhood_window: None,
            sort_fields: vec![],
        },
        match_graph: MatchGraph {
            rules: vec![CompiledRule {
                name: "email".to_string(),
                rule_type: CompiledRuleType::Exact {
                    field: "email".to_string(),
                    weight: 1.0,
                    case_insensitive: true,
                    normalize: true,
                    normalizer: None,
                },
            }],
            leaf_count: 1,
        },
        survivorship: SurvivorshipPolicy::default(),
        decision: DecisionConfig {
            match_threshold: 0.9,
            review_threshold: None,
            reject_threshold: None,
            conflict_strategy: cannon_common::spec::ConflictStrategy::PreferHighConfidence,
            review_webhook: None,
            scoring_method: ScoringMethod::WeightedSum,
            ml_ensemble_config: None,
            custom_scoring_config: None,
            fellegi_sunter: None,
            tie_breaking: vec![],
        },
        reference_identifiers: vec![],
        relations: vec![CompiledRelation {
            name: "cust_to_hh".to_string(),
            from_entity: "customer".to_string(),
            to_entity: "household".to_string(),
            join_key: "household_id".to_string(),
            cardinality: cannon_common::spec::Cardinality::ManyToOne,
            propagate_match: true,
        }],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    };

    let engine = ReconciliationEngine::from_plan(&plan);
    let entities = vec![entity_a, entity_b, entity_c1, entity_c2];
    let resolver = OverrideResolver::new(vec![]);

    // Step 1: Rule-based match (A <-> B)
    let decisions = engine.reconcile(&entities, &resolver);
    assert_eq!(decisions.len(), 1);

    // Step 2: Cluster
    let clusters = engine.cluster_decisions(&entities, &decisions);
    
    // Step 3: Propagate (Should link Household C1 and C2 because they are both linked to matched customers A and B)
    let prop_decisions = engine.propagate_relations(&clusters, &entities, &resolver, &decisions);
    
    assert_eq!(prop_decisions.len(), 1);
    let pd = &prop_decisions[0];
    let pair = if household_id_1 < household_id_2 { (household_id_1, household_id_2) } else { (household_id_2, household_id_1) };
    let pd_pair = if pd.entity_a_id < pd.entity_b_id { (pd.entity_a_id, pd.entity_b_id) } else { (pd.entity_b_id, pd.entity_a_id) };
    assert_eq!(pd_pair, pair);
}

#[test]
fn test_alphanumeric_join_key_propagation() {
    let tenant_id = Uuid::new_v4();
    let external_join_key = "HH_EXT_999";

    let mut data_a = HashMap::new();
    data_a.insert("email".to_string(), "a@example.com".to_string());
    data_a.insert("hh_key".to_string(), external_join_key.to_string());

    let mut data_b = HashMap::new();
    data_b.insert("email".to_string(), "a@example.com".to_string());
    data_b.insert("hh_key".to_string(), external_join_key.to_string());

    let entity_a = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id,
        external_id: "a".to_string(),
        entity_type: "customer".to_string(),
        data: data_a,
        source_name: "s1".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let entity_b = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id,
        external_id: "b".to_string(),
        entity_type: "customer".to_string(),
        data: data_b,
        source_name: "s2".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    };

    let plan = IdentityPlan {
        entity: "customer".to_string(),
        plan_hash: "hash".to_string(),
        identity_version: "v1".to_string(),
        sources: vec![],
        blocking: CompiledBlocking {
            strategy: cannon_common::spec::BlockingStrategy::Single,
            keys: vec![cannon_common::ir::BlockingKey {
                fields: vec!["email".to_string()],
                transformations: vec![cannon_common::ir::BlockingTransformation::Lowercase],
            }],
            fallback_sample_rate: None,
            lsh_bands: None,
            lsh_rows: None,
            neighborhood_window: None,
            sort_fields: vec![],
        },
        match_graph: MatchGraph {
            rules: vec![CompiledRule {
                name: "email".to_string(),
                rule_type: CompiledRuleType::Exact {
                    field: "email".to_string(),
                    weight: 1.0,
                    case_insensitive: true,
                    normalize: true,
                    normalizer: None,
                },
            }],
            leaf_count: 1,
        },
        survivorship: SurvivorshipPolicy::default(),
        decision: DecisionConfig {
            match_threshold: 0.9,
            review_threshold: None,
            reject_threshold: None,
            conflict_strategy: cannon_common::spec::ConflictStrategy::PreferHighConfidence,
            review_webhook: None,
            scoring_method: ScoringMethod::WeightedSum,
            ml_ensemble_config: None,
            custom_scoring_config: None,
            fellegi_sunter: None,
            tie_breaking: vec![],
        },
        reference_identifiers: vec![],
        relations: vec![CompiledRelation {
            name: "cust_to_hh".to_string(),
            from_entity: "customer".to_string(),
            to_entity: "household".to_string(),
            join_key: "hh_key".to_string(),
            cardinality: cannon_common::spec::Cardinality::ManyToOne,
            propagate_match: true,
        }],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    };

    let engine = ReconciliationEngine::from_plan(&plan);
    let entities = vec![entity_a, entity_b];
    let resolver = OverrideResolver::new(vec![]);

    let decisions = engine.reconcile(&entities, &resolver);
    let clusters = engine.cluster_decisions(&entities, &decisions);
    
    // Should NOT panic and should handle the string key HH_EXT_999
    let prop_decisions = engine.propagate_relations(&clusters, &entities, &resolver, &decisions);
    
    // Even though there are no "household" entities, the propagation logic should attempt to find them
    // and not crash even if the key is not a UUID.
    assert_eq!(prop_decisions.len(), 0); // No households found to link
}
