//! Bulk roundtrip tests for all 187 UTILMD Strom PIDs (FV2604).
//!
//! Uses generated fixtures from `example_market_communication_bo4e_transactions/UTILMD/FV2604/generated_strom/`.
//! Full pipeline: EDIFACT → tokenize → split → assemble → map_interchange
//! → map_interchange_reverse → disassemble → render → byte-identical comparison.
//!
//! FV2604 schemas are structurally identical to FV2510 (same 187 PIDs, same field
//! paths). TOML mappings are copied from FV2510 and work unchanged.

mod common;

use common::{bo4e_validation, test_utils, utilmd_strom_fv2604};
use edifact_types::EdifactDelimiters;
use mig_assembly::assembler::Assembler;
use mig_assembly::disassembler::Disassembler;
use mig_assembly::renderer::render_edifact;
use mig_assembly::tokenize::{parse_to_segments, split_messages};
use mig_bo4e::engine::MappingEngine;

/// PIDs with known structural limitations preventing byte-identical roundtrip.
const KNOWN_INCOMPLETE: &[&str] = &[
    "55002", "55602", "55641", "55651",
    // Monster PIDs and PIDs with SG12/SG8 variants in non-alphabetical MIG order —
    // correct variant ordering exposes TOML mapping gaps.
    "55043", "55168", "55169", "55600",
];

/// All 187 UTILMD Strom PIDs — unified list for byte-identical roundtrip testing.
const ALL_PIDS: &[&str] = &[
    "55001", "55002", "55003", "55004", "55005", "55006", "55007", "55008", "55009", "55010",
    "55011", "55012", "55013", "55014", "55015", "55016", "55017", "55018", "55022", "55023",
    "55024", "55035", "55036", "55037", "55038", "55039", "55040", "55041", "55042", "55043",
    "55044", "55051", "55052", "55053", "55060", "55062", "55063", "55064", "55065", "55066",
    "55067", "55069", "55070", "55071", "55072", "55073", "55074", "55075", "55076", "55077",
    "55078", "55080", "55095", "55109", "55110", "55126", "55136", "55137", "55156", "55168",
    "55169", "55170", "55173", "55175", "55177", "55180", "55194", "55195", "55196", "55197",
    "55198", "55199", "55200", "55201", "55202", "55203", "55204", "55205", "55206", "55207",
    "55208", "55209", "55210", "55211", "55212", "55213", "55214", "55218", "55220", "55223",
    "55224", "55225", "55227", "55230", "55232", "55235", "55236", "55237", "55238", "55239",
    "55240", "55241", "55242", "55243", "55553", "55555", "55557", "55559", "55600", "55601",
    "55602", "55603", "55604", "55605", "55607", "55608", "55609", "55611", "55613", "55614",
    "55615", "55616", "55617", "55618", "55619", "55620", "55621", "55622", "55623", "55624",
    "55625", "55626", "55627", "55628", "55629", "55630", "55632", "55633", "55634", "55635",
    "55636", "55638", "55639", "55640", "55641", "55642", "55643", "55644", "55645", "55646",
    "55647", "55648", "55649", "55650", "55651", "55652", "55653", "55654", "55655", "55656",
    "55657", "55658", "55659", "55660", "55661", "55662", "55663", "55664", "55665", "55666",
    "55667", "55669", "55670", "55671", "55672", "55673", "55674", "55675", "55684", "55685",
    "55686", "55687", "55688", "55689", "55690", "55691", "55692",
];

#[test]
fn test_all_pids_roundtrip() {
    assert_eq!(ALL_PIDS.len(), 187, "should cover all 187 Strom PIDs");

    let mut passed = 0;
    let mut failed: Vec<String> = Vec::new();

    for pid in ALL_PIDS {
        if KNOWN_INCOMPLETE.contains(pid) {
            eprintln!("PID {pid}: KNOWN_INCOMPLETE — skipping");
            continue;
        }
        match std::panic::catch_unwind(|| run_generated_roundtrip(pid)) {
            Ok(()) => passed += 1,
            Err(_) => {
                eprintln!("PID {pid}: FAILED roundtrip");
                failed.push(pid.to_string());
            }
        }
    }

    eprintln!(
        "\nStrom FV2604 bulk roundtrip: {passed} passed, {} failed, {} known-incomplete, {} total",
        failed.len(),
        KNOWN_INCOMPLETE.len(),
        ALL_PIDS.len()
    );
    if !failed.is_empty() {
        eprintln!("Failed PIDs: {:?}", failed);
    }
    assert!(
        failed.is_empty(),
        "Expected all PIDs to pass byte-identical roundtrip, but {} failed: {:?}",
        failed.len(),
        failed
    );
}

