//! Validation module for Identity Specifications.
//!
//! Provides semantic and safety validation beyond basic schema parsing.

use crate::spec::*;
use crate::ir::{IdentityPlan, ScoringMethod};
use crate::models::TenantTier;
use std::collections::HashSet;

/// Validation error with context.
#[derive(Debug, Clone)]
pub struct ValidationError {
    pub path: String,
    pub message: String,
    pub severity: Severity,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Severity {
    Error,
    Warning,
}

impl std::fmt::Display for ValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}] {}: {}", 
            match self.severity {
                Severity::Error => "ERROR",
                Severity::Warning => "WARN",
            },
            self.path,
            self.message
        )
    }
}

impl std::error::Error for ValidationError {}

/// Validation result.
pub type ValidationResult = Result<Vec<ValidationError>, Vec<ValidationError>>;

/// Safety limits for spec validation.
pub struct SafetyLimits {
    pub max_sources: usize,
    pub max_rules: usize,
    pub max_blocking_keys: usize,
    pub max_hierarchy_depth: u8,
}

impl Default for SafetyLimits {
    fn default() -> Self {
        Self {
            max_sources: 10,
            max_rules: 50,
            max_blocking_keys: 5,
            max_hierarchy_depth: 10,
        }
    }
}

/// Validator for identity specifications.
pub struct SpecValidator {
    limits: SafetyLimits,
}

impl SpecValidator {
    pub fn new() -> Self {
        Self {
            limits: SafetyLimits::default(),
        }
    }

    pub fn with_limits(limits: SafetyLimits) -> Self {
        Self { limits }
    }

    /// Validate the entire specification.
    /// Returns Ok with warnings if valid, Err with errors if invalid.
    pub fn validate(&self, spec: &IdentitySpec) -> ValidationResult {
        let mut errors = Vec::new();
        let mut warnings = Vec::new();

        // Semantic validations
        self.validate_api_version(spec, &mut errors);
        self.validate_sources(spec, &mut errors, &mut warnings);
        self.validate_rules(spec, &mut errors);
        self.validate_field_references(spec, &mut errors);
        self.validate_unique_names(spec, &mut errors);
        self.validate_weight_bounds(spec, &mut errors);
        self.validate_threshold_bounds(spec, &mut errors);
        
        // Safety validations
        self.validate_safety_limits(spec, &mut errors);

        // Hierarchy validations
        if let Some(ref hierarchy) = spec.entity.hierarchy {
            self.validate_hierarchy(hierarchy, &mut errors);
        }

        // Blocking validations
        if let Some(ref blocking) = spec.blocking {
            self.validate_blocking(blocking, &mut errors);
        }

        if errors.is_empty() {
            Ok(warnings)
        } else {
            Err(errors)
        }
    }

    fn validate_api_version(&self, spec: &IdentitySpec, errors: &mut Vec<ValidationError>) {
        if !spec.api_version.starts_with("kanoniv/") {
            errors.push(ValidationError {
                path: "api_version".to_string(),
                message: format!("Invalid API version '{}'. Expected 'kanoniv/vX'", spec.api_version),
                severity: Severity::Error,
            });
        }
    }

