//! Determinism tests for the reconciliation engine.
//!
//! Verifies that the engine produces identical output regardless of:
//! - Input entity ordering (shuffle invariance)
//! - Random UUID assignment per run
//! - Parallel execution ordering (rayon)

use cannon_core::types::{NormalizedEntity, MatchDecision};
use cannon_core::overrides::OverrideResolver;
use cannon_core::ReconciliationEngine;
use cannon_core::survivorship::SurvivorshipEngine;
use cannon_common::ir::{
    IdentityPlan, CompiledBlocking, CompiledRule, MatchGraph,
    SurvivorshipPolicy, DecisionConfig, ScoringMethod,
};
use cannon_common::spec::ConflictStrategy;
use uuid::Uuid;
use std::collections::{HashMap, BTreeSet};

/// Create a NormalizedEntity with fresh UUID but stable external_id and source.
fn make_entity(external_id: &str, source: &str, data: Vec<(&str, &str)>) -> NormalizedEntity {
    let mut map = HashMap::new();
    for (k, v) in data {
        map.insert(k.to_string(), v.to_string());
    }
    NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: Uuid::nil(),
        external_id: external_id.to_string(),
        entity_type: "customer".to_string(),
        data: map,
        source_name: source.to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: chrono::Utc::now(),
    }
}

/// Build an engine with exact-match rules on email and name fields.
fn make_test_engine() -> (ReconciliationEngine, IdentityPlan) {
    let plan = IdentityPlan {
        entity: "customer".to_string(),
        plan_hash: "determinism_test".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],
                },
                cannon_common::ir::BlockingKey {
                    fields: vec!["phone".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,
                    },
                },
                CompiledRule {
                    name: "name_exact".to_string(),
                    rule_type: cannon_common::ir::CompiledRuleType::Exact {
                        field: "name".to_string(),
                        weight: 0.5,
                        case_insensitive: true,
                        normalize: true,
                        normalizer: None,
                    },
                },
            ],
            leaf_count: 2,
        },
        survivorship: SurvivorshipPolicy::default(),
        decision: DecisionConfig {
            match_threshold: 0.6,
            review_threshold: Some(0.3),
            reject_threshold: Some(0.1),
            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![],
            clustering: None,
            min_total_weight: 0.0,
            allow_single_field: vec![],
        },
        reference_identifiers: vec![],
        relations: vec![],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    };
    let engine = ReconciliationEngine::from_plan(&plan);
    (engine, plan)
}

