//! YAML Parsing Tests for IdentitySpec
//! Tests YAML-001 thru YAML-007 (valid specs) and YAML-101 thru YAML-110 (invalid specs)

use cannon_common::spec::{IdentitySpec, RuleSpec};

// ============================================================================
// YAML-001: Minimal Valid Spec
// ============================================================================
#[test]
fn yaml_001_parse_minimal_valid_spec() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "customer_v1"
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_field
rules:
  - name: email_match
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;

    let spec = IdentitySpec::from_yaml(yaml);
    assert!(spec.is_ok(), "Minimal spec should parse: {:?}", spec.err());
    
    let spec = spec.unwrap();
    assert_eq!(spec.entity.name, "customer");
    assert_eq!(spec.sources.len(), 1);
    assert_eq!(spec.rules.len(), 1);
    assert_eq!(spec.decision.thresholds.match_threshold, 0.9);
}

// ============================================================================
// YAML-002: Full-Featured Spec
// ============================================================================
#[test]
fn yaml_002_parse_full_featured_spec() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "customer_v2"
metadata:
  owner: data-team
  description: "Customer identity resolution"

entity:
  name: customer
  compliance:
    retention_days: 365
    pii_fields: [email, phone]

sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
      phone: phone_number
      name: full_name
    temporal:
      valid_from: updated_at
      strategy: latest_only

  - name: erp
    system: sap
    table: customers
    id: cust_id
    attributes:
      email: email
      customer_id: id

reference_identifiers:
  - name: ssn
    field: ssn
    type: external
    conditions:
      - field: country
        operator: equals
        value: "US"

blocking:
  strategy: composite
  keys:
    - [email]
    - [phone]

rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
  - name: name_fuzzy
    type: similarity
    field: name
    algorithm: jaro_winkler
    threshold: 0.85
    weight: 0.8

survivorship:
  default_strategy: most_recent
  field_strategies:
    - field: email
      strategy: most_complete

relations:
  - name: household
    from_entity: customer
    to_entity: customer
    cardinality: many_to_many
    join_key: household_id
    propagate_match: true

decision:
  thresholds:
    match: 0.9
    review: 0.7
    reject: 0.3
"#;

    let spec = IdentitySpec::from_yaml(yaml);
    assert!(spec.is_ok(), "Full spec should parse: {:?}", spec.err());
    
    let spec = spec.unwrap();
    assert_eq!(spec.sources.len(), 2);
    assert!(spec.reference_identifiers.is_some());
    assert!(spec.blocking.is_some());
    assert!(spec.survivorship.is_some());
    assert!(spec.relations.is_some());
}

// ============================================================================
// YAML-003: Blocking Strategies
// ============================================================================
#[test]
fn yaml_003_parse_single_blocking_strategy() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
blocking:
  strategy: single
  keys:
    - [email]
rules:
  - name: r1
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let blocking = spec.blocking.unwrap();
    assert_eq!(blocking.strategy, cannon_common::spec::BlockingStrategy::Single);
}

#[test]
fn yaml_003_parse_lsh_blocking_strategy() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      bio: bio_text
blocking:
  strategy: lsh
  keys:
    - [bio]
rules:
  - name: r1
    type: exact
    field: bio
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let blocking = spec.blocking.unwrap();
    assert_eq!(blocking.strategy, cannon_common::spec::BlockingStrategy::Lsh);
}

// ============================================================================
// YAML-004: Rule Types
// ============================================================================
#[test]
fn yaml_004_parse_all_rule_types() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
      name: name
      age: age
rules:
  - name: exact_rule
    type: exact
    field: email
    weight: 1.0
  - name: similarity_rule
    type: similarity
    field: name
    algorithm: jaro_winkler
    threshold: 0.8
    weight: 0.8
  - name: range_rule
    type: range
    field: age
    tolerance: 5
    weight: 0.5
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    assert_eq!(spec.rules.len(), 3);
}

// ============================================================================
// YAML-005: Survivorship Strategies
// ============================================================================
#[test]
fn yaml_005_parse_survivorship_strategies() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
rules:
  - name: r1
    type: exact
    field: email
    weight: 1.0
survivorship:
  default_strategy: most_recent
  field_strategies:
    - field: email
      strategy: most_complete
    - field: phone
      strategy: source_priority
      source_order: [crm, erp]
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    assert!(spec.survivorship.is_some());
}

// ============================================================================
// YAML-006: Relations
// ============================================================================
#[test]
fn yaml_006_parse_relations_with_propagate() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
rules:
  - name: r1
    type: exact
    field: email
    weight: 1.0
relations:
  - name: household
    from_entity: customer
    to_entity: customer
    cardinality: many_to_many
    join_key: household_id
    propagate_match: true
  - name: company
    from_entity: customer
    to_entity: company
    cardinality: many_to_one
    join_key: company_id
    propagate_match: false
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let relations = spec.relations.unwrap();
    assert_eq!(relations.len(), 2);
    assert!(relations[0].propagate_match);
    assert!(!relations[1].propagate_match);
}

// ============================================================================
// YAML-007: Reference Identifiers with Conditions
// ============================================================================
#[test]
fn yaml_007_parse_reference_identifiers_with_conditions() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      ssn: ssn_field
      country: country_code
