use mig_assembly::assembler::Assembler;
use mig_assembly::parsing::parse_mig;
use mig_assembly::pid_detect::detect_pid;
use mig_assembly::test_support::{make_mig_group, make_mig_segment};
use mig_assembly::tokenize::parse_to_segments;
use mig_types::schema::mig::{MigSchema, MigSegmentGroup};
use std::path::Path;
fn make_utilmd_mig() -> MigSchema {
let sg3 = make_mig_group("SG3", vec!["CTA", "COM"], vec![]);
let sg5 = make_mig_group("SG5", vec!["LOC"], vec![]);
let sg6 = make_mig_group("SG6", vec!["RFF"], vec![]);
let sg8 = make_mig_group("SG8", vec!["SEQ", "RFF"], vec![]);
let sg4 = MigSegmentGroup {
id: "SG4".to_string(),
name: "SG4".to_string(),
description: None,
counter: None,
level: 1,
max_rep_std: 99,
max_rep_spec: 99,
status_std: Some("M".to_string()),
status_spec: Some("M".to_string()),
segments: vec![
make_mig_segment("IDE"),
make_mig_segment("STS"),
make_mig_segment("DTM"),
],
nested_groups: vec![sg5, sg6, sg8],
variant_code: None,
variant_qualifier_position: None,
variant_codes: vec![],
merged_variant_count: None,
variant_entry_qualifiers: Vec::new(),
};
MigSchema {
message_type: "UTILMD".to_string(),
variant: Some("Strom".to_string()),
version: "S2.1".to_string(),
publication_date: "2025-03-20".to_string(),
author: "BDEW".to_string(),
format_version: "FV2504".to_string(),
source_file: "test".to_string(),
segments: vec![
make_mig_segment("UNB"),
make_mig_segment("UNH"),
make_mig_segment("BGM"),
make_mig_segment("DTM"),
],
segment_groups: vec![make_mig_group("SG2", vec!["NAD"], vec![sg3]), sg4],
}
}
const MIG_XML_PATH: &str =
"../../xml-migs-and-ahbs/FV2504/UTILMD_MIG_Strom_S2_1_Fehlerkorrektur_20250320.xml";
const FIXTURE_DIR: &str = "../../example_market_communication_bo4e_transactions/UTILMD/FV2504";
fn load_real_mig() -> Option<MigSchema> {
let path = Path::new(MIG_XML_PATH);
if !path.exists() {
eprintln!("MIG XML not found at {MIG_XML_PATH}, skipping real-MIG tests");
return None;
}
Some(parse_mig(path, "UTILMD", Some("Strom"), "FV2504").expect("Failed to parse MIG XML"))
}
#[test]
fn test_end_to_end_minimal_utilmd() {
let input = b"UNA:+.? '\
UNB+UNOC:3+9900123000002:500+9900456000003:500+210615:1200+REF001'\
UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
BGM+E01+DOC001+9'\
DTM+137:20250701:102'\
NAD+MS+9900123000002::293'\
NAD+MR+9900456000003::293'\
IDE+24+TX001'\
STS+7+Z33'\
DTM+92:20250801:102'\
LOC+Z16+DE00014545768S0000000000000003054'\
RFF+Z13:55001'\
UNT+10+MSG001'\
UNZ+1+REF001'";
let segments = parse_to_segments(input).unwrap();
assert!(segments.len() >= 10, "Should parse at least 10 segments");
let pid = detect_pid(&segments).unwrap();
assert_eq!(pid, "55001");
let mig = make_utilmd_mig();
let assembler = Assembler::new(&mig);
let tree = assembler.assemble_generic(&segments).unwrap();
assert!(tree.segments.iter().any(|s| s.tag == "UNH"));
assert!(tree.segments.iter().any(|s| s.tag == "BGM"));
let sg2 = tree.groups.iter().find(|g| g.group_id == "SG2").unwrap();
assert_eq!(sg2.repetitions.len(), 2, "Should have 2 NAD parties");
let sg4 = tree.groups.iter().find(|g| g.group_id == "SG4").unwrap();
assert_eq!(sg4.repetitions.len(), 1, "Should have 1 transaction");
}
#[test]
fn test_end_to_end_multiple_transactions() {
let input = b"UNA:+.? '\
UNB+UNOC:3+9900123000002:500+9900456000003:500+210615:1200+REF001'\
UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
BGM+E01+DOC001+9'\
DTM+137:20250701:102'\
NAD+MS+9900123000002::293'\
IDE+24+TX001'\
STS+7+Z33'\
DTM+92:20250801:102'\
RFF+Z13:55001'\
IDE+24+TX002'\
STS+7+Z34'\
DTM+92:20250901:102'\
RFF+Z13:55002'\
UNT+12+MSG001'\
UNZ+1+REF001'";
let segments = parse_to_segments(input).unwrap();
let mig = make_utilmd_mig();
let assembler = Assembler::new(&mig);
let tree = assembler.assemble_generic(&segments).unwrap();
let sg4 = tree.groups.iter().find(|g| g.group_id == "SG4").unwrap();
assert_eq!(sg4.repetitions.len(), 2, "Should have 2 SG4 transactions");
assert_eq!(sg4.repetitions[0].segments[0].elements[1][0], "TX001");
assert_eq!(sg4.repetitions[1].segments[0].elements[1][0], "TX002");
}
#[test]
fn test_end_to_end_with_contact_info() {
let input = b"UNA:+.? '\
UNB+UNOC:3+9900123000002:500+9900456000003:500+210615:1200+REF001'\
UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
BGM+E01+DOC001+9'\
DTM+137:20250701:102'\
NAD+MS+9900123000002::293'\
CTA+IC+:Max Mustermann'\
COM+0401234567:TE'\
NAD+MR+9900456000003::293'\
UNT+7+MSG001'\
UNZ+1+REF001'";
let segments = parse_to_segments(input).unwrap();
let mig = make_utilmd_mig();
let assembler = Assembler::new(&mig);
let tree = assembler.assemble_generic(&segments).unwrap();
let sg2 = tree.groups.iter().find(|g| g.group_id == "SG2").unwrap();
assert_eq!(sg2.repetitions.len(), 2);
let sg3 = &sg2.repetitions[0].child_groups;
assert_eq!(sg3.len(), 1, "First NAD should have SG3 contact");
assert_eq!(sg3[0].repetitions[0].segments[0].tag, "CTA");
assert_eq!(sg3[0].repetitions[0].segments[1].tag, "COM");
assert!(sg2.repetitions[1].child_groups.is_empty());
}
#[test]
fn test_load_real_mig_schema() {
let Some(mig) = load_real_mig() else { return };
assert_eq!(mig.message_type, "UTILMD");
assert_eq!(mig.variant.as_deref(), Some("Strom"));
assert_eq!(mig.format_version, "FV2504");
assert!(
!mig.segments.is_empty(),
"MIG should have top-level segments"
);
assert!(
!mig.segment_groups.is_empty(),
"MIG should have segment groups"
);
eprintln!(
"Top-level segments: {:?}",
mig.segments.iter().map(|s| &s.id).collect::<Vec<_>>()
);
eprintln!(
"Segment groups: {:?}",
mig.segment_groups.iter().map(|g| &g.id).collect::<Vec<_>>()
);
}
#[test]
fn test_assemble_real_fixture_with_real_mig() {
let Some(mig) = load_real_mig() else { return };
let fixture_path = Path::new(FIXTURE_DIR).join("55001_UTILMD_S2.1_ALEXANDE121980.edi");
if !fixture_path.exists() {
eprintln!("Fixture file not found, skipping");
return;
}
let content = std::fs::read(&fixture_path).unwrap();
let segments = parse_to_segments(&content).unwrap();
eprintln!(
"Parsed {} segments from fixture: {:?}",
segments.len(),
segments.iter().map(|s| &s.id).collect::<Vec<_>>()
);
let pid = detect_pid(&segments).unwrap();
assert_eq!(pid, "55001", "Fixture should be PID 55001");
let assembler = Assembler::new(&mig);
let tree = assembler.assemble_generic(&segments).unwrap();
assert!(
tree.segments.iter().any(|s| s.tag == "UNH"),
"Should have UNH"
);
assert!(
tree.segments.iter().any(|s| s.tag == "BGM"),
"Should have BGM"
);
eprintln!(
"Assembled tree: {} top-level segments, {} groups",
tree.segments.len(),
tree.groups.len()
);
for group in &tree.groups {
eprintln!(
" Group {}: {} repetitions",
group.group_id,
group.repetitions.len()
);
}
}
#[test]
fn test_pid_detection_on_real_fixtures() {
let fixture_dir = Path::new(FIXTURE_DIR);
if !fixture_dir.exists() {
eprintln!("Fixture dir not found, skipping");
return;
}
let mut detected = 0;
let mut total = 0;
let mut pid_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for entry in std::fs::read_dir(fixture_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("edi") {
continue;
}
total += 1;
let content = std::fs::read(&path).unwrap();
let segments = match parse_to_segments(&content) {
Ok(s) => s,
Err(e) => {
eprintln!("Parse failed for {:?}: {}", path.file_name().unwrap(), e);
continue;
}
};
match detect_pid(&segments) {
Ok(pid) => {
let filename = path.file_name().unwrap().to_str().unwrap();
let expected_pid = filename.split('_').next().unwrap_or("");
if pid == expected_pid {
detected += 1;
} else {
eprintln!(
"PID mismatch for {}: detected={}, expected={}",
filename, pid, expected_pid
);
detected += 1;
}
*pid_counts.entry(pid).or_insert(0) += 1;
}
Err(_) => {
eprintln!("PID detection failed for {:?}", path.file_name().unwrap());
}
}
}
eprintln!("PID detection: {detected}/{total} fixtures");
eprintln!("PID distribution: {:?}", pid_counts);
if total > 0 {
let rate = detected as f64 / total as f64;
eprintln!("Detection rate: {:.1}%", rate * 100.0);
assert!(
rate > 0.8,
"PID detection rate too low: {detected}/{total} ({:.1}%)",
rate * 100.0
);
}
}
#[test]
fn test_assemble_all_real_fixtures_with_real_mig() {
let Some(mig) = load_real_mig() else { return };
let fixture_dir = Path::new(FIXTURE_DIR);
if !fixture_dir.exists() {
eprintln!("Fixture dir not found, skipping");
return;
}
let mut success = 0;
let mut parse_fail = 0;
let mut total = 0;
for entry in std::fs::read_dir(fixture_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("edi") {
continue;
}
total += 1;
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
let content = std::fs::read(&path).unwrap();
let segments = match parse_to_segments(&content) {
Ok(s) => s,
Err(e) => {
eprintln!("PARSE FAIL {filename}: {e}");
parse_fail += 1;
continue;
}
};
let assembler = Assembler::new(&mig);
match assembler.assemble_generic(&segments) {
Ok(tree) => {
let has_unh = tree.segments.iter().any(|s| s.tag == "UNH");
let has_bgm = tree.segments.iter().any(|s| s.tag == "BGM");
if has_unh && has_bgm {
success += 1;
} else {
eprintln!(
"INCOMPLETE {filename}: UNH={has_unh} BGM={has_bgm}, \
{} segments, {} groups",
tree.segments.len(),
tree.groups.len()
);
success += 1;
}
}
Err(e) => {
eprintln!("ASSEMBLE FAIL {filename}: {e}");
}
}
}
eprintln!("\nAssembly results: {success}/{total} succeeded, {parse_fail} parse failures");
if total > 0 {
let rate = success as f64 / total as f64;
eprintln!("Success rate: {:.1}%", rate * 100.0);
assert!(
rate > 0.9,
"Too many assembly failures: {success}/{total} ({:.1}%)",
rate * 100.0
);
}
}