mig-bo4e 0.1.47

Declarative TOML-based MIG-tree to BO4E mapping engine
Documentation
//! Integration tests for Bo4eFieldIndex — resolves EDIFACT paths to BO4E paths.

mod common;

use automapper_generator::parsing::ahb_parser::parse_ahb;
use common::test_utils::discover_xml_paths;
use mig_assembly::parsing::parse_mig;
use mig_assembly::pid_filter::filter_mig_for_pid;
use mig_bo4e::engine::MappingEngine;
use mig_bo4e::error_mapping::Bo4eFieldIndex;
use mig_bo4e::path_resolver::PathResolver;
use mig_types::schema::mig::MigSchema;
use std::collections::HashSet;
use std::path::Path;

const MAPPINGS_DIR: &str = "../../mappings/FV2504/UTILMD_Strom/pid_55001";
const MESSAGE_DIR: &str = "../../mappings/FV2504/UTILMD_Strom/message";
const SCHEMA_DIR: &str = "../../crates/mig-types/src/generated/fv2504/utilmd/pids";

fn path_resolver() -> PathResolver {
    PathResolver::from_schema_dir(std::path::Path::new(SCHEMA_DIR))
}

fn load_pid_filtered_mig(pid_id: &str) -> Option<MigSchema> {
    let (mig_path, ahb_path) = discover_xml_paths("FV2504", "UTILMD", Some("Strom"))?;
    let mig = parse_mig(&mig_path, "UTILMD", Some("Strom"), "FV2504").ok()?;
    let ahb = parse_ahb(&ahb_path, "UTILMD", Some("Strom"), "FV2504").ok()?;
    let pid = ahb.workflows.iter().find(|w| w.id == pid_id)?;
    let numbers: HashSet<String> = pid.segment_numbers.iter().cloned().collect();
    Some(filter_mig_for_pid(&mig, &numbers))
}

fn load_split_engines() -> Option<(MappingEngine, MappingEngine)> {
    let msg_dir = Path::new(MESSAGE_DIR);
    let tx_dir = Path::new(MAPPINGS_DIR);
    if !msg_dir.exists() || !tx_dir.exists() {
        return None;
    }
    MappingEngine::load_split(msg_dir, tx_dir)
        .ok()
        .map(|(m, t)| {
            let r = path_resolver();
            (m.with_path_resolver(r.clone()), t.with_path_resolver(r))
        })
}

#[test]
fn test_marktlokation_path_resolution() {
    let mig = match load_pid_filtered_mig("55001") {
        Some(m) => m,
        None => {
            eprintln!("Skipping: MIG/AHB XML not available");
            return;
        }
    };
    let (msg_engine, tx_engine) = match load_split_engines() {
        Some(e) => e,
        None => {
            eprintln!("Skipping: mapping files not available");
            return;
        }
    };

    let mut all_defs: Vec<_> = msg_engine.definitions().to_vec();
    all_defs.extend(tx_engine.definitions().iter().cloned());

    let index = Bo4eFieldIndex::build(&all_defs, &mig);

    // marktlokation.toml: source_group = "SG4.SG5", loc.1.0 = "marktlokationsId"
    // LOC at SG4/SG5 → element 1 = composite C517 → component 0 = data element 3225
    let result = index.resolve("SG4/SG5/LOC/C517/3225", None);
    assert_eq!(
        result.as_deref(),
        Some("stammdaten.Marktlokation.marktlokationsId"),
        "Should resolve LOC/C517/3225 to Marktlokation.marktlokationsId"
    );
}

#[test]
fn test_prozessdaten_path_resolution() {
    let mig = match load_pid_filtered_mig("55001") {
        Some(m) => m,
        None => {
            eprintln!("Skipping: MIG/AHB XML not available");
            return;
        }
    };
    let (msg_engine, tx_engine) = match load_split_engines() {
        Some(e) => e,
        None => {
            eprintln!("Skipping: mapping files not available");
            return;
        }
    };

    let mut all_defs: Vec<_> = msg_engine.definitions().to_vec();
    all_defs.extend(tx_engine.definitions().iter().cloned());

    let index = Bo4eFieldIndex::build(&all_defs, &mig);

    // prozessdaten.toml: source_group = "SG4", ide.1 = "vorgangId"
    // IDE at SG4 → element 1 = composite C206 → sub 0 = data element 7402 (Vorgangsnummer)
    let result = index.resolve("SG4/IDE/C206/7402", None);
    assert_eq!(
        result.as_deref(),
        Some("stammdaten.Prozessdaten.vorgangId"),
        "Should resolve IDE/C206/7402 to Prozessdaten.vorgangId"
    );
}

