//! Graph-based clustering tests.
//!
//! Tests for the coherence-aware graph clustering method that breaks
//! weak transitive chains.

use cannon_common::spec::IdentitySpec;
use cannon_core::engine::compiler::SpecCompiler;
use cannon_core::engine::ReconciliationEngine;
use cannon_core::engine::types::{NormalizedEntity, MatchDecision, Decision};
use cannon_common::ir::{CompiledClusteringConfig, ClusteringMethod};
use std::collections::HashMap;
use uuid::Uuid;

fn make_entity(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: 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(),
    }
}

fn make_decision(a: Uuid, b: Uuid, confidence: f64) -> MatchDecision {
    MatchDecision {
        entity_a_id: a,
        entity_b_id: b,
        decision: Decision::Merge,
        confidence,
        posterior_probability: None,
        rule_results: vec![],
        matched_on: vec![],
        blocking_key: None,
        temporal_adjustment: None,
    }
}

fn make_engine_with_graph_clustering(
    min_edge_weight: f64,
    min_cluster_coherence: f64,
    max_cluster_size: usize,
) -> ReconciliationEngine {
    let mut engine = ReconciliationEngine::new(vec![], vec![], 0.85, 0.6);
    engine.clustering_config = Some(CompiledClusteringConfig {
        method: ClusteringMethod::Graph,
        min_edge_weight,
        min_cluster_coherence,
        max_cluster_size,
    });
    engine
}

fn make_engine_simple() -> ReconciliationEngine {
    let mut engine = ReconciliationEngine::new(vec![], vec![], 0.85, 0.6);
    engine.clustering_config = Some(CompiledClusteringConfig {
        method: ClusteringMethod::Simple,
        min_edge_weight: 0.0,
        min_cluster_coherence: 0.0,
        max_cluster_size: usize::MAX,
    });
    engine
}

#[test]
fn test_graph_breaks_weak_chain() {
    // A-B(0.95), B-C(0.60) with coherence=0.7 -> two clusters
    let a = make_entity("a", "src", vec![("name", "alice")]);
    let b = make_entity("b", "src", vec![("name", "alice")]);
    let c = make_entity("c", "src", vec![("name", "alice")]);
    let entities = vec![a.clone(), b.clone(), c.clone()];

    let decisions = vec![
        make_decision(a.id, b.id, 0.95),
        make_decision(b.id, c.id, 0.60),
    ];

    let engine = make_engine_with_graph_clustering(0.0, 0.7, usize::MAX);
    let _clusters = engine.cluster_decisions(&entities, &decisions);

    // avg(0.95, 0.60) = 0.775 > 0.7, so stays together at coherence=0.7.
    // Use a higher coherence threshold to trigger the split.
    let engine2 = make_engine_with_graph_clustering(0.0, 0.8, usize::MAX);
    let clusters2 = engine2.cluster_decisions(&entities, &decisions);

    // avg(0.95, 0.60) = 0.775 < 0.8 -> splits.
    // After removing weakest (0.60): {A,B} stays (single edge 0.95), {C} singleton.
    let multi_clusters: Vec<&Vec<Uuid>> = clusters2.iter().filter(|c| c.len() > 1).collect();
    assert_eq!(multi_clusters.len(), 1, "Should have exactly one multi-member cluster");
    assert_eq!(multi_clusters[0].len(), 2, "A-B cluster should have 2 members");
}

#[test]
fn test_graph_edge_filtering() {
    // min_edge_weight drops low-confidence edges
    let a = make_entity("a", "src", vec![("name", "alice")]);
    let b = make_entity("b", "src", vec![("name", "alice")]);
    let c = make_entity("c", "src", vec![("name", "alice")]);
    let entities = vec![a.clone(), b.clone(), c.clone()];

    let decisions = vec![
        make_decision(a.id, b.id, 0.95),
        make_decision(b.id, c.id, 0.60),
    ];

    // min_edge_weight=0.7 should drop the 0.60 edge
    let engine = make_engine_with_graph_clustering(0.7, 0.0, usize::MAX);
    let clusters = engine.cluster_decisions(&entities, &decisions);

    // Only A-B edge survives -> {A,B} and {C}
    let multi: Vec<&Vec<Uuid>> = clusters.iter().filter(|c| c.len() > 1).collect();
    assert_eq!(multi.len(), 1, "Should have one 2-member cluster");
    assert_eq!(multi[0].len(), 2);
    assert_eq!(clusters.len(), 2, "Should have 2 clusters total (one pair + one singleton)");
}

