//! PID validation errors — typed, LLM-consumable error reports.

use std::fmt;

use serde_json::Value;

use crate::pid_requirements::{CodeValue, EntityRequirement, FieldRequirement, PidRequirements};

/// Severity of a validation error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Severity {
    /// Field is unconditionally required (Muss/X) or condition evaluated to True.
    Error,
    /// Condition evaluated to Unknown (depends on external context).
    Warning,
}

/// A single PID validation error.
#[derive(Debug, Clone)]
pub enum PidValidationError {
    /// An entire entity is missing from the interchange.
    MissingEntity {
        entity: String,
        ahb_status: String,
        severity: Severity,
    },
    /// A required field is None/missing.
    MissingField {
        entity: String,
        field: String,
        ahb_status: String,
        rust_type: Option<String>,
        valid_values: Vec<(String, String)>,
        severity: Severity,
    },
    /// A code field has a value not in the allowed set.
    InvalidCode {
        entity: String,
        field: String,
        value: String,
        valid_values: Vec<(String, String)>,
    },
}

impl PidValidationError {
    pub fn severity(&self) -> &Severity {
        match self {
            Self::MissingEntity { severity, .. } => severity,
            Self::MissingField { severity, .. } => severity,
            Self::InvalidCode { .. } => &Severity::Error,
        }
    }

    pub fn is_error(&self) -> bool {
        matches!(self.severity(), Severity::Error)
    }
}

impl fmt::Display for PidValidationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PidValidationError::MissingEntity {
                entity,
                ahb_status,
                severity,
            } => {
                let label = severity_label(severity);
                write!(
                    f,
                    "{label}: missing entity '{entity}' (required: {ahb_status})"
                )
            }
            PidValidationError::MissingField {
                entity,
                field,
                ahb_status,
                rust_type,
                valid_values,
                severity,
            } => {
                let label = severity_label(severity);
                write!(
                    f,
                    "{label}: missing {entity}.{field} (required: {ahb_status})"
                )?;
                if let Some(rt) = rust_type {
                    write!(f, "\n  → type: {rt}")?;
                }
                if !valid_values.is_empty() {
                    let codes: Vec<String> = valid_values
                        .iter()
                        .map(|(code, meaning)| {
                            if meaning.is_empty() {
                                code.clone()
                            } else {
                                format!("{code} ({meaning})")
                            }
                        })
                        .collect();
                    write!(f, "\n  → valid: {}", codes.join(", "))?;
                }
                Ok(())
            }
            PidValidationError::InvalidCode {
                entity,
                field,
                value,
                valid_values,
            } => {
                write!(f, "INVALID: {entity}.{field} = \"{value}\"")?;
                if !valid_values.is_empty() {
                    let codes: Vec<String> = valid_values.iter().map(|(c, _)| c.clone()).collect();
                    write!(f, "\n  → valid: {}", codes.join(", "))?;
                }
                Ok(())
            }
        }
    }
}

fn severity_label(severity: &Severity) -> &'static str {
    match severity {
        Severity::Error => "ERROR",
        Severity::Warning => "WARNING",
    }
}

/// A collection of validation errors for a PID.
pub struct ValidationReport(pub Vec<PidValidationError>);

impl ValidationReport {
    /// Returns true if the report contains any errors (not just warnings).
    pub fn has_errors(&self) -> bool {
        self.0.iter().any(|e| e.is_error())
    }

    /// Returns only the errors (not warnings).
    pub fn errors(&self) -> Vec<&PidValidationError> {
        self.0.iter().filter(|e| e.is_error()).collect()
    }

    /// Returns true if the report is empty (no errors or warnings).
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Returns the number of validation errors.
    pub fn len(&self) -> usize {
        self.0.len()
    }
}

impl fmt::Display for ValidationReport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (i, err) in self.0.iter().enumerate() {
            if i > 0 {
                writeln!(f)?;
            }
            write!(f, "{err}")?;
        }
        Ok(())
    }
}