#[test]
fn test_pid_count_coverage() {
    assert_eq!(ALL_PIDS.len(), 187, "should cover all 187 Strom PIDs");
    // Verify no duplicates
    let mut sorted = ALL_PIDS.to_vec();
    sorted.sort();
    sorted.dedup();
    assert_eq!(sorted.len(), 187, "should have no duplicate PIDs");
}

// ── SG2/SG3 normalization ──

/// Normalize SG2/SG3 block ordering in rendered EDIFACT.
///
/// Generated fixtures place CTA/COM (SG3) after ALL NAD (SG2) segments,
/// while the reverse mapper places them under the first SG2 rep.
/// This normalizes by moving CTA/COM segments to just after the first NAD.
fn normalize_sg2_sg3_ordering(rendered: &str) -> String {
    let segs: Vec<&str> = rendered.split('\'').collect();
    let mut result: Vec<&str> = Vec::new();
    let mut sg3_segs: Vec<&str> = Vec::new();
    let mut first_nad_idx: Option<usize> = None;
    let mut past_sg2 = false;

    for seg in &segs {
        let tag = seg.split('+').next().unwrap_or("");
        if tag == "NAD" && !past_sg2 {
            if first_nad_idx.is_none() {
                first_nad_idx = Some(result.len());
            }
            result.push(seg);
        } else if (tag == "CTA" || tag == "COM") && !past_sg2 {
            sg3_segs.push(seg);
        } else {
            if !past_sg2 && first_nad_idx.is_some() && !sg3_segs.is_empty() {
                let insert_pos = first_nad_idx.unwrap() + 1;
                for (i, sg3) in sg3_segs.drain(..).enumerate() {
                    result.insert(insert_pos + i, sg3);
                }
                past_sg2 = true;
            } else if !past_sg2 && first_nad_idx.is_some() {
                past_sg2 = true;
            }
            result.push(seg);
        }
    }
    if !sg3_segs.is_empty() {
        if let Some(idx) = first_nad_idx {
            let insert_pos = idx + 1;
            for (i, sg3) in sg3_segs.into_iter().enumerate() {
                result.insert(insert_pos + i, sg3);
            }
        }
    }
    result.join("'")
}

// ── Roundtrip helper (byte-identical) ──