#[test]
fn test_graph_backward_compat() {
    // method=Simple produces identical output to the old union-find
    let a = make_entity("a", "src", vec![("name", "alice")]);
    let b = make_entity("b", "src", vec![("name", "alice")]);
    let c = make_entity("c", "src", vec![("name", "alice")]);
    let entities = vec![a.clone(), b.clone(), c.clone()];

    let decisions = vec![
        make_decision(a.id, b.id, 0.95),
        make_decision(b.id, c.id, 0.60),
    ];

    let engine_simple = make_engine_simple();
    let clusters_simple = engine_simple.cluster_decisions(&entities, &decisions);

    // Simple union-find: A-B-C all merge transitively
    assert_eq!(clusters_simple.iter().filter(|c| c.len() > 1).count(), 1);
    assert_eq!(clusters_simple.iter().find(|c| c.len() > 1).unwrap().len(), 3);
}

#[test]
fn test_graph_fully_connected() {
    // High-confidence clique stays together
    let a = make_entity("a", "src", vec![("name", "alice")]);
    let b = make_entity("b", "src", vec![("name", "alice")]);
    let c = make_entity("c", "src", vec![("name", "alice")]);
    let entities = vec![a.clone(), b.clone(), c.clone()];

    let decisions = vec![
        make_decision(a.id, b.id, 0.95),
        make_decision(b.id, c.id, 0.92),
        make_decision(a.id, c.id, 0.90),
    ];

    let engine = make_engine_with_graph_clustering(0.0, 0.8, usize::MAX);
    let clusters = engine.cluster_decisions(&entities, &decisions);

    // avg(0.95, 0.92, 0.90) = 0.923 > 0.8 -> stays together
    let multi: Vec<&Vec<Uuid>> = clusters.iter().filter(|c| c.len() > 1).collect();
    assert_eq!(multi.len(), 1);
    assert_eq!(multi[0].len(), 3, "Fully connected high-confidence clique should stay together");
}

#[test]
fn test_graph_large_cluster_split() {
    // max_cluster_size forces coherence check on large clusters
    let entities: Vec<NormalizedEntity> = (0..5)
        .map(|i| make_entity(&format!("e{}", i), "src", vec![("name", "alice")]))
        .collect();

    // Chain: e0-e1(0.95), e1-e2(0.93), e2-e3(0.91), e3-e4(0.50)
    let decisions = vec![
        make_decision(entities[0].id, entities[1].id, 0.95),
        make_decision(entities[1].id, entities[2].id, 0.93),
        make_decision(entities[2].id, entities[3].id, 0.91),
        make_decision(entities[3].id, entities[4].id, 0.50),
    ];

    // max_cluster_size=3 forces coherence check, min_cluster_coherence=0.7
    let engine = make_engine_with_graph_clustering(0.0, 0.7, 3);
    let clusters = engine.cluster_decisions(&entities, &decisions);

    // The 5-node chain exceeds max_cluster_size=3.
    // Weakest edge (0.50) gets removed first.
    // Sub-component {e0,e1,e2,e3} still > 3, so check again.
    // Edges: 0.95, 0.93, 0.91 avg=0.93 > 0.7 but size=4 > 3.
    // Remove weakest (0.91), get {e0,e1,e2} and {e3}.
    // {e0,e1,e2}: size=3, edges avg(0.95,0.93)=0.94 > 0.7, OK.
    // {e3} singleton, {e4} singleton.
    let max_cluster_size = clusters.iter().map(|c| c.len()).max().unwrap_or(0);
    assert!(max_cluster_size <= 3, "No cluster should exceed max_cluster_size=3, got {}", max_cluster_size);
}