// ── Validation Logic ──────────────────────────────────────────────────────

/// Validate a BO4E JSON value against PID requirements.
///
/// Walks the requirements and checks:
/// 1. Required entities are present in the JSON
/// 2. Required fields are present within each entity
/// 3. Code fields have values in the allowed set
pub fn validate_pid_json(json: &Value, requirements: &PidRequirements) -> Vec<PidValidationError> {
    let mut errors = Vec::new();

    for entity_req in &requirements.entities {
        let key = to_camel_case(&entity_req.entity);

        match json.get(&key) {
            None => {
                if is_unconditionally_required(&entity_req.ahb_status) {
                    errors.push(PidValidationError::MissingEntity {
                        entity: entity_req.entity.clone(),
                        ahb_status: entity_req.ahb_status.clone(),
                        severity: Severity::Error,
                    });
                }
            }
            Some(val) => {
                if entity_req.is_array {
                    if let Some(arr) = val.as_array() {
                        for element in arr {
                            validate_entity_fields(element, entity_req, &mut errors);
                        }
                    }
                } else {
                    validate_entity_fields(val, entity_req, &mut errors);
                }
            }
        }
    }

    errors
}

/// Traverse a dot-separated path in a JSON value, trying both the original
/// key and its camelCase variant at each level.
fn get_nested<'a>(json: &'a Value, path: &str) -> Option<&'a Value> {
    let mut current = json;
    for part in path.split('.') {
        current = current.get(part).or_else(|| {
            if part.contains('_') {
                current.get(snake_to_camel_case(part))
            } else {
                None
            }
        })?;
    }
    Some(current)
}

/// Validate fields within a single entity JSON object.
fn validate_entity_fields(
    entity_json: &Value,
    entity_req: &EntityRequirement,
    errors: &mut Vec<PidValidationError>,
) {
    for field_req in &entity_req.fields {
        // Traverse dot-separated paths (e.g. "produktIdentifikation.funktion")
        // and try camelCase variants at each level for typed struct compatibility.
        //
        // For companion fields, also check under the *Edifact wrapper object
        // (forward mapping format), falling back to the entity root (typed PID format).
        let val = get_nested(entity_json, &field_req.bo4e_name).or_else(|| {
            if field_req.is_companion {
                if let Some(ref companion_type) = entity_req.companion_type {
                    let companion_key = to_camel_case(companion_type);
                    let companion_obj = entity_json.get(&companion_key)?;
                    get_nested(companion_obj, &field_req.bo4e_name)
                } else {
                    None
                }
            } else {
                None
            }
        });

        // Treat null values as missing — JSON null means "not provided"
        let val = val.filter(|v| !v.is_null());

        match val {
            None => {
                if is_unconditionally_required(&field_req.ahb_status) {
                    errors.push(PidValidationError::MissingField {
                        entity: entity_req.entity.clone(),
                        field: field_req.bo4e_name.clone(),
                        ahb_status: field_req.ahb_status.clone(),
                        rust_type: field_req.enum_name.clone(),
                        valid_values: code_values_to_tuples(&field_req.valid_codes),
                        severity: Severity::Error,
                    });
                }
            }
            Some(val) => {
                if !field_req.valid_codes.is_empty() {
                    validate_code_value(val, entity_req, field_req, errors);
                }
            }
        }
    }
}

/// Validate that a code field's value is in the allowed set.
fn validate_code_value(
    val: &Value,
    entity_req: &EntityRequirement,
    field_req: &FieldRequirement,
    errors: &mut Vec<PidValidationError>,
) {
    let value_str = match val.as_str() {
        Some(s) => s,
        None => return, // Non-string values skip code validation
    };

    let is_valid = field_req.valid_codes.iter().any(|cv| cv.code == value_str);
    if !is_valid {
        errors.push(PidValidationError::InvalidCode {
            entity: entity_req.entity.clone(),
            field: field_req.bo4e_name.clone(),
            value: value_str.to_string(),
            valid_values: code_values_to_tuples(&field_req.valid_codes),
        });
    }
}