/// Generate a realistic set of 20 entities with overlapping data across sources.
fn make_test_entities() -> Vec<NormalizedEntity> {
    vec![
        // Cluster 1: Alice - shared email across 3 sources
        make_entity("sf_001", "salesforce", vec![("email", "alice@example.com"), ("name", "Alice Smith"), ("phone", "555-0101")]),
        make_entity("st_001", "stripe", vec![("email", "alice@example.com"), ("name", "Alice S"), ("phone", "555-0101")]),
        make_entity("hb_001", "hubspot", vec![("email", "alice@example.com"), ("name", "Alice Smith"), ("phone", "555-0102")]),
        // Cluster 2: Bob - shared email + phone
        make_entity("sf_002", "salesforce", vec![("email", "bob@example.com"), ("name", "Bob Jones"), ("phone", "555-0200")]),
        make_entity("st_002", "stripe", vec![("email", "bob@example.com"), ("name", "Robert Jones"), ("phone", "555-0200")]),
        // Cluster 3: Carol - email only match
        make_entity("sf_003", "salesforce", vec![("email", "carol@example.com"), ("name", "Carol White"), ("phone", "555-0300")]),
        make_entity("hb_003", "hubspot", vec![("email", "carol@example.com"), ("name", "Carol W"), ("phone", "555-0301")]),
        // Cluster 4: Dave - phone only match (no email overlap)
        make_entity("sf_004", "salesforce", vec![("email", "dave@corp.com"), ("name", "Dave Brown"), ("phone", "555-0400")]),
        make_entity("st_004", "stripe", vec![("email", "david@personal.com"), ("name", "Dave Brown"), ("phone", "555-0400")]),
        // Singletons - no matches
        make_entity("sf_005", "salesforce", vec![("email", "eve@example.com"), ("name", "Eve Green"), ("phone", "555-0500")]),
        make_entity("st_006", "stripe", vec![("email", "frank@example.com"), ("name", "Frank Lee"), ("phone", "555-0600")]),
        make_entity("hb_007", "hubspot", vec![("email", "grace@example.com"), ("name", "Grace Kim"), ("phone", "555-0700")]),
        // Cluster 5: shared email, 4 sources
        make_entity("sf_008", "salesforce", vec![("email", "shared@company.com"), ("name", "Team Account"), ("phone", "555-0800")]),
        make_entity("st_008", "stripe", vec![("email", "shared@company.com"), ("name", "Team Acct"), ("phone", "555-0801")]),
        make_entity("hb_008", "hubspot", vec![("email", "shared@company.com"), ("name", "Team Account"), ("phone", "555-0800")]),
        make_entity("zd_008", "zendesk", vec![("email", "shared@company.com"), ("name", "Team"), ("phone", "555-0802")]),
        // More singletons
        make_entity("sf_009", "salesforce", vec![("email", "henry@example.com"), ("name", "Henry Park"), ("phone", "555-0900")]),
        make_entity("st_010", "stripe", vec![("email", "iris@example.com"), ("name", "Iris Chen"), ("phone", "555-1000")]),
        make_entity("hb_011", "hubspot", vec![("email", "jack@example.com"), ("name", "Jack Wu"), ("phone", "555-1100")]),
        make_entity("zd_012", "zendesk", vec![("email", "kate@example.com"), ("name", "Kate Liu"), ("phone", "555-1200")]),
    ]
}

/// Shuffle entities using a simple deterministic permutation based on seed.
fn shuffle_entities(entities: &[NormalizedEntity], seed: u64) -> Vec<NormalizedEntity> {
    let mut indices: Vec<usize> = (0..entities.len()).collect();
    // Simple Fisher-Yates with LCG PRNG
    let mut rng = seed;
    for i in (1..indices.len()).rev() {
        rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
        let j = (rng >> 33) as usize % (i + 1);
        indices.swap(i, j);
    }
    // Create new entities with fresh UUIDs (simulating a fresh SDK run)
    indices.iter().map(|&i| {
        let orig = &entities[i];
        NormalizedEntity {
            id: Uuid::new_v4(), // Fresh random UUID each time
            tenant_id: Uuid::nil(),
            external_id: orig.external_id.clone(),
            entity_type: orig.entity_type.clone(),
            data: orig.data.clone(),
            source_name: orig.source_name.clone(),
            valid_from: orig.valid_from,
            valid_to: orig.valid_to,
            last_updated: orig.last_updated,
        }
    }).collect()
}

/// Extract a UUID-independent decision signature for comparison.
fn decision_sig(d: &MatchDecision, entity_map: &HashMap<Uuid, &NormalizedEntity>) -> String {
    let ext_a = entity_map.get(&d.entity_a_id)
        .map(|e| format!("{}:{}", e.source_name, e.external_id))
        .unwrap_or_default();
    let ext_b = entity_map.get(&d.entity_b_id)
        .map(|e| format!("{}:{}", e.source_name, e.external_id))
        .unwrap_or_default();
    // Sort the pair so (A,B) == (B,A)
    let (first, second) = if ext_a <= ext_b { (ext_a, ext_b) } else { (ext_b, ext_a) };
    let mut matched_on = d.matched_on.clone();
    matched_on.sort();
    format!(
        "{}|{}|{:?}|{:.10}|{}",
        first, second, d.decision, d.confidence,
        matched_on.join(",")
    )
}