#[test]
fn test_prefix_match_for_entity_level() {
    let mig = match load_pid_filtered_mig("55001") {
        Some(m) => m,
        None => {
            eprintln!("Skipping: MIG/AHB XML not available");
            return;
        }
    };
    let (msg_engine, tx_engine) = match load_split_engines() {
        Some(e) => e,
        None => {
            eprintln!("Skipping: mapping files not available");
            return;
        }
    };

    let mut all_defs: Vec<_> = msg_engine.definitions().to_vec();
    all_defs.extend(tx_engine.definitions().iter().cloned());

    let index = Bo4eFieldIndex::build(&all_defs, &mig);

    // SG2/NAD/... should prefix-match to Marktteilnehmer
    let result = index.resolve("SG2/NAD/unknown_element", None);
    assert!(
        result.is_some(),
        "Should prefix-match SG2/NAD to a stammdaten entity"
    );
    let resolved = result.unwrap();
    assert!(
        resolved.starts_with("stammdaten."),
        "SG2 entities should be in stammdaten, got: {resolved}"
    );
}

#[test]
fn test_haushaltskunde_path_resolution() {
    let mig = match load_pid_filtered_mig("55001") {
        Some(m) => m,
        None => {
            eprintln!("Skipping: MIG/AHB XML not available");
            return;
        }
    };
    let (msg_engine, tx_engine) = match load_split_engines() {
        Some(e) => e,
        None => {
            eprintln!("Skipping: mapping files not available");
            return;
        }
    };

    let mut all_defs: Vec<_> = msg_engine.definitions().to_vec();
    all_defs.extend(tx_engine.definitions().iter().cloned());

    let reverse_resolver = mig_bo4e::path_resolver::ReversePathResolver::from_schema_dir(
        std::path::Path::new(SCHEMA_DIR),
    );
    let index = Bo4eFieldIndex::build_with_resolver(&all_defs, &mig, &reverse_resolver);

    // _33_marktlokation_zuordnung.toml: source_group = "SG4.SG8.SG10",
    // companion_fields: "cci.c240.d7037" = "haushaltskunde"
    // CCI at SG4/SG8/SG10 → element 2 = composite C240 → component 0 = data element 7037
    let result = index.resolve("SG4/SG8/SG10/CCI/C240/7037", None);
    assert_eq!(
        result.as_deref(),
        Some("stammdaten.Marktlokation.marktlokationEdifact.haushaltskunde"),
        "Should resolve CCI/C240/7037 to Marktlokation companion haushaltskunde"
    );
}

/// DTM qualifier paths (C507/2005) should resolve to the sibling data field,
/// not just the entity level. DTM+92 qualifier → gueltigAb, DTM+93 → gueltigBis.
#[test]
fn test_dtm_qualifier_path_resolution() {
    let mig = match load_pid_filtered_mig("55001") {
        Some(m) => m,
        None => {
            eprintln!("Skipping: MIG/AHB XML not available");
            return;
        }
    };
    let (msg_engine, tx_engine) = match load_split_engines() {
        Some(e) => e,
        None => {
            eprintln!("Skipping: mapping files not available");
            return;
        }
    };

    let mut all_defs: Vec<_> = msg_engine.definitions().to_vec();
    all_defs.extend(tx_engine.definitions().iter().cloned());

    let reverse_resolver = mig_bo4e::path_resolver::ReversePathResolver::from_schema_dir(
        std::path::Path::new(SCHEMA_DIR),
    );
    let index = Bo4eFieldIndex::build_with_resolver(&all_defs, &mig, &reverse_resolver);

    // DTM+93 qualifier (C507/2005) with hint "93" → should resolve to gueltigBis
    let result = index.resolve("SG4/DTM/C507/2005", Some("93"));
    assert_eq!(
        result.as_deref(),
        Some("stammdaten.Prozessdaten.gueltigBis"),
        "DTM+93 qualifier should resolve to Prozessdaten.gueltigBis"
    );

    // DTM+92 qualifier (C507/2005) with hint "92" → should resolve to gueltigAb
    let result = index.resolve("SG4/DTM/C507/2005", Some("92"));
    assert_eq!(
        result.as_deref(),
        Some("stammdaten.Prozessdaten.gueltigAb"),
        "DTM+92 qualifier should resolve to Prozessdaten.gueltigAb"
    );

    // DTM data field (C507/2380) still works
    let result = index.resolve("SG4/DTM/C507/2380", Some("93"));
    assert_eq!(
        result.as_deref(),
        Some("stammdaten.Prozessdaten.gueltigBis"),
        "DTM+93 data field should resolve to Prozessdaten.gueltigBis"
    );
}

#[test]
fn test_unknown_path_returns_none() {
    let mig = match load_pid_filtered_mig("55001") {
        Some(m) => m,
        None => {
            eprintln!("Skipping: MIG/AHB XML not available");
            return;
        }
    };
    let (msg_engine, tx_engine) = match load_split_engines() {
        Some(e) => e,
        None => {
            eprintln!("Skipping: mapping files not available");
            return;
        }
    };

    let mut all_defs: Vec<_> = msg_engine.definitions().to_vec();
    all_defs.extend(tx_engine.definitions().iter().cloned());

    let index = Bo4eFieldIndex::build(&all_defs, &mig);

    assert!(index.resolve("SG99/UNKNOWN/9999", None).is_none());
}