//! Integration tests for the semantic resolution (typed resolution graph) engine.
//!
//! Tests the full pipeline: spec -> compile -> engine -> evaluate -> cluster,
//! verifying that subtype inference and constraint matrix produce correct
//! ResolutionDecision values and clustering behavior.

use cannon_common::spec::IdentitySpec;
use cannon_core::engine::ReconciliationEngine;
use cannon_core::engine::types::{NormalizedEntity, ResolutionDecision, Decision};
use cannon_core::engine::compiler::SpecCompiler;
use cannon_core::engine::overrides::OverrideResolver;
use std::collections::HashMap;
use uuid::Uuid;
use chrono::Utc;

fn make_entity(ext_id: &str, data: Vec<(&str, &str)>) -> NormalizedEntity {
    NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: Uuid::new_v4(),
        external_id: ext_id.to_string(),
        entity_type: "product".to_string(),
        data: data.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
        source_name: "test".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: Utc::now(),
    }
}

/// A product spec with subtypes and constraints that produces Relate decisions.
fn product_spec_yaml() -> &'static str {
    r#"
api_version: kanoniv/v2
identity_version: product_v1
entity:
  name: product
  subtypes:
    - name: sellable_sku
      priority: 10
      conditions:
        - field: barcode
          operator: present
        - field: mpn
          operator: present
    - name: product_variant
      priority: 5
      conditions:
        - field: color
          operator: present
        - field: brand
          operator: present
    - name: product_model
      priority: 1
      conditions:
        - field: brand
          operator: present
  constraints:
    - subtypes: [product_model, product_variant]
      action: relate
      link_type: variant_of
      direction: b_to_a
    - subtypes: [product_model, product_model]
      action: merge
    - subtypes: [product_variant, product_variant]
      action: merge
    - subtypes: [product_variant, sellable_sku]
      action: merge
  subtype_confidence_threshold: 0.7
sources:
  - name: catalog
    adapter: csv
    location: products.csv
    primary_key: id
    attributes:
      brand: brand
      product_name: product_name
      barcode: barcode
      mpn: mpn
      color: color
blocking:
  strategy: composite
  keys:
    - [brand]
rules:
  - name: brand
    type: exact
    field: brand
    weight: 0.5
  - name: product_name
    type: similarity
    field: product_name
    algorithm: jaro_winkler
    threshold: 0.8
    weight: 0.5
decision:
  thresholds:
    match: 0.5
    review: 0.3
"#
}

/// A person spec with NO subtypes - should work exactly like before.
fn person_spec_yaml() -> &'static str {
    r#"
api_version: kanoniv/v2
identity_version: person_v1
entity:
  name: person
sources:
  - name: crm
    adapter: csv
    location: people.csv
    primary_key: id
    attributes:
      email: email
      first_name: first_name
      last_name: last_name
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.8
    review: 0.5
"#
}

#[test]
fn test_product_spec_with_subtypes_produces_relate() {
    let spec: IdentitySpec = serde_yaml::from_str(product_spec_yaml()).unwrap();
    let plan = SpecCompiler::compile(&spec).unwrap();

    // Verify subtype rules were compiled
    assert_eq!(plan.subtype_rules.len(), 3);
    assert_eq!(plan.constraint_matrix.len(), 4);
    assert!((plan.subtype_confidence_threshold - 0.7).abs() < f64::EPSILON);

    let engine = ReconciliationEngine::from_plan(&plan);
    assert!(engine.semantic_engine.is_active());

    // Create a model entity (brand only) and a variant entity (brand + color)
    let model = make_entity("model1", vec![
        ("brand", "samsung"),
        ("product_name", "samsung galaxy"),
    ]);
    let variant = make_entity("variant1", vec![
        ("brand", "samsung"),
        ("color", "blue"),
        ("product_name", "samsung galaxy"),
    ]);

    let resolver = OverrideResolver::new(vec![]);
    let decision = engine.evaluate_pair(&model, &variant, &resolver, 1.0);

    // Model (brand present) inferred as product_model
    assert!(decision.subtype_a.is_some());
    assert_eq!(decision.subtype_a.as_ref().unwrap().subtype, "product_model");

    // Variant (brand + color present) inferred as product_variant
    assert!(decision.subtype_b.is_some());
    assert_eq!(decision.subtype_b.as_ref().unwrap().subtype, "product_variant");

    // Constraint (model, variant) -> Relate with variant_of
    assert!(
        matches!(decision.resolution, ResolutionDecision::Relate { ref link_type, .. } if link_type == "variant_of"),
        "Expected Relate(variant_of), got {:?}",
        decision.resolution
    );
}