#[test]
fn test_graph_clustering_from_spec() {
    // Test clustering config parsed from YAML spec
    let yaml = r#"
api_version: kanoniv/v2
identity_version: test_v1

entity:
  name: customer

sources:
  - name: crm
    system: test
    table: contacts
    id: id
    attributes:
      email: email

rules:
  - type: exact
    name: email_exact
    field: email

decision:
  thresholds:
    match: 0.85
    review: 0.6
  clustering:
    method: graph
    min_edge_weight: 0.7
    min_cluster_coherence: 0.6
"#;

    let spec: IdentitySpec = serde_yaml::from_str(yaml).unwrap();
    let plan = SpecCompiler::compile(&spec).unwrap();

    assert!(plan.decision.clustering.is_some());
    let clustering = plan.decision.clustering.as_ref().unwrap();
    assert_eq!(clustering.method, ClusteringMethod::Graph);
    assert!((clustering.min_edge_weight - 0.7).abs() < f64::EPSILON);
    assert!((clustering.min_cluster_coherence - 0.6).abs() < f64::EPSILON);

    let engine = ReconciliationEngine::from_plan(&plan);
    assert!(engine.clustering_config.is_some());
    assert_eq!(engine.clustering_config.as_ref().unwrap().method, ClusteringMethod::Graph);
}

#[test]
fn test_graph_large_chain_no_stack_overflow() {
    // A long chain of 500 nodes with low coherence should not blow the stack.
    // Before the iterative fix, this would recurse ~500 levels deep.
    let entities: Vec<NormalizedEntity> = (0..500)
        .map(|i| make_entity(&format!("e{}", i), "src", vec![("name", &format!("name{}", i))]))
        .collect();

    // Chain: e0-e1(0.55), e1-e2(0.55), ... e498-e499(0.55)
    let decisions: Vec<MatchDecision> = (0..499)
        .map(|i| make_decision(entities[i].id, entities[i + 1].id, 0.55))
        .collect();

    // coherence=0.8 means all edges (0.55) are below threshold -> splits everything
    let engine = make_engine_with_graph_clustering(0.0, 0.8, usize::MAX);

    let start = std::time::Instant::now();
    let clusters = engine.cluster_decisions(&entities, &decisions);
    let elapsed = start.elapsed();

    // All should end up as singletons (each edge is below coherence)
    assert_eq!(clusters.len(), 500, "All 500 entities should be singletons");
    assert!(elapsed.as_secs() < 10, "Should complete in under 10 seconds, took {:?}", elapsed);
}

#[test]
fn test_graph_large_chain_with_max_cluster_size() {
    // 200-node chain with max_cluster_size=10 -> should split into ~20 clusters
    let entities: Vec<NormalizedEntity> = (0..200)
        .map(|i| make_entity(&format!("e{}", i), "src", vec![("name", "shared")]))
        .collect();

    // Chain with varying confidence
    let decisions: Vec<MatchDecision> = (0..199)
        .map(|i| make_decision(entities[i].id, entities[i + 1].id, 0.9))
        .collect();

    let engine = make_engine_with_graph_clustering(0.0, 0.0, 10);
    let clusters = engine.cluster_decisions(&entities, &decisions);

    for cluster in &clusters {
        assert!(cluster.len() <= 10,
            "Cluster has {} members, exceeds max_cluster_size=10", cluster.len());
    }
    // All 200 entities should be accounted for
    let total: usize = clusters.iter().map(|c| c.len()).sum();
    assert_eq!(total, 200, "All entities must be present in clusters");
}

#[test]
fn test_graph_no_edges_all_singletons() {
    // When no merge decisions, all entities become singletons
    let entities: Vec<NormalizedEntity> = (0..3)
        .map(|i| make_entity(&format!("e{}", i), "src", vec![("name", &format!("name{}", i))]))
        .collect();

    let decisions = vec![];
    let engine = make_engine_with_graph_clustering(0.0, 0.0, usize::MAX);
    let clusters = engine.cluster_decisions(&entities, &decisions);

    assert_eq!(clusters.len(), 3, "All entities should be singletons");
    for cluster in &clusters {
        assert_eq!(cluster.len(), 1);
    }
}