/// Extract UUID-independent cluster membership for comparison.
fn cluster_membership(
    clusters: &[Vec<Uuid>],
    entity_map: &HashMap<Uuid, &NormalizedEntity>,
) -> BTreeSet<Vec<String>> {
    clusters.iter().map(|cluster| {
        let mut members: Vec<String> = cluster.iter()
            .filter_map(|id| entity_map.get(id))
            .map(|e| format!("{}:{}", e.source_name, e.external_id))
            .collect();
        members.sort();
        members
    }).collect()
}

#[test]
fn test_shuffled_input_produces_identical_decisions() {
    let base_entities = make_test_entities();
    let (engine, _plan) = make_test_engine();
    let resolver = OverrideResolver::new(vec![]);

    // Run with original order
    let entity_map_base: HashMap<Uuid, &NormalizedEntity> = base_entities.iter()
        .map(|e| (e.id, e)).collect();
    let decisions_base = engine.reconcile(&base_entities, &resolver);
    let mut sigs_base: Vec<String> = decisions_base.iter()
        .map(|d| decision_sig(d, &entity_map_base))
        .collect();
    sigs_base.sort();

    // Run 10 shuffled permutations with fresh UUIDs
    for seed in 0..10u64 {
        let shuffled = shuffle_entities(&base_entities, seed);
        let entity_map_shuf: HashMap<Uuid, &NormalizedEntity> = shuffled.iter()
            .map(|e| (e.id, e)).collect();
        let decisions_shuf = engine.reconcile(&shuffled, &resolver);
        let mut sigs_shuf: Vec<String> = decisions_shuf.iter()
            .map(|d| decision_sig(d, &entity_map_shuf))
            .collect();
        sigs_shuf.sort();

        assert_eq!(
            sigs_base.len(), sigs_shuf.len(),
            "Seed {}: decision count differs ({} vs {})",
            seed, sigs_base.len(), sigs_shuf.len()
        );
        assert_eq!(
            sigs_base, sigs_shuf,
            "Seed {}: decision signatures differ", seed
        );
    }
}

#[test]
fn test_shuffled_input_produces_identical_clusters() {
    let base_entities = make_test_entities();
    let (engine, _plan) = make_test_engine();
    let resolver = OverrideResolver::new(vec![]);

    let entity_map_base: HashMap<Uuid, &NormalizedEntity> = base_entities.iter()
        .map(|e| (e.id, e)).collect();
    let decisions_base = engine.reconcile(&base_entities, &resolver);
    let clusters_base = engine.cluster_decisions(&base_entities, &decisions_base);
    let membership_base = cluster_membership(&clusters_base, &entity_map_base);

    for seed in 0..10u64 {
        let shuffled = shuffle_entities(&base_entities, seed);
        let entity_map_shuf: HashMap<Uuid, &NormalizedEntity> = shuffled.iter()
            .map(|e| (e.id, e)).collect();
        let decisions_shuf = engine.reconcile(&shuffled, &resolver);
        let clusters_shuf = engine.cluster_decisions(&shuffled, &decisions_shuf);
        let membership_shuf = cluster_membership(&clusters_shuf, &entity_map_shuf);

        assert_eq!(
            membership_base, membership_shuf,
            "Seed {}: cluster membership differs", seed
        );
    }
}