/// Convert CodeValue vec to (code, meaning) tuples.
fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
    codes
        .iter()
        .map(|cv| (cv.code.clone(), cv.meaning.clone()))
        .collect()
}

/// Convert PascalCase entity name to camelCase JSON key.
///
/// "Prozessdaten" → "prozessdaten"
/// "RuhendeMarktlokation" → "ruhendeMarktlokation"
/// "Marktlokation" → "marktlokation"
fn to_camel_case(s: &str) -> String {
    if s.is_empty() {
        return String::new();
    }
    let mut chars = s.chars();
    let first = chars.next().unwrap();
    let mut result = first.to_lowercase().to_string();
    result.extend(chars);
    result
}

/// Convert a snake_case field name to camelCase.
///
/// This mirrors what `#[serde(rename_all = "camelCase")]` does at runtime, allowing
/// the validator to find fields in JSON that was produced by typed structs even when
/// the requirement stores the field name as snake_case (as it comes from TOML).
///
/// Examples:
/// - `"code_codepflege"` → `"codeCodepflege"`
/// - `"vorgang_id"` → `"vorgangId"`
/// - `"marktlokation"` → `"marktlokation"` (unchanged — no underscores)
fn snake_to_camel_case(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut capitalize_next = false;
    for ch in s.chars() {
        if ch == '_' {
            capitalize_next = true;
        } else if capitalize_next {
            result.extend(ch.to_uppercase());
            capitalize_next = false;
        } else {
            result.push(ch);
        }
    }
    result
}