#[test]
fn test_person_spec_no_subtypes_unchanged() {
    let spec: IdentitySpec = serde_yaml::from_str(person_spec_yaml()).unwrap();
    let plan = SpecCompiler::compile(&spec).unwrap();

    // No subtype rules compiled
    assert!(plan.subtype_rules.is_empty());
    assert!(plan.constraint_matrix.is_empty());

    let engine = ReconciliationEngine::from_plan(&plan);
    assert!(!engine.semantic_engine.is_active());

    // Two persons with matching email
    let person_a = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: Uuid::new_v4(),
        external_id: "p1".to_string(),
        entity_type: "person".to_string(),
        data: HashMap::from([
            ("email".to_string(), "bob@example.com".to_string()),
            ("first_name".to_string(), "bob".to_string()),
            ("last_name".to_string(), "smith".to_string()),
        ]),
        source_name: "crm".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: Utc::now(),
    };
    let person_b = NormalizedEntity {
        id: Uuid::new_v4(),
        tenant_id: Uuid::new_v4(),
        external_id: "p2".to_string(),
        entity_type: "person".to_string(),
        data: HashMap::from([
            ("email".to_string(), "bob@example.com".to_string()),
            ("first_name".to_string(), "robert".to_string()),
            ("last_name".to_string(), "smith".to_string()),
        ]),
        source_name: "crm".to_string(),
        valid_from: None,
        valid_to: None,
        last_updated: Utc::now(),
    };

    let resolver = OverrideResolver::new(vec![]);
    let decision = engine.evaluate_pair(&person_a, &person_b, &resolver, 1.0);

    // No subtypes inferred
    assert!(decision.subtype_a.is_none());
    assert!(decision.subtype_b.is_none());

    // Standard merge
    assert_eq!(decision.decision, Decision::Merge);
    assert_eq!(decision.resolution, ResolutionDecision::Merge);
}

#[test]
fn test_relate_decisions_not_clustered() {
    let spec: IdentitySpec = serde_yaml::from_str(product_spec_yaml()).unwrap();
    let plan = SpecCompiler::compile(&spec).unwrap();
    let engine = ReconciliationEngine::from_plan(&plan);

    // Model and variant entities that will get Relate decisions
    let model = make_entity("model1", vec![
        ("brand", "samsung"),
        ("product_name", "samsung galaxy"),
    ]);
    let variant = make_entity("variant1", vec![
        ("brand", "samsung"),
        ("color", "blue"),
        ("product_name", "samsung galaxy"),
    ]);

    let entities = vec![model.clone(), variant.clone()];

    let resolver = OverrideResolver::new(vec![]);
    let (decisions, _) = engine.reconcile_with_telemetry(&entities, &resolver);

    // Should have at least one decision
    let relate_decisions: Vec<_> = decisions.iter()
        .filter(|d| matches!(d.resolution, ResolutionDecision::Relate { .. }))
        .collect();

    // Relate decisions should exist
    assert!(!relate_decisions.is_empty(), "Expected Relate decisions but got none");

    // Cluster: Relate decisions should NOT merge the entities
    let clusters = engine.cluster_decisions(&entities, &decisions);

    // Each entity should be in its own cluster (not merged)
    let non_singleton_clusters: Vec<_> = clusters.iter().filter(|c| c.len() > 1).collect();
    assert!(
        non_singleton_clusters.is_empty(),
        "Relate decisions should not create merged clusters, found {:?}",
        non_singleton_clusters
    );
}