fn run_generated_roundtrip(pid: &str) {
    let Some(fixture_path) = utilmd_strom_fv2604::discover_generated_fixture(pid) else {
        panic!("PID {pid}: no FV2604 generated fixture found");
    };

    let Some(filtered_mig) = utilmd_strom_fv2604::load_pid_filtered_mig(pid) else {
        panic!("PID {pid}: FV2604 MIG/AHB XML not available");
    };

    let tx_dir = utilmd_strom_fv2604::pid_dir(pid);
    if !utilmd_strom_fv2604::message_dir().exists() || !tx_dir.exists() {
        panic!("PID {pid}: FV2604 mapping directories not found");
    }
    let (msg_engine, tx_engine) = utilmd_strom_fv2604::load_split_engines(pid);

    let fixture_name = fixture_path.file_name().unwrap().to_str().unwrap();
    let edifact_input = std::fs::read_to_string(&fixture_path).unwrap();

    // Step 1: Tokenize and split
    let segments = parse_to_segments(edifact_input.as_bytes()).unwrap();
    let chunks = split_messages(segments).unwrap();
    assert!(
        !chunks.messages.is_empty(),
        "PID {pid} ({fixture_name}): should have at least one message"
    );

    let msg_chunk = &chunks.messages[0];
    let mut msg_segs = vec![msg_chunk.unh.clone()];
    msg_segs.extend(msg_chunk.body.iter().cloned());
    msg_segs.push(msg_chunk.unt.clone());

    // Step 2: Assemble with PID-filtered MIG
    let assembler = Assembler::new(&filtered_mig);
    let original_tree = assembler.assemble_generic(&msg_segs).unwrap();

    // Step 3: Forward mapping
    let mapped = MappingEngine::map_interchange(
        &msg_engine,
        &tx_engine,
        &original_tree,
        utilmd_strom_fv2604::TX_GROUP,
        true,
    );

    assert!(
        !mapped.transaktionen.is_empty(),
        "PID {pid} ({fixture_name}): forward mapping should produce at least one transaction"
    );

    // Step 3b: BO4E schema validation
    let mapped_for_validation = MappingEngine::map_interchange(
        &msg_engine,
        &tx_engine,
        &original_tree,
        utilmd_strom_fv2604::TX_GROUP,
        false,
    );
    bo4e_validation::validate_mapped_message(
        pid,
        fixture_name,
        &msg_engine,
        &tx_engine,
        &mapped_for_validation,
    );

    // Step 4: Reverse mapping
    let mut reverse_tree = MappingEngine::map_interchange_reverse(
        &msg_engine,
        &tx_engine,
        &mapped,
        utilmd_strom_fv2604::TX_GROUP,
        Some(&filtered_mig),
    );

    let unh_assembled = test_utils::owned_to_assembled(&msg_chunk.unh);
    reverse_tree.segments.insert(0, unh_assembled);
    reverse_tree.post_group_start += 1;

    let original_has_unt = original_tree.segments.last().map(|s| s.tag.as_str()) == Some("UNT");
    if original_has_unt {
        let unt_assembled = test_utils::owned_to_assembled(&msg_chunk.unt);
        reverse_tree.segments.push(unt_assembled);
    }

    // Step 5: Disassemble and render
    let disassembler = Disassembler::new(&filtered_mig);
    let delimiters = EdifactDelimiters::default();

    let original_dis = disassembler.disassemble(&original_tree);
    let original_rendered = render_edifact(&original_dis, &delimiters);

    let reverse_dis = disassembler.disassemble(&reverse_tree);
    let reverse_rendered = render_edifact(&reverse_dis, &delimiters);

    // Step 6: Normalize SG2/SG3 ordering
    let original_normalized = normalize_sg2_sg3_ordering(&original_rendered);
    let reverse_normalized = normalize_sg2_sg3_ordering(&reverse_rendered);

    // Step 7: Compare segment tags
    let orig_norm_segs: Vec<&str> = original_normalized.split('\'').collect();
    let rev_norm_segs: Vec<&str> = reverse_normalized.split('\'').collect();
    let original_tags: Vec<&str> = orig_norm_segs
        .iter()
        .map(|s| s.split('+').next().unwrap_or(""))
        .collect();
    let reverse_tags: Vec<&str> = rev_norm_segs
        .iter()
        .map(|s| s.split('+').next().unwrap_or(""))
        .collect();

    if original_tags != reverse_tags {
        eprintln!("PID {pid} ({fixture_name}): segment tag mismatch!");
        for (i, tag) in original_tags.iter().enumerate() {
            if reverse_tags.get(i) != Some(tag) {
                eprintln!(
                    "  first difference at position {i}: original={tag}, reversed={}",
                    reverse_tags.get(i).unwrap_or(&"<missing>")
                );
                break;
            }
        }
    }

    assert_eq!(
        original_tags, reverse_tags,
        "PID {pid} ({fixture_name}): segment tags should match after roundtrip"
    );

    // Step 8: Compare full rendered EDIFACT (normalized)
    assert_eq!(
        original_normalized, reverse_normalized,
        "PID {pid} ({fixture_name}): full EDIFACT roundtrip should be byte-identical"
    );

    eprintln!(
        "PID {pid}: {fixture_name} -- roundtrip OK, {} segments byte-identical",
        original_tags.len()
    );
}
