//! Adapter-First + PII Masking Integration Tests
//!
//! Tests the full flow: adapter-first spec → compilation → reconciliation → PII safety.
//! These are pure unit tests (no DB/Redis required).

use uuid::Uuid;
use chrono::Utc;
use std::collections::{HashMap, HashSet};

use cannon_common::spec::IdentitySpec;
use cannon_core::compiler::SpecCompiler;
use cannon_core::types::{NormalizedEntity, Decision};
use cannon_core::overrides::OverrideResolver;
use cannon_core::ReconciliationEngine;

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

// ============================================================================
// APII-001: Full flow — adapter spec compiles and runs reconciliation
// ============================================================================
#[test]
fn apii_001_adapter_spec_full_reconciliation() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: contact_id
    schema:
      email:
        type: string
        pii: true
      name:
        type: string
    attributes:
      email: email
      name: name
  - name: billing
    adapter: snowflake
    location: billing.public.accounts
    primary_key: account_id
    schema:
      email:
        type: string
        pii: true
      phone:
        type: string
        pii: true
    attributes:
      email: email
      phone: phone
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.8
"#;

    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let plan = SpecCompiler::compile(&spec).expect("Adapter spec should compile");
    let engine = ReconciliationEngine::from_plan(&plan);

    // Verify PII fields extracted from both sources
    let crm_pii = &plan.sources[0].pii_fields;
    assert!(crm_pii.contains(&"email".to_string()));
    let billing_pii = &plan.sources[1].pii_fields;
    assert!(billing_pii.contains(&"email".to_string()));
    assert!(billing_pii.contains(&"phone".to_string()));

    // Create matching entities from different adapter sources
    let e1 = make_entity("crm", vec![("email", "john@example.com"), ("name", "John Doe")]);
    let e2 = make_entity("billing", vec![("email", "john@example.com"), ("phone", "555-1234")]);

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

    assert!(!decisions.is_empty(), "Should produce at least one decision");
    let merge_count = decisions.iter().filter(|d| d.decision == Decision::Merge).count();
    assert!(merge_count > 0, "Email match should produce a merge");
}

// ============================================================================
// APII-002: Source-schema PII fields propagate into plan
// ============================================================================
#[test]
fn apii_002_source_schema_pii_propagation() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
  compliance:
    frameworks: [gdpr, hipaa]
    pii_fields: [ssn]
    audit_required: true
sources:
  - name: medical
    adapter: postgres
    location: public.patients
    primary_key: patient_id
    schema:
      ssn:
        type: string
        pii: true
      email:
        type: string
        pii: true
      diagnosis:
        type: string
        pii: true
      name:
        type: string
    attributes:
      ssn: social_security
      email: email_address
      name: patient_name
rules:
  - name: ssn_exact
    type: exact
    field: ssn
    weight: 1.0
decision:
  thresholds:
    match: 0.95
"#;

    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let plan = SpecCompiler::compile(&spec).expect("HIPAA spec should compile");

    // Verify compliance-level PII
    let compliance_pii: HashSet<String> = plan.compliance.as_ref()
        .map(|c| c.pii_fields.iter().cloned().collect())
        .unwrap_or_default();
    assert!(compliance_pii.contains("ssn"));

    // Verify source-level PII
    let source_pii: HashSet<String> = plan.sources[0].pii_fields.iter().cloned().collect();
    assert!(source_pii.contains("ssn"));
    assert!(source_pii.contains("email"));
    assert!(source_pii.contains("diagnosis"));

    // Merge both (as the pipeline does)
    let mut all_pii = compliance_pii;
    for source in &plan.sources {
        all_pii.extend(source.pii_fields.iter().cloned());
    }
    assert_eq!(all_pii.len(), 3); // ssn, email, diagnosis (ssn deduped)
    assert!(all_pii.contains("ssn"));
    assert!(all_pii.contains("email"));
    assert!(all_pii.contains("diagnosis"));
}

// ============================================================================
// APII-003: Reconciliation results contain PII in rule_results (pre-masking)
// ============================================================================
#[test]
fn apii_003_reconcile_results_contain_pii_before_masking() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: src_a
    adapter: postgres
    location: public.contacts
    primary_key: id
    schema:
      email:
        type: string
        pii: true
    attributes:
      email: email
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.8
"#;

    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let plan = SpecCompiler::compile(&spec).unwrap();
    let engine = ReconciliationEngine::from_plan(&plan);

    let e1 = make_entity("src_a", vec![("email", "sensitive@example.com")]);
    let e2 = make_entity("src_a", vec![("email", "sensitive@example.com")]);

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

    assert!(!decisions.is_empty());
    let d = &decisions[0];
    assert_eq!(d.decision, Decision::Merge);

    // Rule results may contain the PII field name in explanation
    // This is expected — masking happens in the pipeline BEFORE persistence
    let has_email_rule = d.rule_results.iter().any(|r| r.rule_name.contains("email"));
    assert!(has_email_rule, "Rule results should reference PII field (pre-masking)");
}