#[test]
fn test_merge_decisions_still_clustered() {
    let spec: IdentitySpec = serde_yaml::from_str(product_spec_yaml()).unwrap();
    let plan = SpecCompiler::compile(&spec).unwrap();
    let engine = ReconciliationEngine::from_plan(&plan);

    // Two variant entities with same brand and color - should merge
    let variant_a = make_entity("var1", vec![
        ("brand", "samsung"),
        ("color", "blue"),
        ("product_name", "samsung galaxy blue"),
    ]);
    let variant_b = make_entity("var2", vec![
        ("brand", "samsung"),
        ("color", "blue"),
        ("product_name", "samsung galaxy blue"),
    ]);

    let entities = vec![variant_a.clone(), variant_b.clone()];
    let resolver = OverrideResolver::new(vec![]);
    let (decisions, _) = engine.reconcile_with_telemetry(&entities, &resolver);

    // Check that merge decisions with ResolutionDecision::Merge exist
    let merge_decisions: Vec<_> = decisions.iter()
        .filter(|d| d.resolution == ResolutionDecision::Merge)
        .collect();
    assert!(!merge_decisions.is_empty(), "Expected Merge decisions");

    // Cluster: Merge decisions should cluster the entities together
    let clusters = engine.cluster_decisions(&entities, &decisions);
    let multi_clusters: Vec<_> = clusters.iter().filter(|c| c.len() > 1).collect();
    assert!(
        !multi_clusters.is_empty(),
        "Merge decisions should create merged clusters"
    );
}

#[test]
fn test_mixed_merge_and_relate_same_run() {
    let spec: IdentitySpec = serde_yaml::from_str(product_spec_yaml()).unwrap();
    let plan = SpecCompiler::compile(&spec).unwrap();
    let engine = ReconciliationEngine::from_plan(&plan);

    // Model entity
    let model = make_entity("model1", vec![
        ("brand", "samsung"),
        ("product_name", "samsung galaxy"),
    ]);
    // Two variant entities (should merge with each other, relate to model)
    let variant_a = make_entity("var1", vec![
        ("brand", "samsung"),
        ("color", "blue"),
        ("product_name", "samsung galaxy"),
    ]);
    let variant_b = make_entity("var2", vec![
        ("brand", "samsung"),
        ("color", "blue"),
        ("product_name", "samsung galaxy"),
    ]);

    let entities = vec![model.clone(), variant_a.clone(), variant_b.clone()];
    let resolver = OverrideResolver::new(vec![]);
    let (decisions, _) = engine.reconcile_with_telemetry(&entities, &resolver);

    // Count decision types
    let merge_count = decisions.iter()
        .filter(|d| d.resolution == ResolutionDecision::Merge)
        .count();
    let relate_count = decisions.iter()
        .filter(|d| matches!(d.resolution, ResolutionDecision::Relate { .. }))
        .count();

    // Should have both merge and relate decisions in the same run
    // variant_a + variant_b -> Merge (both product_variant)
    // model + variant_a -> Relate (model vs variant)
    // model + variant_b -> Relate (model vs variant)
    assert!(merge_count > 0, "Expected merge decisions, got 0");
    assert!(relate_count > 0, "Expected relate decisions, got 0");

    // Cluster: only the two variants should be merged
    let clusters = engine.cluster_decisions(&entities, &decisions);
    let merged_cluster = clusters.iter().find(|c| c.len() > 1);
    assert!(merged_cluster.is_some(), "Two variants should be in one cluster");

    let merged = merged_cluster.unwrap();
    // The merged cluster should contain variant_a and variant_b, NOT the model
    assert_eq!(merged.len(), 2, "Merged cluster should have exactly 2 members");
    assert!(
        !merged.contains(&model.id),
        "Model should NOT be in the merged cluster (it's related, not merged)"
    );
}
