use cannon_core::types::NormalizedEntity;
use cannon_core::ReconciliationEngine;
use cannon_core::overrides::OverrideResolver;
use cannon_core::blocking::{BlockingStrategy as BlockingStrategyTrait, PlanBlocking};
use cannon_common::ir::{
    IdentityPlan, BlockingKey, BlockingTransformation, CompiledBlocking,
    MatchGraph, CompiledRule, CompiledRuleType, DecisionConfig, SurvivorshipPolicy,
    SurvivorshipStrategy, ScoringMethod,
};
use cannon_common::spec::{BlockingStrategy, ConflictStrategy};
use uuid::Uuid;
use std::collections::HashMap;
use chrono::Utc;

fn make_entity(tenant_id: Uuid, ext_id: &str, data: HashMap<String, String>) -> NormalizedEntity {
    NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id,
        external_id: ext_id.to_string(),
        entity_type: "customer".to_string(),
        data,
        source_name: "src1".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: Utc::now(),
    }
}

fn make_test_plan(keys: Vec<BlockingKey>) -> IdentityPlan {
    IdentityPlan {
        entity: "test".to_string(),
        plan_hash: "hash".to_string(),
        identity_version: "1".to_string(),
        sources: vec![],
        blocking: CompiledBlocking {
            strategy: BlockingStrategy::Composite,
            keys,
            fallback_sample_rate: None,
            lsh_bands: None,
            lsh_rows: None,
            neighborhood_window: None,
            sort_fields: vec![],
        },
        match_graph: MatchGraph {
            rules: vec![CompiledRule {
                name: "phone_match".to_string(),
                rule_type: CompiledRuleType::Exact {
                    field: "phone".to_string(),
                    weight: 1.0,
                    case_insensitive: true,
                    normalize: true,
                    normalizer: None,
                },
            }],
            leaf_count: 1,
        },
        survivorship: SurvivorshipPolicy {
            default_strategy: SurvivorshipStrategy::MostRecent,
            field_policies: HashMap::new(),
        },
        decision: DecisionConfig {
            match_threshold: 0.8,
            review_threshold: Some(0.5),
            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![],
        exclusions: vec![],
        compliance: None,
        hierarchy: None,
        audit: None,
        metadata: None,
        governance: None,
    }
}

#[test]
fn test_phonetic_blocking() {
    let tenant_id = Uuid::new_v4();
    let mut data1 = HashMap::new();
    data1.insert("name".to_string(), "John Smith".to_string());
    let e1 = make_entity(tenant_id, "ext1", data1);

    let mut data2 = HashMap::new();
    data2.insert("name".to_string(), "Jon Smyth".to_string());
    let e2 = make_entity(tenant_id, "ext2", data2);

    let plan = make_test_plan(vec![BlockingKey {
        fields: vec!["name".to_string()],
        transformations: vec![BlockingTransformation::Soundex],
    }]);

    let engine = ReconciliationEngine::from_plan(&plan);
    let resolver = OverrideResolver::new(vec![]);
    let decisions = engine.reconcile(&[e1, e2], &resolver);

    // Names don't match exactly.
    assert!(decisions.is_empty());
}

#[test]
fn test_lsh_blocking() {
    let tenant_id = Uuid::new_v4();
    let mut data1 = HashMap::new();
    data1.insert("bio".to_string(), "The quick brown fox jumps over the lazy dog".to_string());
    let e1 = make_entity(tenant_id, "ext1", data1);

    let mut data2 = HashMap::new();
    data2.insert("bio".to_string(), "The quick brown fox jumps over a lazy dog".to_string());
    let e2 = make_entity(tenant_id, "ext2", data2);

    let mut plan = make_test_plan(vec![BlockingKey {
        fields: vec!["bio".to_string()],
        transformations: vec![],
    }]);
    plan.blocking.strategy = BlockingStrategy::Lsh;
    plan.decision.match_threshold = 0.0;
    plan.decision.review_threshold = Some(0.0);
    plan.decision.reject_threshold = Some(0.0);
    plan.match_graph.rules = vec![CompiledRule {
        name: "bio_exact".to_string(),
        rule_type: CompiledRuleType::Exact {
            field: "bio".to_string(),
            weight: 1.0,
            case_insensitive: true,
            normalize: true,
            normalizer: None,
        },
    }];

    let engine = ReconciliationEngine::from_plan(&plan);
    let resolver = OverrideResolver::new(vec![]);
    let decisions = engine.reconcile(&[e1, e2], &resolver);

    // LSH should cluster these very similar bios.
    assert!(!decisions.is_empty());
}

#[test]
fn test_phone_normalization_blocking() {
    // Two entities with different phone formats should produce the same blocking key
    let tenant_id = Uuid::new_v4();
    let mut data1 = HashMap::new();
    data1.insert("phone".to_string(), "(555) 123-4567".to_string());
    let e1 = make_entity(tenant_id, "ext1", data1);

    let mut data2 = HashMap::new();
    data2.insert("phone".to_string(), "+15551234567".to_string());
    let e2 = make_entity(tenant_id, "ext2", data2);

    let blocking = PlanBlocking {
        keys: vec![BlockingKey {
            fields: vec!["phone".to_string()],
            transformations: vec![BlockingTransformation::NormalizePhone],
        }],
    };

    let keys1 = blocking.generate_block_key(&e1);
    let keys2 = blocking.generate_block_key(&e2);

    assert!(!keys1.is_empty());
    assert!(!keys2.is_empty());
    assert_eq!(keys1, keys2, "Normalized phone keys should match: {:?} vs {:?}", keys1, keys2);
}

#[test]
fn test_email_normalization_blocking() {
    // Gmail dot-insensitivity: john.doe@gmail.com == johndoe@gmail.com
    let tenant_id = Uuid::new_v4();
    let mut data1 = HashMap::new();
    data1.insert("email".to_string(), "john.doe@gmail.com".to_string());
    let e1 = make_entity(tenant_id, "ext1", data1);

    let mut data2 = HashMap::new();
    data2.insert("email".to_string(), "johndoe@gmail.com".to_string());
    let e2 = make_entity(tenant_id, "ext2", data2);

    let blocking = PlanBlocking {
        keys: vec![BlockingKey {
            fields: vec!["email".to_string()],
            transformations: vec![BlockingTransformation::NormalizeEmail],
        }],
    };

    let keys1 = blocking.generate_block_key(&e1);
    let keys2 = blocking.generate_block_key(&e2);

    assert!(!keys1.is_empty());
    assert!(!keys2.is_empty());
    assert_eq!(keys1, keys2, "Normalized email keys should match: {:?} vs {:?}", keys1, keys2);
}

#[test]
fn test_area_code_e164() {
    // AreaCode should extract 555 from E.164 +15551234567, not 155
    let tenant_id = Uuid::new_v4();
    let mut data1 = HashMap::new();
    data1.insert("phone".to_string(), "+15551234567".to_string());
    let e1 = make_entity(tenant_id, "ext1", data1);

    let mut data2 = HashMap::new();
    data2.insert("phone".to_string(), "(555) 123-4567".to_string());
    let e2 = make_entity(tenant_id, "ext2", data2);

    let blocking = PlanBlocking {
        keys: vec![BlockingKey {
            fields: vec!["phone".to_string()],
            transformations: vec![BlockingTransformation::AreaCode],
        }],
    };

    let keys1 = blocking.generate_block_key(&e1);
    let keys2 = blocking.generate_block_key(&e2);

    assert_eq!(keys1, vec!["555".to_string()]);
    assert_eq!(keys2, vec!["555".to_string()]);
}

#[test]
fn test_name_normalization_blocking() {
    // Unicode NFC vs NFD and whitespace differences should produce the same key
    let tenant_id = Uuid::new_v4();
    let mut data1 = HashMap::new();
    data1.insert("name".to_string(), "  John   Smith  ".to_string());
    let e1 = make_entity(tenant_id, "ext1", data1);

    let mut data2 = HashMap::new();
    data2.insert("name".to_string(), "john smith".to_string());
    let e2 = make_entity(tenant_id, "ext2", data2);

    let blocking = PlanBlocking {
        keys: vec![BlockingKey {
            fields: vec!["name".to_string()],
            transformations: vec![BlockingTransformation::NormalizeName],
        }],
    };

    let keys1 = blocking.generate_block_key(&e1);
    let keys2 = blocking.generate_block_key(&e2);

    assert!(!keys1.is_empty());
    assert!(!keys2.is_empty());
    assert_eq!(keys1, keys2, "Normalized name keys should match: {:?} vs {:?}", keys1, keys2);
}

#[test]
fn test_domain_normalization_blocking() {
    // https://www.example.com/about vs example.com should produce the same key
    let tenant_id = Uuid::new_v4();
    let mut data1 = HashMap::new();
    data1.insert("website".to_string(), "https://www.acme.com/about".to_string());
    let e1 = make_entity(tenant_id, "ext1", data1);

    let mut data2 = HashMap::new();
    data2.insert("website".to_string(), "acme.com".to_string());
    let e2 = make_entity(tenant_id, "ext2", data2);

    let blocking = PlanBlocking {
        keys: vec![BlockingKey {
            fields: vec!["website".to_string()],
            transformations: vec![BlockingTransformation::NormalizeDomain],
        }],
    };

    let keys1 = blocking.generate_block_key(&e1);
    let keys2 = blocking.generate_block_key(&e2);

    assert!(!keys1.is_empty());
    assert!(!keys2.is_empty());
    assert_eq!(keys1, keys2, "Normalized domain keys should match: {:?} vs {:?}", keys1, keys2);
}