    fn validate_sources(
        &self,
        spec: &IdentitySpec,
        errors: &mut Vec<ValidationError>,
        warnings: &mut Vec<ValidationError>,
    ) {
        if spec.sources.is_empty() {
            errors.push(ValidationError {
                path: "sources".to_string(),
                message: "At least one source is required".to_string(),
                severity: Severity::Error,
            });
        }

        let valid_field_types = ["string", "integer", "float", "boolean", "date", "timestamp", "json"];

        for (i, source) in spec.sources.iter().enumerate() {
            let path = format!("sources[{}]", i);

            if source.attributes.is_empty() {
                warnings.push(ValidationError {
                    path: format!("{}.attributes", path),
                    message: "Source has no attribute mappings".to_string(),
                    severity: Severity::Warning,
                });
            }

            // Must have either adapter+location+primary_key OR legacy system+table+id
            let has_new = source.adapter.is_some() || source.location.is_some() || source.primary_key.is_some();
            let has_legacy = source.system.is_some() || source.table.is_some() || source.id.is_some();
            if !has_new && !has_legacy {
                errors.push(ValidationError {
                    path: path.clone(),
                    message: "Source must have either adapter/location/primary_key or legacy system/table/id fields".to_string(),
                    severity: Severity::Error,
                });
            }

            // Warehouse adapters require location
            if let Some(ref adapter) = source.adapter {
                let warehouse_adapters = [
                    AdapterType::Snowflake,
                    AdapterType::Bigquery,
                    AdapterType::Redshift,
                    AdapterType::Databricks,
                    AdapterType::Postgres,
                ];
                if warehouse_adapters.contains(adapter) && source.location.is_none() && source.table.is_none() {
                    errors.push(ValidationError {
                        path: format!("{}.location", path),
                        message: format!("Warehouse adapter {:?} requires a location", adapter),
                        severity: Severity::Error,
                    });
                }
            }

            // Schema type validation
            if let Some(ref schema) = source.schema {
                for (field, field_schema) in schema {
                    if !valid_field_types.contains(&field_schema.field_type.as_str()) {
                        errors.push(ValidationError {
                            path: format!("{}.schema.{}.type", path, field),
                            message: format!(
                                "Invalid field type '{}'. Must be one of: {}",
                                field_schema.field_type,
                                valid_field_types.join(", ")
                            ),
                            severity: Severity::Error,
                        });
                    }
                }
            }

            // Sampling rate bounds
            if let Some(ref sampling) = source.sampling {
                if sampling.rate <= 0.0 || sampling.rate > 1.0 {
                    errors.push(ValidationError {
                        path: format!("{}.sampling.rate", path),
                        message: format!("Sampling rate must be between 0 (exclusive) and 1 (inclusive), got {}", sampling.rate),
                        severity: Severity::Error,
                    });
                }
            }

            // Tag validation: lowercase alphanumeric + hyphens, max 20
            if source.tags.len() > 20 {
                errors.push(ValidationError {
                    path: format!("{}.tags", path),
                    message: format!("Too many tags: {} (max 20)", source.tags.len()),
                    severity: Severity::Error,
                });
            }
            for tag in &source.tags {
                if !tag.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
                    errors.push(ValidationError {
                        path: format!("{}.tags", path),
                        message: format!("Tag '{}' must be lowercase alphanumeric with hyphens only", tag),
                        severity: Severity::Error,
                    });
                }
            }

            // PII cross-reference warning
            if !source.pii_fields().is_empty() && spec.entity.compliance.is_none() {
                warnings.push(ValidationError {
                    path: format!("{}.schema", path),
                    message: "Source declares PII fields but no compliance config is set on the entity".to_string(),
                    severity: Severity::Warning,
                });
            }
        }