rules:
  - name: r1
    type: exact
    field: ssn
    weight: 1.0
reference_identifiers:
  - name: ssn
    field: ssn
    type: external
    conditions:
      - field: country
        operator: equals
        value: "US"
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).unwrap();
    let ref_ids = spec.reference_identifiers.unwrap();
    assert_eq!(ref_ids.len(), 1);
    assert!(!ref_ids[0].conditions.is_empty());
}

// ============================================================================
// YAML-101: Missing entity.name
// ============================================================================
#[test]
fn yaml_101_reject_missing_entity_name() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity: {}
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
rules:
  - name: r1
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml);
    assert!(spec.is_err(), "Should reject missing entity.name");
}

// ============================================================================
// YAML-102: Missing sources
// ============================================================================
#[test]
fn yaml_102_reject_missing_sources() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
rules:
  - name: r1
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml);
    assert!(spec.is_err(), "Should reject missing sources");
}

// ============================================================================
// YAML-104: Invalid rule type enum
// ============================================================================
#[test]
fn yaml_104_reject_invalid_rule_type() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
rules:
  - name: r1
    type: invalid_type_xyz
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml);
    assert!(spec.is_err(), "Should reject unknown rule type");
}

// ============================================================================
// YAML-107: Malformed YAML syntax
// ============================================================================
#[test]
fn yaml_107_reject_malformed_yaml() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
  bad_indent
sources: []
"#;
    let spec = IdentitySpec::from_yaml(yaml);
    assert!(spec.is_err(), "Should reject malformed YAML");
}

// ============================================================================
// YAML-200: Map-style sources
// ============================================================================
#[test]
fn yaml_200_parse_map_style_sources() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  crm:
    adapter: csv
    location: data/contacts.csv
    primary_key: id
    attributes:
      email: email_field
  erp:
    adapter: postgres
    location: public.customers
    primary_key: cust_id
    attributes:
      email: email
rules:
  - name: r1
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).expect("Map-style sources should parse");
    assert_eq!(spec.sources.len(), 2);

    // BTreeMap iterates in sorted order, so "crm" comes before "erp"
    let names: Vec<&str> = spec.sources.iter().map(|s| s.name.as_str()).collect();
    assert!(names.contains(&"crm"), "Should contain source named 'crm'");
    assert!(names.contains(&"erp"), "Should contain source named 'erp'");

    let crm = spec.sources.iter().find(|s| s.name == "crm").unwrap();
    assert_eq!(
        crm.adapter,
        Some(cannon_common::spec::AdapterType::Csv)
    );
    assert_eq!(crm.resolved_location(), "data/contacts.csv");
    assert_eq!(crm.resolved_primary_key(), "id");
}

// ============================================================================
// YAML-201: Array-style sources still work
// ============================================================================
#[test]
fn yaml_201_parse_array_style_sources_still_works() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    adapter: csv
    location: data/contacts.csv
    primary_key: id
    attributes:
      email: email_field
rules:
  - name: r1
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).expect("Array-style sources should still parse");
    assert_eq!(spec.sources.len(), 1);
    assert_eq!(spec.sources[0].name, "crm");
    assert_eq!(
        spec.sources[0].adapter,
        Some(cannon_common::spec::AdapterType::Csv)
    );
}

// ============================================================================
// YAML-202: fields array parses to field string
// ============================================================================
#[test]
fn yaml_202_fields_array_parses_to_field() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
      name: name
      age: age
rules:
  - name: exact_rule
    type: exact
    fields: [email]
    weight: 1.0
  - name: sim_rule
    type: similarity
    fields: [name]
    algorithm: jaro_winkler
    threshold: 0.85
    weight: 0.8
  - name: range_rule
    type: range
    fields: [age]
    tolerance: 5
    weight: 0.5
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).expect("fields array should parse");
    assert_eq!(spec.rules.len(), 3);

    match &spec.rules[0] {
        RuleSpec::Exact { field, .. } => assert_eq!(field, "email"),
        other => panic!("Expected Exact, got {:?}", other),
    }
    match &spec.rules[1] {
        RuleSpec::Similarity { field, .. } => assert_eq!(field, "name"),
        other => panic!("Expected Similarity, got {:?}", other),
    }
    match &spec.rules[2] {
        RuleSpec::Range { field, .. } => assert_eq!(field, "age"),
        other => panic!("Expected Range, got {:?}", other),
    }
}

// ============================================================================
// YAML-203: field string still works (backward compat)
// ============================================================================
#[test]
fn yaml_203_field_string_still_works() {
    let yaml = r#"
api_version: "kanoniv/v2"
identity_version: "v1"
entity:
  name: customer
sources:
  - name: crm
    system: sf
    table: t
    id: id
    attributes:
      email: email
rules:
  - name: exact_rule
    type: exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#;
    let spec = IdentitySpec::from_yaml(yaml).expect("field string should still parse");
    match &spec.rules[0] {
        RuleSpec::Exact { field, .. } => assert_eq!(field, "email"),
        other => panic!("Expected Exact, got {:?}", other),
    }
}