// ============================================================================
// APII-004: Mixed adapter sources reconcile correctly
// ============================================================================
#[test]
fn apii_004_mixed_adapter_sources() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: postgres_source
    adapter: postgres
    location: public.contacts
    primary_key: id
    tags: [production, primary]
    attributes:
      email: email
      name: name
  - name: csv_source
    adapter: csv
    location: s3://bucket/customers.csv
    primary_key: row_id
    tags: [staging, secondary]
    attributes:
      email: email
      name: full_name
  - name: legacy_source
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
blocking:
  strategy: composite
  keys:
    - [email]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.8
"#;

    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let plan = SpecCompiler::compile(&spec).expect("Mixed adapter spec should compile");

    assert_eq!(plan.sources.len(), 3);
    assert_eq!(plan.sources[0].adapter, "postgres");
    assert_eq!(plan.sources[1].adapter, "csv");
    assert_eq!(plan.sources[2].adapter, "salesforce"); // Legacy fallback

    assert_eq!(plan.sources[0].location, "public.contacts");
    assert_eq!(plan.sources[1].location, "s3://bucket/customers.csv");
    assert_eq!(plan.sources[2].location, "contacts"); // Legacy fallback

    assert_eq!(plan.sources[0].tags, vec!["production", "primary"]);
    assert_eq!(plan.sources[1].tags, vec!["staging", "secondary"]);
    assert!(plan.sources[2].tags.is_empty());

    // All three should reconcile
    let engine = ReconciliationEngine::from_plan(&plan);
    let e1 = make_entity("postgres_source", vec![("email", "shared@example.com"), ("name", "Alice")]);
    let e2 = make_entity("csv_source", vec![("email", "shared@example.com"), ("name", "Alice B")]);
    let e3 = make_entity("legacy_source", vec![("email", "shared@example.com")]);

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

    let merge_count = decisions.iter().filter(|d| d.decision == Decision::Merge).count();
    assert!(merge_count >= 2, "Three entities with same email should produce merges");
}

// ============================================================================
// APII-005: Sampling config compiled correctly
// ============================================================================
#[test]
fn apii_005_sampling_config_roundtrip() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: large_source
    adapter: bigquery
    location: project.dataset.table
    primary_key: id
    sampling:
      strategy: stratified
      rate: 0.25
    attributes:
      email: email
  - name: full_source
    adapter: postgres
    location: public.contacts
    primary_key: id
    attributes:
      email: email
rules:
  - name: email_match
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;

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

    // First source has sampling
    let samp = plan.sources[0].sampling.as_ref().unwrap();
    assert_eq!(samp.strategy, "stratified");
    assert!((samp.rate - 0.25).abs() < f64::EPSILON);

    // Second source has no sampling
    assert!(plan.sources[1].sampling.is_none());
}

// ============================================================================
// APII-006: Governance config roundtrip
// ============================================================================
#[test]
fn apii_006_governance_config_roundtrip() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    tags:
      - production
      - verified
    attributes:
      email: email
rules:
  - name: email_match
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
governance:
  require_freshness: true
  require_schema: false
  required_tags:
    - production
"#;

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

    let gov = plan.governance.as_ref().expect("Governance should compile");
    assert!(gov.require_freshness);
    assert!(!gov.require_schema);
    assert_eq!(gov.required_tags, vec!["production"]);
}

// ============================================================================
// APII-007: Schema drift detection data — verify schema is available in plan
// ============================================================================
#[test]
fn apii_007_schema_available_for_drift_detection() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    schema:
      email:
        type: string
        pii: true
        nullable: false
      name:
        type: string
        nullable: true
      created_at:
        type: timestamp
    attributes:
      email: email
      name: name
rules:
  - name: email_match
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;

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

    let schema = plan.sources[0].schema.as_ref().unwrap();
    let expected_fields: HashSet<String> = ["email", "name", "created_at"]
        .iter().map(|s| s.to_string()).collect();
    let actual_fields: HashSet<String> = schema.keys().cloned().collect();
    assert_eq!(actual_fields, expected_fields);

    // Simulate schema drift detection (as the pipeline does)
    let entity = make_entity("crm", vec![
        ("email", "test@example.com"),
        ("name", "Test"),
        // "created_at" is missing — drift!
        ("extra_field", "unexpected"), // extra field — drift!
    ]);

    let actual_data_fields: HashSet<&String> = entity.data.keys().collect();
    let expected_schema_fields: HashSet<&String> = schema.keys().collect();

    let missing: Vec<_> = expected_schema_fields.difference(&actual_data_fields).collect();
    let extra: Vec<_> = actual_data_fields.difference(&expected_schema_fields).collect();

    assert_eq!(missing.len(), 1); // created_at
    assert!(missing.iter().any(|f| f.as_str() == "created_at"));
    assert_eq!(extra.len(), 1); // extra_field
    assert!(extra.iter().any(|f| f.as_str() == "extra_field"));
}