        // Governance: required_tags check
        if let Some(ref governance) = spec.governance {
            for (i, source) in spec.sources.iter().enumerate() {
                for required_tag in &governance.required_tags {
                    if !source.tags.contains(required_tag) {
                        errors.push(ValidationError {
                            path: format!("sources[{}].tags", i),
                            message: format!("Source '{}' missing required governance tag '{}'", source.name, required_tag),
                            severity: Severity::Error,
                        });
                    }
                }
            }
        }
    }

    fn validate_rules(&self, spec: &IdentitySpec, errors: &mut Vec<ValidationError>) {
        if spec.rules.is_empty() {
            errors.push(ValidationError {
                path: "rules".to_string(),
                message: "At least one rule is required".to_string(),
                severity: Severity::Error,
            });
        }

        for (i, rule) in spec.rules.iter().enumerate() {
            self.validate_rule(rule, &format!("rules[{}]", i), errors);
        }
    }

    fn validate_rule(&self, rule: &RuleSpec, path: &str, errors: &mut Vec<ValidationError>) {
        match rule {
            RuleSpec::Similarity { threshold, .. } => {
                if *threshold < 0.0 || *threshold > 1.0 {
                    errors.push(ValidationError {
                        path: format!("{}.threshold", path),
                        message: format!("Threshold must be between 0 and 1, got {}", threshold),
                        severity: Severity::Error,
                    });
                }
            }
            RuleSpec::Composite { children, .. } => {
                for (i, child) in children.iter().enumerate() {
                    self.validate_rule(child, &format!("{}.children[{}]", path, i), errors);
                }
            }
            _ => {}
        }
    }

    fn validate_field_references(&self, spec: &IdentitySpec, errors: &mut Vec<ValidationError>) {
        // Collect all available fields from sources
        let available_fields: HashSet<String> = spec
            .sources
            .iter()
            .flat_map(|s| s.attributes.keys().cloned())
            .collect();

        // Check rule field references
        for (i, rule) in spec.rules.iter().enumerate() {
            self.check_rule_fields(rule, &format!("rules[{}]", i), &available_fields, errors);
        }
    }

    fn check_rule_fields(
        &self,
        rule: &RuleSpec,
        path: &str,
        available_fields: &HashSet<String>,
        errors: &mut Vec<ValidationError>,
    ) {
        let field = match rule {
            RuleSpec::Exact { field, .. } => Some(field),
            RuleSpec::Similarity { field, .. } => Some(field),
            RuleSpec::Range { field, .. } => Some(field),
            RuleSpec::Composite { children, .. } => {
                for (i, child) in children.iter().enumerate() {
                    self.check_rule_fields(
                        child,
                        &format!("{}.children[{}]", path, i),
                        available_fields,
                        errors,
                    );
                }
                None
            }
            RuleSpec::Ml { .. } => None,
        };

        if let Some(field) = field {
            if !available_fields.contains(field) {
                errors.push(ValidationError {
                    path: format!("{}.field", path),
                    message: format!("Field '{}' not found in any source attributes", field),
                    severity: Severity::Error,
                });
            }
        }
    }

    fn validate_unique_names(&self, spec: &IdentitySpec, errors: &mut Vec<ValidationError>) {
        let mut source_names = HashSet::new();
        for (i, source) in spec.sources.iter().enumerate() {
            if !source_names.insert(&source.name) {
                errors.push(ValidationError {
                    path: format!("sources[{}].name", i),
                    message: format!("Duplicate source name '{}'", source.name),
                    severity: Severity::Error,
                });
            }
        }

        let mut rule_names = HashSet::new();
        for (i, rule) in spec.rules.iter().enumerate() {
            let name = self.get_rule_name(rule);
            if !rule_names.insert(name.clone()) {
                errors.push(ValidationError {
                    path: format!("rules[{}].name", i),
                    message: format!("Duplicate rule name '{}'", name),
                    severity: Severity::Error,
                });
            }
        }
    }

    fn get_rule_name(&self, rule: &RuleSpec) -> String {
        match rule {
            RuleSpec::Exact { name, .. } => name.clone(),
            RuleSpec::Similarity { name, .. } => name.clone(),
            RuleSpec::Range { name, .. } => name.clone(),
            RuleSpec::Composite { name, .. } => name.clone(),
            RuleSpec::Ml { name, .. } => name.clone(),
        }
    }

    fn validate_weight_bounds(&self, spec: &IdentitySpec, errors: &mut Vec<ValidationError>) {
        for (i, rule) in spec.rules.iter().enumerate() {
            self.check_rule_weight(rule, &format!("rules[{}]", i), errors);
        }
    }

    fn check_rule_weight(&self, rule: &RuleSpec, path: &str, errors: &mut Vec<ValidationError>) {
        let weight = match rule {
            RuleSpec::Exact { weight, .. } => Some(*weight),
            RuleSpec::Similarity { weight, .. } => Some(*weight),
            RuleSpec::Range { weight, .. } => Some(*weight),
            RuleSpec::Ml { weight, .. } => Some(*weight),
            RuleSpec::Composite { children, .. } => {
                for (i, child) in children.iter().enumerate() {
                    self.check_rule_weight(child, &format!("{}.children[{}]", path, i), errors);
                }
                None
            }
        };

        if let Some(w) = weight {
            if w < 0.0 || w > 1.0 {
                errors.push(ValidationError {
                    path: format!("{}.weight", path),
                    message: format!("Weight must be between 0 and 1, got {}", w),
                    severity: Severity::Error,
                });
            }
        }
    }

    fn validate_threshold_bounds(&self, spec: &IdentitySpec, errors: &mut Vec<ValidationError>) {
        let thresholds = &spec.decision.thresholds;
        
        if thresholds.match_threshold < 0.0 || thresholds.match_threshold > 1.0 {
            errors.push(ValidationError {
                path: "decision.thresholds.match".to_string(),
                message: format!("Match threshold must be between 0 and 1, got {}", thresholds.match_threshold),
                severity: Severity::Error,
            });
        }

        if let Some(review) = thresholds.review {
            if review < 0.0 || review > 1.0 {
                errors.push(ValidationError {
                    path: "decision.thresholds.review".to_string(),
                    message: format!("Review threshold must be between 0 and 1, got {}", review),
                    severity: Severity::Error,
                });
            }
            if review > thresholds.match_threshold {
                errors.push(ValidationError {
                    path: "decision.thresholds.review".to_string(),
                    message: "Review threshold must be less than or equal to match threshold".to_string(),
                    severity: Severity::Error,
                });
            }
        }
    }

    fn validate_safety_limits(&self, spec: &IdentitySpec, errors: &mut Vec<ValidationError>) {
        if spec.sources.len() > self.limits.max_sources {
            errors.push(ValidationError {
                path: "sources".to_string(),
                message: format!(
                    "Too many sources: {} (max: {})",
                    spec.sources.len(),
                    self.limits.max_sources
                ),
                severity: Severity::Error,
            });
        }

        if spec.rules.len() > self.limits.max_rules {
            errors.push(ValidationError {
                path: "rules".to_string(),
                message: format!(
                    "Too many rules: {} (max: {})",
                    spec.rules.len(),
                    self.limits.max_rules
                ),
                severity: Severity::Error,
            });
        }
    }

    fn validate_hierarchy(&self, hierarchy: &HierarchySpec, errors: &mut Vec<ValidationError>) {
        if hierarchy.depth > self.limits.max_hierarchy_depth {
            errors.push(ValidationError {
                path: "entity.hierarchy.depth".to_string(),
                message: format!(
                    "Hierarchy depth {} exceeds maximum of {}",
                    hierarchy.depth, self.limits.max_hierarchy_depth
                ),
                severity: Severity::Error,
            });
        }
    }

    /// Validate that the spec doesn't use enterprise features that exceed the tenant's tier.
    pub fn validate_tier(&self, spec: &IdentitySpec, tier: &TenantTier) -> Vec<ValidationError> {
        let mut errors = Vec::new();

        if spec.entity.compliance.is_some() && !tier.has_compliance() {
            errors.push(ValidationError {
                path: "entity.compliance".to_string(),
                message: format!("Compliance features require Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.entity.hierarchy.is_some() && !tier.has_hierarchy() {
            errors.push(ValidationError {
                path: "entity.hierarchy".to_string(),
                message: format!("Hierarchy features require Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.sources.iter().any(|s| s.freshness.is_some()) && !tier.has_freshness() {
            errors.push(ValidationError {
                path: "sources[].freshness".to_string(),
                message: format!("Freshness checks require Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.decision.audit.is_some() && !tier.has_audit_config() {
            errors.push(ValidationError {
                path: "decision.audit".to_string(),
                message: format!("Audit configuration requires Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.metadata.is_some() && !tier.has_metadata() {
            errors.push(ValidationError {
                path: "metadata".to_string(),
                message: format!("Spec metadata requires Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.sources.iter().any(|s| s.sampling.is_some()) && !tier.has_sampling() {
            errors.push(ValidationError {
                path: "sources[].sampling".to_string(),
                message: format!("Sampling requires Growth or Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.exclusions.as_ref().is_some_and(|v| !v.is_empty()) && !tier.has_exclusions() {
            errors.push(ValidationError {
                path: "exclusions".to_string(),
                message: format!("Exclusions require Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.relations.as_ref().is_some_and(|v| !v.is_empty()) && !tier.has_relations() {
            errors.push(ValidationError {
                path: "relations".to_string(),
                message: format!("Relations require Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.governance.is_some() && !tier.has_governance() {
            errors.push(ValidationError {
                path: "governance".to_string(),
                message: format!("Governance policies require Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if spec.reference_identifiers.as_ref().is_some_and(|v| !v.is_empty()) && !tier.has_reference_identifiers() {
            errors.push(ValidationError {
                path: "reference_identifiers".to_string(),
                message: format!("Reference identifiers require Growth or Enterprise tier (current: {})", tier),
                severity: Severity::Error,
            });
        }

        if let Some(ref scoring) = spec.decision.scoring {
            match scoring.method {
                ScoringMethod::MlEnsemble | ScoringMethod::Custom => {
                    if !tier.has_advanced_scoring() {
                        errors.push(ValidationError {
                            path: "decision.scoring.method".to_string(),
                            message: format!("ML ensemble and custom scoring require Enterprise tier (current: {})", tier),
                            severity: Severity::Error,
                        });
                    }
                }
                ScoringMethod::WeightedSum | ScoringMethod::FellegiSunter => {} // available to all tiers
            }
        }

        errors
    }

    fn validate_blocking(&self, blocking: &BlockingSpec, errors: &mut Vec<ValidationError>) {
        if blocking.keys.len() > self.limits.max_blocking_keys {
            errors.push(ValidationError {
                path: "blocking.keys".to_string(),
                message: format!(
                    "Too many blocking keys: {} (max: {})",
                    blocking.keys.len(),
                    self.limits.max_blocking_keys
                ),
                severity: Severity::Error,
            });
        }
    }
}

impl Default for SpecValidator {
    fn default() -> Self {
        Self::new()
    }
}

/// Validate a compiled IdentityPlan against the tenant's current tier.
///
/// This is a lightweight check for use at pipeline execution time (worker),
/// mirroring the spec-level `validate_tier()` but operating on the compiled IR.
/// Returns a list of tier violations; an empty list means the plan is allowed.
pub fn validate_plan_tier(plan: &IdentityPlan, tier: &TenantTier) -> Vec<ValidationError> {
    let mut errors = Vec::new();

    if plan.compliance.is_some() && !tier.has_compliance() {
        errors.push(ValidationError {
            path: "plan.compliance".to_string(),
            message: format!("Compliance features require Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if plan.hierarchy.is_some() && !tier.has_hierarchy() {
        errors.push(ValidationError {
            path: "plan.hierarchy".to_string(),
            message: format!("Hierarchy features require Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if plan.sources.iter().any(|s| s.freshness.is_some()) && !tier.has_freshness() {
        errors.push(ValidationError {
            path: "plan.sources[].freshness".to_string(),
            message: format!("Freshness checks require Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if plan.audit.is_some() && !tier.has_audit_config() {
        errors.push(ValidationError {
            path: "plan.audit".to_string(),
            message: format!("Audit configuration requires Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if plan.metadata.is_some() && !tier.has_metadata() {
        errors.push(ValidationError {
            path: "plan.metadata".to_string(),
            message: format!("Spec metadata requires Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if plan.sources.iter().any(|s| s.sampling.is_some()) && !tier.has_sampling() {
        errors.push(ValidationError {
            path: "plan.sources[].sampling".to_string(),
            message: format!("Sampling requires Growth or Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if !plan.exclusions.is_empty() && !tier.has_exclusions() {
        errors.push(ValidationError {
            path: "plan.exclusions".to_string(),
            message: format!("Exclusions require Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if !plan.relations.is_empty() && !tier.has_relations() {
        errors.push(ValidationError {
            path: "plan.relations".to_string(),
            message: format!("Relations require Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if plan.governance.is_some() && !tier.has_governance() {
        errors.push(ValidationError {
            path: "plan.governance".to_string(),
            message: format!("Governance policies require Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    if !plan.reference_identifiers.is_empty() && !tier.has_reference_identifiers() {
        errors.push(ValidationError {
            path: "plan.reference_identifiers".to_string(),
            message: format!("Reference identifiers require Growth or Enterprise tier (current: {})", tier),
            severity: Severity::Error,
        });
    }

    match plan.decision.scoring_method {
        ScoringMethod::MlEnsemble | ScoringMethod::Custom => {
            if !tier.has_advanced_scoring() {
                errors.push(ValidationError {
                    path: "plan.decision.scoring_method".to_string(),
                    message: format!("ML ensemble and custom scoring require Enterprise tier (current: {})", tier),
                    severity: Severity::Error,
                });
            }
        }
        ScoringMethod::WeightedSum | ScoringMethod::FellegiSunter => {} // available to all tiers
    }

    errors
}

#[cfg(test)]
mod tests {
    use super::*;

    fn minimal_valid_spec() -> IdentitySpec {
        IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1

entity:
  name: customer

sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address

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

decision:
  thresholds:
    match: 0.9
"#).unwrap()
    }

    #[test]
    fn test_valid_spec_passes() {
        let spec = minimal_valid_spec();
        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_ok());
    }

    #[test]
    fn test_invalid_api_version() {
        let mut spec = minimal_valid_spec();
        spec.api_version = "invalid/v1".to_string();
        
        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        assert!(result.unwrap_err().iter().any(|e| e.path == "api_version"));
    }

    #[test]
    fn test_tier_gating_starter_rejects_compliance() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
  compliance:
    frameworks: [gdpr]
    pii_fields: [email]
    audit_required: true
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.path.contains("compliance")));
    }

    #[test]
    fn test_tier_gating_enterprise_accepts_compliance() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
  compliance:
    frameworks: [gdpr]
    pii_fields: [email]
    audit_required: true
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Enterprise);
        assert!(tier_errors.is_empty());
    }

    #[test]
    fn test_tier_gating_starter_rejects_hierarchy() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
  hierarchy:
    parent_field: parent_id
    depth: 3
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.path.contains("hierarchy")));
    }

    #[test]
    fn test_tier_gating_growth_rejects_audit() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
  audit:
    log_all_comparisons: true
    log_decisions: true
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Growth);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.path.contains("audit")));
    }

    #[test]
    fn test_missing_field_reference() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1

entity:
  name: customer

sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address

rules:
  - type: exact
    name: phone_exact
    field: phone
    weight: 1.0

decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        assert!(result.unwrap_err().iter().any(|e| e.message.contains("phone")));
    }

    // =========================================================================
    // Adapter-first validation tests
    // =========================================================================

    #[test]
    fn test_adapter_source_validates() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_ok(), "Adapter-first source should validate: {:?}", result.err());
    }

    #[test]
    fn test_source_missing_both_adapter_and_legacy() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: orphan
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        let errors = result.unwrap_err();
        assert!(errors.iter().any(|e| e.message.contains("adapter/location/primary_key")));
    }

    #[test]
    fn test_warehouse_adapter_requires_location() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: warehouse
    adapter: snowflake
    primary_key: id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        let errors = result.unwrap_err();
        assert!(errors.iter().any(|e| e.message.contains("location")));
    }

    #[test]
    fn test_invalid_schema_field_type() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    schema:
      email:
        type: varchar
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        let errors = result.unwrap_err();
        assert!(errors.iter().any(|e| e.message.contains("varchar")));
    }

    #[test]
    fn test_valid_schema_field_types() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    schema:
      email:
        type: string
      age:
        type: integer
      score:
        type: float
      active:
        type: boolean
      dob:
        type: date
      created_at:
        type: timestamp
      metadata:
        type: json
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_ok(), "All valid field types should pass: {:?}", result.err());
    }

    #[test]
    fn test_sampling_rate_out_of_bounds() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    sampling:
      strategy: random
      rate: 1.5
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        let errors = result.unwrap_err();
        assert!(errors.iter().any(|e| e.message.contains("Sampling rate")));
    }

    #[test]
    fn test_sampling_rate_zero_rejected() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    sampling:
      strategy: random
      rate: 0.0
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
    }

    #[test]
    fn test_tag_validation_invalid_chars() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    tags:
      - "Production Source"
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        let errors = result.unwrap_err();
        assert!(errors.iter().any(|e| e.message.contains("lowercase alphanumeric")));
    }

    #[test]
    fn test_governance_required_tags_missing() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    tags:
      - staging
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
governance:
  required_tags:
    - production
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_err());
        let errors = result.unwrap_err();
        assert!(errors.iter().any(|e| e.message.contains("missing required governance tag")));
    }

    #[test]
    fn test_governance_required_tags_satisfied() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    tags:
      - production
      - verified
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
governance:
  required_tags:
    - production
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        assert!(result.is_ok(), "Tags satisfied should pass: {:?}", result.err());
    }

    #[test]
    fn test_pii_without_compliance_warns() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    schema:
      ssn:
        type: string
        pii: true
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let result = validator.validate(&spec);
        // Should pass (it's a warning, not an error)
        assert!(result.is_ok());
        let warnings = result.unwrap();
        assert!(warnings.iter().any(|w| w.message.contains("PII fields") && w.message.contains("compliance")));
    }

    #[test]
    fn test_tier_gating_starter_rejects_sampling() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    sampling:
      strategy: random
      rate: 0.5
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.message.contains("Sampling")));
    }

    #[test]
    fn test_tier_gating_growth_accepts_sampling() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    adapter: postgres
    location: public.contacts
    primary_key: id
    sampling:
      strategy: random
      rate: 0.5
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Growth);
        assert!(tier_errors.is_empty(), "Growth tier should accept sampling: {:?}", tier_errors);
    }

    #[test]
    fn test_tier_gating_starter_rejects_exclusions() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
exclusions:
  - type: field_value
    field: email
    operator: eq
    value: "test@example.com"
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.path.contains("exclusions")));
    }

    #[test]
    fn test_tier_gating_starter_rejects_relations() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
relations:
  - name: customer_to_account
    from_entity: customer
    to_entity: account
    join_key: account_id
    cardinality: many_to_one
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.path.contains("relations")));
    }

    #[test]
    fn test_tier_gating_starter_rejects_governance() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    tags:
      - production
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
governance:
  require_freshness: true
  required_tags:
    - production
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.path.contains("governance")));
    }

    #[test]
    fn test_tier_gating_starter_rejects_reference_identifiers() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
reference_identifiers:
  - name: duns
    type: external
    authority: dnb
    field: duns_number
    match_weight: 0.95
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.path.contains("reference_identifiers")));
    }

    #[test]
    fn test_tier_gating_growth_allows_reference_identifiers() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  thresholds:
    match: 0.9
reference_identifiers:
  - name: duns
    type: external
    authority: dnb
    field: duns_number
    match_weight: 0.95
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Growth);
        assert!(tier_errors.is_empty(), "Growth tier should accept reference identifiers: {:?}", tier_errors);
    }

    #[test]
    fn test_tier_gating_starter_rejects_ml_ensemble_scoring() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  scoring:
    method: ml_ensemble
    ml_ensemble:
      coefficients:
        email_exact: 2.5
      bias: -1.0
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.message.contains("ML ensemble")));
    }

    #[test]
    fn test_tier_gating_starter_rejects_custom_scoring() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  scoring:
    method: custom
    custom:
      expression: "email_exact * 0.8"
  thresholds:
    match: 0.9
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Starter);
        assert!(!tier_errors.is_empty());
        assert!(tier_errors.iter().any(|e| e.message.contains("custom scoring")));
    }

    #[test]
    fn test_tier_gating_enterprise_allows_all() {
        let spec = IdentitySpec::from_yaml(r#"
api_version: kanoniv/v2
identity_version: test_v1
entity:
  name: customer
  compliance:
    frameworks: [gdpr]
    pii_fields: [email]
    audit_required: true
  hierarchy:
    parent_field: parent_id
    depth: 3
sources:
  - name: crm
    system: salesforce
    table: contacts
    id: contact_id
    tags:
      - production
    freshness:
      max_age: 24h
    sampling:
      strategy: random
      rate: 0.5
    attributes:
      email: email_address
rules:
  - type: exact
    name: email_exact
    field: email
    weight: 1.0
decision:
  scoring:
    method: ml_ensemble
    ml_ensemble:
      coefficients:
        email_exact: 2.5
      bias: -1.0
  thresholds:
    match: 0.9
  audit:
    log_all_comparisons: true
    log_decisions: true
metadata:
  name: full-spec
  owner: test
exclusions:
  - type: field_value
    field: email
    operator: eq
    value: "test@example.com"
relations:
  - name: customer_to_account
    from_entity: customer
    to_entity: account
    join_key: account_id
    cardinality: many_to_one
governance:
  require_freshness: true
  required_tags:
    - production
reference_identifiers:
  - name: duns
    type: external
    authority: dnb
    field: duns_number
    match_weight: 0.95
"#).unwrap();

        let validator = SpecValidator::new();
        let tier_errors = validator.validate_tier(&spec, &TenantTier::Enterprise);
        assert!(tier_errors.is_empty(), "Enterprise tier should accept all features: {:?}", tier_errors);
    }
}