#[test]
fn test_shuffled_input_produces_identical_golden_records() {
    let base_entities = make_test_entities();
    let (engine, plan) = make_test_engine();
    let resolver = OverrideResolver::new(vec![]);

    let decisions_base = engine.reconcile(&base_entities, &resolver);
    let clusters_base = engine.cluster_decisions(&base_entities, &decisions_base);

    let mut golden_base: Vec<Vec<(String, String)>> = clusters_base.iter().map(|cluster_ids| {
        let cluster_entities: Vec<NormalizedEntity> = cluster_ids.iter()
            .filter_map(|id| base_entities.iter().find(|e| &e.id == id).cloned())
            .collect();
        let golden = SurvivorshipEngine::construct_golden_record(&cluster_entities, &plan.survivorship);
        let mut pairs: Vec<(String, String)> = golden.into_iter().collect();
        pairs.sort();
        pairs
    }).collect();
    golden_base.sort();

    for seed in 0..10u64 {
        let shuffled = shuffle_entities(&base_entities, seed);
        let decisions_shuf = engine.reconcile(&shuffled, &resolver);
        let clusters_shuf = engine.cluster_decisions(&shuffled, &decisions_shuf);

        let mut golden_shuf: Vec<Vec<(String, String)>> = clusters_shuf.iter().map(|cluster_ids| {
            let cluster_entities: Vec<NormalizedEntity> = cluster_ids.iter()
                .filter_map(|id| shuffled.iter().find(|e| &e.id == id).cloned())
                .collect();
            let golden = SurvivorshipEngine::construct_golden_record(&cluster_entities, &plan.survivorship);
            let mut pairs: Vec<(String, String)> = golden.into_iter().collect();
            pairs.sort();
            pairs
        }).collect();
        golden_shuf.sort();

        assert_eq!(
            golden_base, golden_shuf,
            "Seed {}: golden records differ", seed
        );
    }
}

#[test]
fn test_repeated_runs_same_order_identical() {
    // Even without shuffling, fresh UUIDs should produce identical results.
    let (engine, _plan) = make_test_engine();
    let resolver = OverrideResolver::new(vec![]);

    let entities_1 = make_test_entities();
    let entities_2 = make_test_entities(); // Fresh UUIDs, same data

    let map_1: HashMap<Uuid, &NormalizedEntity> = entities_1.iter().map(|e| (e.id, e)).collect();
    let map_2: HashMap<Uuid, &NormalizedEntity> = entities_2.iter().map(|e| (e.id, e)).collect();

    let decisions_1 = engine.reconcile(&entities_1, &resolver);
    let decisions_2 = engine.reconcile(&entities_2, &resolver);

    let mut sigs_1: Vec<String> = decisions_1.iter().map(|d| decision_sig(d, &map_1)).collect();
    let mut sigs_2: Vec<String> = decisions_2.iter().map(|d| decision_sig(d, &map_2)).collect();
    sigs_1.sort();
    sigs_2.sort();

    assert_eq!(sigs_1, sigs_2, "Two runs with same data but fresh UUIDs should produce identical decisions");
}

#[test]
fn test_telemetry_deterministic() {
    let base_entities = make_test_entities();
    let (engine, _plan) = make_test_engine();
    let resolver = OverrideResolver::new(vec![]);

    let (decisions_base, telemetry_base) = engine.reconcile_with_telemetry(&base_entities, &resolver);
    let entity_map_base: HashMap<Uuid, &NormalizedEntity> = base_entities.iter()
        .map(|e| (e.id, e)).collect();
    let mut sigs_base: Vec<String> = decisions_base.iter()
        .map(|d| decision_sig(d, &entity_map_base)).collect();
    sigs_base.sort();

    for seed in 0..5u64 {
        let shuffled = shuffle_entities(&base_entities, seed);
        let entity_map_shuf: HashMap<Uuid, &NormalizedEntity> = shuffled.iter()
            .map(|e| (e.id, e)).collect();
        let (decisions_shuf, telemetry_shuf) = engine.reconcile_with_telemetry(&shuffled, &resolver);
        let mut sigs_shuf: Vec<String> = decisions_shuf.iter()
            .map(|d| decision_sig(d, &entity_map_shuf)).collect();
        sigs_shuf.sort();

        assert_eq!(sigs_base, sigs_shuf, "Seed {}: telemetry-path decisions differ", seed);
        assert_eq!(
            telemetry_base.blocking_groups, telemetry_shuf.blocking_groups,
            "Seed {}: blocking group count differs", seed
        );
        assert_eq!(
            telemetry_base.pairs_evaluated, telemetry_shuf.pairs_evaluated,
            "Seed {}: pairs evaluated differs", seed
        );
    }
}