/// Returns true if the AHB status indicates an unconditionally required field.
fn is_unconditionally_required(ahb_status: &str) -> bool {
    matches!(ahb_status, "X" | "Muss" | "Soll")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pid_requirements::{
        CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
    };
    use serde_json::json;

    fn sample_requirements() -> PidRequirements {
        PidRequirements {
            pid: "55001".to_string(),
            beschreibung: "Anmeldung verb. MaLo".to_string(),
            entities: vec![
                EntityRequirement {
                    entity: "Prozessdaten".to_string(),
                    bo4e_type: "Prozessdaten".to_string(),
                    companion_type: None,
                    ahb_status: "Muss".to_string(),
                    is_array: false,
                    map_key: None,
                    fields: vec![
                        FieldRequirement {
                            bo4e_name: "vorgangId".to_string(),
                            ahb_status: "X".to_string(),
                            is_companion: false,
                            field_type: "data".to_string(),
                            format: None,
                            enum_name: None,
                            valid_codes: vec![],
                            child_group: None,
                        },
                        FieldRequirement {
                            bo4e_name: "transaktionsgrund".to_string(),
                            ahb_status: "X".to_string(),
                            is_companion: false,
                            field_type: "code".to_string(),
                            format: None,
                            enum_name: Some("Transaktionsgrund".to_string()),
                            valid_codes: vec![
                                CodeValue {
                                    code: "E01".to_string(),
                                    meaning: "Ein-/Auszug (Einzug)".to_string(),
                                    enum_name: None,
                                },
                                CodeValue {
                                    code: "E03".to_string(),
                                    meaning: "Wechsel".to_string(),
                                    enum_name: None,
                                },
                            ],
                            child_group: None,
                        },
                    ],
                },
                EntityRequirement {
                    entity: "Marktlokation".to_string(),
                    bo4e_type: "Marktlokation".to_string(),
                    companion_type: Some("MarktlokationEdifact".to_string()),
                    ahb_status: "Muss".to_string(),
                    is_array: false,
                    map_key: None,
                    fields: vec![
                        FieldRequirement {
                            bo4e_name: "marktlokationsId".to_string(),
                            ahb_status: "X".to_string(),
                            is_companion: false,
                            field_type: "data".to_string(),
                            format: None,
                            enum_name: None,
                            valid_codes: vec![],
                            child_group: None,
                        },
                        FieldRequirement {
                            bo4e_name: "haushaltskunde".to_string(),
                            ahb_status: "X".to_string(),
                            is_companion: false,
                            field_type: "code".to_string(),
                            format: None,
                            enum_name: Some("Haushaltskunde".to_string()),
                            valid_codes: vec![
                                CodeValue {
                                    code: "Z15".to_string(),
                                    meaning: "Ja".to_string(),
                                    enum_name: None,
                                },
                                CodeValue {
                                    code: "Z18".to_string(),
                                    meaning: "Nein".to_string(),
                                    enum_name: None,
                                },
                            ],
                            child_group: None,
                        },
                    ],
                },
                EntityRequirement {
                    entity: "Geschaeftspartner".to_string(),
                    bo4e_type: "Geschaeftspartner".to_string(),
                    companion_type: Some("GeschaeftspartnerEdifact".to_string()),
                    ahb_status: "Muss".to_string(),
                    is_array: true,
                    map_key: None,
                    fields: vec![FieldRequirement {
                        bo4e_name: "identifikation".to_string(),
                        ahb_status: "X".to_string(),
                        is_companion: false,
                        field_type: "data".to_string(),
                        format: None,
                        enum_name: None,
                        valid_codes: vec![],
                        child_group: None,
                    }],
                },
            ],
        }
    }

    #[test]
    fn test_validate_complete_json() {
        let reqs = sample_requirements();
        let json = json!({
            "prozessdaten": {
                "vorgangId": "ABC123",
                "transaktionsgrund": "E01"
            },
            "marktlokation": {
                "marktlokationsId": "51234567890",
                "haushaltskunde": "Z15"
            },
            "geschaeftspartner": [
                { "identifikation": "9900000000003" }
            ]
        });

        let errors = validate_pid_json(&json, &reqs);
        assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
    }

    #[test]
    fn test_validate_missing_entity() {
        let reqs = sample_requirements();
        let json = json!({
            "prozessdaten": {
                "vorgangId": "ABC123",
                "transaktionsgrund": "E01"
            },
            "geschaeftspartner": [
                { "identifikation": "9900000000003" }
            ]
        });
        // Marktlokation is missing

        let errors = validate_pid_json(&json, &reqs);
        assert_eq!(errors.len(), 1);
        match &errors[0] {
            PidValidationError::MissingEntity {
                entity,
                ahb_status,
                severity,
            } => {
                assert_eq!(entity, "Marktlokation");
                assert_eq!(ahb_status, "Muss");
                assert_eq!(severity, &Severity::Error);
            }
            other => panic!("Expected MissingEntity, got: {other:?}"),
        }

        // Display check
        let msg = errors[0].to_string();
        assert!(msg.contains("ERROR"));
        assert!(msg.contains("Marktlokation"));
        assert!(msg.contains("Muss"));
    }

    #[test]
    fn test_validate_missing_field() {
        let reqs = sample_requirements();
        let json = json!({
            "prozessdaten": {
                "transaktionsgrund": "E01"
                // vorgangId is missing
            },
            "marktlokation": {
                "marktlokationsId": "51234567890",
                "haushaltskunde": "Z15"
            },
            "geschaeftspartner": [
                { "identifikation": "9900000000003" }
            ]
        });

        let errors = validate_pid_json(&json, &reqs);
        assert_eq!(errors.len(), 1);
        match &errors[0] {
            PidValidationError::MissingField {
                entity,
                field,
                ahb_status,
                severity,
                ..
            } => {
                assert_eq!(entity, "Prozessdaten");
                assert_eq!(field, "vorgangId");
                assert_eq!(ahb_status, "X");
                assert_eq!(severity, &Severity::Error);
            }
            other => panic!("Expected MissingField, got: {other:?}"),
        }

        let msg = errors[0].to_string();
        assert!(msg.contains("ERROR"));
        assert!(msg.contains("Prozessdaten.vorgangId"));
    }

    #[test]
    fn test_validate_invalid_code() {
        let reqs = sample_requirements();
        let json = json!({
            "prozessdaten": {
                "vorgangId": "ABC123",
                "transaktionsgrund": "E01"
            },
            "marktlokation": {
                "marktlokationsId": "51234567890",
                "haushaltskunde": "Z99"  // Invalid code
            },
            "geschaeftspartner": [
                { "identifikation": "9900000000003" }
            ]
        });

        let errors = validate_pid_json(&json, &reqs);
        assert_eq!(errors.len(), 1);
        match &errors[0] {
            PidValidationError::InvalidCode {
                entity,
                field,
                value,
                valid_values,
            } => {
                assert_eq!(entity, "Marktlokation");
                assert_eq!(field, "haushaltskunde");
                assert_eq!(value, "Z99");
                assert_eq!(valid_values.len(), 2);
                assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
                assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
            }
            other => panic!("Expected InvalidCode, got: {other:?}"),
        }

        let msg = errors[0].to_string();
        assert!(msg.contains("INVALID"));
        assert!(msg.contains("Z99"));
        assert!(msg.contains("Z15"));
    }

    #[test]
    fn test_validate_array_entity() {
        let reqs = sample_requirements();
        let json = json!({
            "prozessdaten": {
                "vorgangId": "ABC123",
                "transaktionsgrund": "E01"
            },
            "marktlokation": {
                "marktlokationsId": "51234567890",
                "haushaltskunde": "Z15"
            },
            "geschaeftspartner": [
                { "identifikation": "9900000000003" },
                { }  // Missing identifikation in second element
            ]
        });

        let errors = validate_pid_json(&json, &reqs);
        assert_eq!(errors.len(), 1);
        match &errors[0] {
            PidValidationError::MissingField { entity, field, .. } => {
                assert_eq!(entity, "Geschaeftspartner");
                assert_eq!(field, "identifikation");
            }
            other => panic!("Expected MissingField, got: {other:?}"),
        }
    }

    #[test]
    fn test_to_camel_case() {
        assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
        assert_eq!(
            to_camel_case("RuhendeMarktlokation"),
            "ruhendeMarktlokation"
        );
        assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
        assert_eq!(to_camel_case(""), "");
    }

    #[test]
    fn test_snake_to_camel_case() {
        assert_eq!(snake_to_camel_case("code_codepflege"), "codeCodepflege");
        assert_eq!(snake_to_camel_case("vorgang_id"), "vorgangId");
        assert_eq!(snake_to_camel_case("marktlokation"), "marktlokation");
        assert_eq!(snake_to_camel_case(""), "");
        assert_eq!(snake_to_camel_case("a_b_c"), "aBC");
    }

    /// A field stored as snake_case in requirements (e.g. from TOML) must be found
    /// in JSON that was produced by a typed struct using `#[serde(rename_all = "camelCase")]`.
    #[test]
    fn test_camel_case_fallback_for_snake_case_bo4e_name() {
        let reqs = PidRequirements {
            pid: "55077".to_string(),
            beschreibung: "Test camelCase fallback".to_string(),
            entities: vec![EntityRequirement {
                entity: "Zuordnung".to_string(),
                bo4e_type: "Zuordnung".to_string(),
                companion_type: None,
                ahb_status: "Muss".to_string(),
                is_array: false,
                map_key: None,
                fields: vec![
                    FieldRequirement {
                        // snake_case as stored in TOML requirements
                        bo4e_name: "code_codepflege".to_string(),
                        ahb_status: "X".to_string(),
                        is_companion: false,
                        field_type: "data".to_string(),
                        format: None,
                        enum_name: None,
                        valid_codes: vec![],
                        child_group: None,
                    },
                    FieldRequirement {
                        bo4e_name: "codeliste".to_string(),
                        ahb_status: "X".to_string(),
                        is_companion: false,
                        field_type: "data".to_string(),
                        format: None,
                        enum_name: None,
                        valid_codes: vec![],
                        child_group: None,
                    },
                ],
            }],
        };

        // JSON produced by a typed struct with #[serde(rename_all = "camelCase")]:
        // code_codepflege → codeCodepflege
        let json_camel = json!({
            "zuordnung": {
                "codeCodepflege": "DE_BDEW",
                "codeliste": "6"
            }
        });

        let errors = validate_pid_json(&json_camel, &reqs);
        assert!(
            errors.is_empty(),
            "Expected no errors when field is present under camelCase key, got: {errors:?}"
        );

        // Also verify that snake_case key in JSON still works (backward compat).
        let json_snake = json!({
            "zuordnung": {
                "code_codepflege": "DE_BDEW",
                "codeliste": "6"
            }
        });

        let errors = validate_pid_json(&json_snake, &reqs);
        assert!(
            errors.is_empty(),
            "Expected no errors when field is present under snake_case key, got: {errors:?}"
        );

        // When the field is truly absent, a MissingField error must still be raised.
        let json_missing = json!({
            "zuordnung": {
                "codeliste": "6"
            }
        });

        let errors = validate_pid_json(&json_missing, &reqs);
        assert_eq!(errors.len(), 1);
        match &errors[0] {
            PidValidationError::MissingField { field, .. } => {
                assert_eq!(field, "code_codepflege");
            }
            other => panic!("Expected MissingField, got: {other:?}"),
        }
    }

    #[test]
    fn test_is_unconditionally_required() {
        assert!(is_unconditionally_required("X"));
        assert!(is_unconditionally_required("Muss"));
        assert!(is_unconditionally_required("Soll"));
        assert!(!is_unconditionally_required("Kann"));
        assert!(!is_unconditionally_required("[1]"));
        assert!(!is_unconditionally_required(""));
    }

    #[test]
    fn test_validation_report_display() {
        let errors = vec![
            PidValidationError::MissingEntity {
                entity: "Marktlokation".to_string(),
                ahb_status: "Muss".to_string(),
                severity: Severity::Error,
            },
            PidValidationError::MissingField {
                entity: "Prozessdaten".to_string(),
                field: "vorgangId".to_string(),
                ahb_status: "X".to_string(),
                rust_type: None,
                valid_values: vec![],
                severity: Severity::Error,
            },
        ];
        let report = ValidationReport(errors);
        assert!(report.has_errors());
        assert_eq!(report.len(), 2);
        assert!(!report.is_empty());

        let display = report.to_string();
        assert!(display.contains("missing entity 'Marktlokation'"));
        assert!(display.contains("missing Prozessdaten.vorgangId"));
    }

    #[test]
    fn test_missing_field_with_type_and_values_display() {
        let err = PidValidationError::MissingField {
            entity: "Marktlokation".to_string(),
            field: "haushaltskunde".to_string(),
            ahb_status: "Muss".to_string(),
            rust_type: Some("Haushaltskunde".to_string()),
            valid_values: vec![
                ("Z15".to_string(), "Ja".to_string()),
                ("Z18".to_string(), "Nein".to_string()),
            ],
            severity: Severity::Error,
        };
        let msg = err.to_string();
        assert!(msg.contains("type: Haushaltskunde"));
        assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
    }

    #[test]
    fn test_optional_fields_not_flagged() {
        let reqs = PidRequirements {
            pid: "99999".to_string(),
            beschreibung: "Test".to_string(),
            entities: vec![EntityRequirement {
                entity: "Test".to_string(),
                bo4e_type: "Test".to_string(),
                companion_type: None,
                ahb_status: "Kann".to_string(),
                is_array: false,
                map_key: None,
                fields: vec![FieldRequirement {
                    bo4e_name: "optionalField".to_string(),
                    ahb_status: "Kann".to_string(),
                    is_companion: false,
                    field_type: "data".to_string(),
                    format: None,
                    enum_name: None,
                    valid_codes: vec![],
                    child_group: None,
                }],
            }],
        };

        // Entity missing but optional — no error
        let errors = validate_pid_json(&json!({}), &reqs);
        assert!(errors.is_empty());

        // Entity present, field missing but optional — no error
        let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
        assert!(errors.is_empty());
    }

    /// Regression test for issue #48: nested dot-path fields reported as missing
    /// even when present (e.g. `produktIdentifikation.funktion`).
    #[test]
    fn test_nested_dot_path_fields_not_falsely_missing() {
        let reqs = PidRequirements {
            pid: "55001".to_string(),
            beschreibung: "Test nested paths".to_string(),
            entities: vec![EntityRequirement {
                entity: "ProduktpaketDaten".to_string(),
                bo4e_type: "ProduktpaketDaten".to_string(),
                companion_type: None,
                ahb_status: "Muss".to_string(),
                is_array: true,
                map_key: None,
                fields: vec![
                    FieldRequirement {
                        bo4e_name: "produktIdentifikation.funktion".to_string(),
                        ahb_status: "X".to_string(),
                        is_companion: false,
                        field_type: "code".to_string(),
                        format: None,
                        enum_name: Some("Produktidentifikation".to_string()),
                        valid_codes: vec![CodeValue {
                            code: "5".to_string(),
                            meaning: "Produktidentifikation".to_string(),
                            enum_name: None,
                        }],
                        child_group: None,
                    },
                    FieldRequirement {
                        bo4e_name: "produktMerkmal.code".to_string(),
                        ahb_status: "X".to_string(),
                        is_companion: false,
                        field_type: "code".to_string(),
                        format: None,
                        enum_name: None,
                        valid_codes: vec![],
                        child_group: None,
                    },
                ],
            }],
        };

        // Exact JSON from issue #48
        let json = json!({
            "produktpaketDaten": [{
                "produktIdentifikation": { "funktion": "5", "id": "9991000002082", "typ": "Z11" },
                "produktMerkmal": { "code": "ZH9" }
            }]
        });

        let errors = validate_pid_json(&json, &reqs);
        assert!(
            errors.is_empty(),
            "Nested dot-path fields should be found (issue #48), got: {errors:?}"
        );
    }

    #[test]
    fn test_nested_dot_path_truly_missing() {
        let reqs = PidRequirements {
            pid: "55001".to_string(),
            beschreibung: "Test nested paths missing".to_string(),
            entities: vec![EntityRequirement {
                entity: "ProduktpaketDaten".to_string(),
                bo4e_type: "ProduktpaketDaten".to_string(),
                companion_type: None,
                ahb_status: "Muss".to_string(),
                is_array: true,
                map_key: None,
                fields: vec![FieldRequirement {
                    bo4e_name: "produktIdentifikation.funktion".to_string(),
                    ahb_status: "X".to_string(),
                    is_companion: false,
                    field_type: "data".to_string(),
                    format: None,
                    enum_name: None,
                    valid_codes: vec![],
                    child_group: None,
                }],
            }],
        };

        // Parent exists but nested field is missing
        let json = json!({
            "produktpaketDaten": [{
                "produktIdentifikation": { "id": "123" }
            }]
        });

        let errors = validate_pid_json(&json, &reqs);
        assert_eq!(errors.len(), 1, "Should report missing nested field");
        match &errors[0] {
            PidValidationError::MissingField { field, .. } => {
                assert_eq!(field, "produktIdentifikation.funktion");
            }
            other => panic!("Expected MissingField, got: {other:?}"),
        }
    }
}
