master
Drew Owens Share frozen native Rust mechanical development checkpoint and qualification scope b2d7cdb 26d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070
use fusion_bom_core::*;
use std::collections::BTreeMap;

fn target() -> Target {
    Target {
        document_id: "doc-1".into(),
        revision: "rev-1".into(),
    }
}
fn properties(mass: f64) -> PhysicalProperties {
    PhysicalProperties {
        mass_kg: mass,
        volume_mm3: mass * 1000.0,
    }
}
fn metadata(id: &str, purchased: bool, excluded: bool) -> Metadata {
    let mut attributes = BTreeMap::new();
    if purchased {
        attributes.insert("asPurchased".into(), "true".into());
    }
    if excluded {
        attributes.insert("excludeFromBom".into(), "true".into());
    }
    Metadata::from_attributes(
        id.into(),
        format!("PN-{id}"),
        String::new(),
        attributes,
        None,
    )
    .unwrap()
}
fn component(id: &str, masses: &[f64], purchased: bool, excluded: bool) -> Component {
    Component {
        id: id.into(),
        aliases: Vec::new(),
        metadata: metadata(id, purchased, excluded),
        reference: EntityRef {
            document_id: "doc-1".into(),
            revision: "rev-1".into(),
            token: format!("token-{id}"),
            object_type: "adsk::fusion::Component".into(),
            occurrence_token: None,
            occurrence_path: None,
            root_component_id: (id == "root").then(|| id.into()),
        },
        direct_bodies: masses.iter().copied().map(properties).collect(),
        subtree_properties: None,
    }
}
fn occurrence(id: &str, path: &str, component: &Component, children: &[&str]) -> Occurrence {
    Occurrence {
        id: id.into(),
        path: path.into(),
        component_id: component.id.clone(),
        reference: EntityRef {
            document_id: "doc-1".into(),
            revision: "rev-1".into(),
            token: format!("occ-token-{id}"),
            object_type: "adsk::fusion::Occurrence".into(),
            occurrence_token: None,
            occurrence_path: Some(path.into()),
            root_component_id: None,
        },
        source: SourceLink {
            entity_id: format!("source-{id}"),
            object_type: "adsk::fusion::Occurrence".into(),
            owner_id: component.id.clone(),
            owner_scope: OwnerScope::Component,
            occurrence_id: None,
        },
        timeline_suppressed: Some(false),
        visible: true,
        direct_bodies: component.direct_bodies.clone(),
        children: children.iter().map(|id| (*id).into()).collect(),
    }
}
fn snapshot(
    components: Vec<Component>,
    occurrences: Vec<Occurrence>,
    children: &[&str],
) -> AssemblySnapshot {
    AssemblySnapshot {
        target: target(),
        witness: SnapshotWitness {
            session_id: "native-session-1".into(),
            design_id: "design-1".into(),
            target: target(),
            complete: true,
            read_errors: Vec::new(),
            calculation_accuracy: CalculationAccuracy::High,
        },
        configuration: ConfigurationWitness {
            target: target(),
            configured: false,
            is_configuration: false,
            configuration_row_id: None,
            complete: true,
            unsupported: Vec::new(),
            read_errors: Vec::new(),
            configured_occurrences: Vec::new(),
            table: None,
        },
        root: Root {
            component_id: "root".into(),
            children: children.iter().map(|id| (*id).into()).collect(),
        },
        components,
        occurrences,
    }
}
fn simple() -> AssemblySnapshot {
    let root = component("root", &[], false, false);
    let part = component("part", &[1.5], false, false);
    let child = occurrence("part-1", "Part:1", &part, &[]);
    snapshot(vec![root, part], vec![child], &["part-1"])
}
fn purchasing_fixture() -> AssemblySnapshot {
    let root = component("root", &[1.0], false, false);
    let mut kit = component("kit", &[2.0], true, false);
    kit.subtree_properties = Some(properties(5.0));
    let bolt = component("bolt", &[1.5], false, false);
    let housing = component("housing", &[4.0], false, true);
    let mut suppressed = occurrence("kit-3", "Kit:3", &kit, &[]);
    suppressed.timeline_suppressed = Some(true);
    let mut hidden = occurrence("hidden", "Hidden:1", &bolt, &[]);
    hidden.visible = false;
    let occurrences = vec![
        occurrence("kit-1", "Kit:1", &kit, &["a", "b"]),
        occurrence("kit-2", "Kit:2", &kit, &["c", "d"]),
        occurrence("a", "Kit:1+Bolt:1", &bolt, &[]),
        occurrence("b", "Kit:1+Bolt:2", &bolt, &[]),
        occurrence("c", "Kit:2+Bolt:1", &bolt, &[]),
        occurrence("d", "Kit:2+Bolt:2", &bolt, &[]),
        occurrence("housing", "Housing:1", &housing, &["e"]),
        occurrence("e", "Housing:1+Bolt:1", &bolt, &[]),
        suppressed,
        hidden,
    ];
    snapshot(
        vec![root, kit, bolt, housing],
        occurrences,
        &["kit-1", "kit-2", "housing", "kit-3", "hidden"],
    )
}

#[test]
fn hand_computed_purchased_fixture_keeps_purchasing_and_physical_totals_distinct() {
    let bom = generate(&purchasing_fixture(), Options::default()).unwrap();
    // Root 1 + two kits (2 + 1.5 + 1.5) + excluded housing (4 + 1.5)
    // + hidden bolt 1.5 = 18 kg and ten direct-body instances.
    assert_eq!(
        (
            bom.physical.mass,
            bom.physical.volume,
            bom.physical.body_instances
        ),
        (18.0, 18_000.0, 10)
    );
    assert_eq!(
        (
            bom.part_count,
            bom.total_purchasing_instances,
            bom.instances.len()
        ),
        (3, 4, 4)
    );
    assert_eq!(
        bom.parts
            .iter()
            .map(|row| (row.component_id.as_str(), row.quantity, row.unit_mass))
            .collect::<Vec<_>>(),
        vec![("root", 1, 1.0), ("kit", 2, 5.0), ("bolt", 1, 1.5)]
    );
    assert_eq!(bom.parts[1].scope, "component_subtree");
    assert_eq!(bom.parts[1].occurrence_paths, vec!["Kit:1", "Kit:2"]);
    assert_eq!(
        bom.excluded,
        vec![
            Exclusion {
                path: "Housing:1".into(),
                reason: "excludeFromBom".into()
            },
            Exclusion {
                path: "Kit:3".into(),
                reason: "suppressed".into()
            }
        ]
    );
    assert_eq!(bom.instances[1].child_count, 2);
    assert_eq!(bom.instances[1].parent_path.as_deref(), Some("<root>"));
    assert!(bom.instances[0].occurrence_ref.is_none());
}

#[test]
fn exploding_purchased_parts_changes_quantity_scope_but_not_physical_mass() {
    let source = purchasing_fixture();
    let collapsed = generate(&source, Options::default()).unwrap();
    let exploded = generate(
        &source,
        Options {
            explode_purchased: true,
        },
    )
    .unwrap();
    assert_eq!(exploded.physical, collapsed.physical);
    assert_eq!(
        (
            exploded.part_count,
            exploded.total_purchasing_instances,
            exploded.instances.len()
        ),
        (3, 8, 8)
    );
    assert_eq!(
        exploded
            .parts
            .iter()
            .map(|row| (row.component_id.as_str(), row.quantity, row.unit_mass))
            .collect::<Vec<_>>(),
        vec![("root", 1, 1.0), ("kit", 2, 2.0), ("bolt", 5, 1.5)]
    );
    assert!(exploded
        .parts
        .iter()
        .all(|row| row.scope == "direct_component_bodies"));
    assert!(exploded.instances.iter().all(|row| !row.as_purchased));
    // Metadata still records the source purchasing flag, while the effective
    // structured-row flag correctly reflects explodePurchased.
    assert_eq!(exploded.instances[1].metadata.as_purchased, Some(true));
    assert!(!exploded.instances[1].as_purchased);
}

#[test]
fn reused_empty_assembly_definitions_repeat_children_without_adding_flat_parts() {
    let root = component("root", &[], false, false);
    let assembly = component("assembly", &[], false, false);
    let bolt = component("bolt", &[2.0], false, false);
    let children = vec![
        occurrence("a", "A:1", &assembly, &["a1", "a2"]),
        occurrence("b", "A:2", &assembly, &["b1", "b2"]),
        occurrence("a1", "A:1+B:1", &bolt, &[]),
        occurrence("a2", "A:1+B:2", &bolt, &[]),
        occurrence("b1", "A:2+B:1", &bolt, &[]),
        occurrence("b2", "A:2+B:2", &bolt, &[]),
    ];
    let bom = generate(
        &snapshot(vec![root, assembly, bolt], children, &["a", "b"]),
        Options::default(),
    )
    .unwrap();
    assert_eq!(
        (
            bom.part_count,
            bom.total_purchasing_instances,
            bom.instances.len()
        ),
        (1, 4, 7)
    );
    assert_eq!(bom.physical.mass, 8.0);
    assert_eq!(
        bom.parts[0].occurrence_paths,
        vec!["A:1+B:1", "A:1+B:2", "A:2+B:1", "A:2+B:2"]
    );
}

#[test]
fn root_bom_exclusion_prunes_all_purchasing_but_not_physical_inventory() {
    let mut source = purchasing_fixture();
    source.components[0].metadata = metadata("root", false, true);
    let bom = generate(&source, Options::default()).unwrap();
    assert!(bom.parts.is_empty() && bom.instances.is_empty());
    assert_eq!(
        bom.excluded,
        vec![Exclusion {
            path: "<root>".into(),
            reason: "excludeFromBom".into()
        }]
    );
    assert_eq!(bom.physical.mass, 18.0);
}

#[test]
fn purchasing_uses_definition_properties_while_physical_uses_each_context() {
    let mut source = simple();
    let mut second = occurrence("part-2", "Part:2", &source.components[1], &[]);
    second.direct_bodies[0] = properties(2.5);
    source.occurrences.push(second);
    source.root.children.push("part-2".into());
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!((bom.parts[0].quantity, bom.parts[0].unit_mass), (2, 1.5));
    assert_eq!(bom.physical.mass, 4.0);
}

#[test]
fn purchased_unit_mass_is_explicit_native_subtree_observation_not_flat_sum() {
    let mut source = purchasing_fixture();
    source.components[1].subtree_properties = Some(properties(6.25));
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!(bom.parts[1].unit_mass, 6.25);
    assert_eq!(bom.physical.mass, 18.0);
    source.components[1].subtree_properties = None;
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "source_incomplete"
    );
    assert!(generate(
        &source,
        Options {
            explode_purchased: true
        }
    )
    .is_ok());
}

#[test]
fn purchased_empty_container_is_a_flat_part() {
    let mut source = simple();
    source.components[0].metadata = metadata("root", true, false);
    source.components[0].subtree_properties = Some(properties(1.5));
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!(
        (
            bom.part_count,
            bom.parts[0].component_id.as_str(),
            bom.parts[0].quantity
        ),
        (1, "root", 1)
    );
    assert_eq!(bom.physical.mass, 1.5);
}

#[test]
fn proven_wrapper_aliases_aggregate_and_names_part_numbers_tokens_do_not() {
    let mut source = simple();
    source.components[1].aliases.push("wrapper-alias".into());
    let mut second = occurrence("part-2", "Part:2", &source.components[1], &[]);
    second.component_id = "wrapper-alias".into();
    second.source.owner_id = "wrapper-alias".into();
    source.occurrences.push(second);
    source.root.children.push("part-2".into());
    let mut different = source.components[1].clone();
    different.id = "different-definition".into();
    different.aliases.clear();
    // Same name, part number and token deliberately remain separate.
    let third = occurrence("part-3", "Part:3", &different, &[]);
    source.components.push(different);
    source.occurrences.push(third);
    source.root.children.push("part-3".into());
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!(
        bom.parts
            .iter()
            .map(|row| (row.component_id.as_str(), row.quantity))
            .collect::<Vec<_>>(),
        vec![("part", 2), ("different-definition", 1)]
    );
    assert_eq!(bom.physical.mass, 4.5);
}

#[test]
fn ambiguous_aliases_duplicate_paths_and_shared_instance_nodes_fail_closed() {
    let mut source = simple();
    source.components[1].aliases.push("root".into());
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "ambiguous_identity"
    );
    let mut source = simple();
    let mut duplicate = source.occurrences[0].clone();
    duplicate.id = "part-2".into();
    source.occurrences.push(duplicate);
    source.root.children.push("part-2".into());
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "ambiguous_identity"
    );
    let mut source = simple();
    source.root.children.push("part-1".into());
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "invalid_assembly_graph"
    );
}

#[test]
fn cyclic_component_containment_is_detected_before_suppression_return() {
    let mut root = component("root", &[], false, false);
    root.aliases.push("root-wrapper".into());
    let mut child = occurrence("cycle", "Cycle:1", &root, &[]);
    child.component_id = "root-wrapper".into();
    child.timeline_suppressed = Some(true);
    let source = snapshot(vec![root], vec![child], &["cycle"]);
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "cyclic_assembly"
    );
}

#[test]
fn combined_purchasing_physical_budget_is_not_reset_between_traversals() {
    let source = simple();
    let limits = Limits {
        max_component_visits: 4,
        ..Limits::default()
    };
    assert!(generate_with_limits(&source, Options::default(), limits).is_ok());
    let limits = Limits {
        max_component_visits: 3,
        ..limits
    };
    assert_eq!(
        generate_with_limits(&source, Options::default(), limits)
            .unwrap_err()
            .code,
        "bom_budget"
    );
    let limits = Limits {
        max_physical_body_instances: 0,
        ..Limits::default()
    };
    assert_eq!(
        generate_with_limits(&source, Options::default(), limits)
            .unwrap_err()
            .code,
        "bom_budget"
    );
    let limits = Limits {
        max_depth: 101,
        ..Limits::default()
    };
    assert_eq!(
        generate_with_limits(&source, Options::default(), limits)
            .unwrap_err()
            .code,
        "invalid_limits"
    );
}

#[test]
fn depth_100_is_allowed_and_101_is_refused() {
    fn chain(depth: usize) -> AssemblySnapshot {
        let mut components = vec![component("root", &[], false, false)];
        let mut occurrences = Vec::new();
        for level in 1..=depth {
            let comp = component(&format!("c{level}"), &[], false, false);
            let next = format!("o{}", level + 1);
            let children = if level < depth {
                vec![next.as_str()]
            } else {
                vec![]
            };
            occurrences.push(occurrence(
                &format!("o{level}"),
                &format!("level-{level}"),
                &comp,
                &children,
            ));
            components.push(comp);
        }
        snapshot(components, occurrences, &["o1"])
    }
    assert!(generate(&chain(100), Options::default()).is_ok());
    assert_eq!(
        generate(&chain(101), Options::default()).unwrap_err().code,
        "bom_budget"
    );
}

#[test]
fn invalid_nonfinite_negative_and_overflowing_physical_data_is_refused() {
    for value in [f64::NAN, f64::INFINITY, -1.0] {
        let mut source = simple();
        source.occurrences[0].direct_bodies[0].mass_kg = value;
        assert_eq!(
            generate(&source, Options::default()).unwrap_err().code,
            "invalid_physical_properties"
        );
    }
    let mut source = simple();
    source.components[1].direct_bodies = vec![
        PhysicalProperties {
            mass_kg: f64::MAX,
            volume_mm3: 1.0
        };
        2
    ];
    source.occurrences[0].direct_bodies = source.components[1].direct_bodies.clone();
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "invalid_physical_properties"
    );
    assert_eq!(
        PhysicalProperties::from_fusion_units(2.0, 3.0).unwrap(),
        PhysicalProperties {
            mass_kg: 2.0,
            volume_mm3: 3000.0
        }
    );
    assert!(PhysicalProperties::from_fusion_units(1.0, f64::MAX).is_err());
}

#[test]
fn source_witness_and_reference_completeness_are_not_inferred_from_partial_data() {
    let base = simple();
    let mut cases = Vec::new();
    let mut source = base.clone();
    source.witness.complete = false;
    cases.push(source);
    let mut source = base.clone();
    source
        .witness
        .read_errors
        .push("failed body properties".into());
    cases.push(source);
    let mut source = base.clone();
    source.witness.target.revision = "stale".into();
    cases.push(source);
    let mut source = base.clone();
    source.witness.session_id.clear();
    cases.push(source);
    let mut source = base.clone();
    source.witness.calculation_accuracy = CalculationAccuracy::Low;
    cases.push(source);
    let mut source = base.clone();
    source.components[1].reference.document_id = "foreign".into();
    cases.push(source);
    let mut source = base.clone();
    source.occurrences[0].direct_bodies.clear();
    cases.push(source);
    let mut source = base.clone();
    source.occurrences[0].reference.occurrence_path = Some("wrong".into());
    cases.push(source);
    let mut source = base.clone();
    source.root.children.push("missing".into());
    cases.push(source);
    for source in cases {
        assert_eq!(
            generate(&source, Options::default()).unwrap_err().code,
            "source_incomplete"
        );
    }
    let mut source = base;
    source.root.children.clear();
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "invalid_assembly_graph"
    );
}

#[test]
fn metadata_decodes_json_attributes_strictly_and_preserves_supplier_provenance() {
    let attrs = BTreeMap::from([
        ("supplier".into(), "\"McMaster-Carr\"".into()),
        ("supplierPartNumber".into(), "\"91290A115\"".into()),
        ("sourceFile".into(), "\"C:\\\\CAD\\\\part.step\"".into()),
        ("sha256".into(), "\"digest\"".into()),
        ("asPurchased".into(), "true".into()),
        ("unknownLegacyAttribute".into(), "not JSON".into()),
    ]);
    let data = Metadata::from_attributes(
        "Bolt".into(),
        "PN".into(),
        String::new(),
        attrs.clone(),
        None,
    )
    .unwrap();
    assert_eq!(data.supplier.as_deref(), Some("McMaster-Carr"));
    assert_eq!(data.supplier_part_number.as_deref(), Some("91290A115"));
    assert_eq!(data.source_file.as_deref(), Some("C:\\CAD\\part.step"));
    assert_eq!(data.attributes, attrs);
    for invalid in ["1", "\"true\"", "null", "{", "[]"] {
        let attrs = BTreeMap::from([("asPurchased".into(), invalid.into())]);
        assert_eq!(
            Metadata::from_attributes("Bolt".into(), "PN".into(), String::new(), attrs, None)
                .unwrap_err()
                .code,
            "invalid_metadata"
        );
    }
    let mut source = simple();
    source.components[1].metadata.as_purchased = Some(true);
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "invalid_metadata"
    );
}

fn configured_source() -> AssemblySnapshot {
    let mut source = simple();
    source.occurrences[0].source.entity_id = "same-native-occurrence".into();
    source.occurrences[0].source.occurrence_id = Some("context-a".into());
    let mut second = occurrence("part-2", "Part:2", &source.components[1], &[]);
    second.source.entity_id = "same-native-occurrence".into();
    second.source.occurrence_id = Some("context-b".into());
    second.visible = false;
    second.timeline_suppressed = None;
    source.occurrences.push(second);
    source.root.children.push("part-2".into());
    let suppress = source.occurrences[0].source.clone();
    let visibility = source.occurrences[1].source.clone();
    let rows = vec![
        ConfigurationRow {
            id: "active".into(),
            name: "Working".into(),
            scope: RowScope::Ordinary,
            parent_id: None,
            child_ids: vec![],
        },
        ConfigurationRow {
            id: "other".into(),
            name: "Other".into(),
            scope: RowScope::Ordinary,
            parent_id: None,
            child_ids: vec![],
        },
        ConfigurationRow {
            id: "test".into(),
            name: "Reserved".into(),
            scope: RowScope::Test,
            parent_id: None,
            child_ids: vec![],
        },
    ];
    let mut cells = Vec::new();
    for row in &rows {
        cells.push(ConfigurationCell {
            row_id: row.id.clone(),
            column_id: "suppress".into(),
            scope: row.scope,
            value: CellValue::ConfigurationSuppressCell {
                is_suppressed: false,
                is_effectively_suppressed: row.id != "other",
            },
        });
        cells.push(ConfigurationCell {
            row_id: row.id.clone(),
            column_id: "visible".into(),
            scope: row.scope,
            value: CellValue::ConfigurationVisibilityCell { is_visible: false },
        });
    }
    source.configuration.configured = true;
    source.configuration.table = Some(ConfigurationTable {
        id: "table-1".into(),
        active_row_id: "active".into(),
        active_row_name: "Working".into(),
        has_custom_columns: false,
        theme_table_ids: vec![],
        rows,
        columns: vec![
            ConfigurationColumn {
                id: "suppress".into(),
                kind: ColumnKind::Suppress,
                source: Some(suppress),
            },
            ConfigurationColumn {
                id: "visible".into(),
                kind: ColumnKind::Visibility,
                source: Some(visibility),
            },
        ],
        cells,
    });
    source
}

#[test]
fn active_configuration_effective_suppression_respects_exact_occurrence_context() {
    let source = configured_source();
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!((bom.parts[0].quantity, bom.physical.mass), (1, 1.5));
    assert_eq!(bom.parts[0].occurrence_paths, vec!["Part:2"]);
    assert_eq!(bom.excluded[0].reason, "configuration_suppressed");
    assert_eq!(bom.configuration_suppression.len(), 1);
    assert_eq!(bom.configuration_suppression[0].row_id, "active");
    assert_eq!(bom.configuration.row_id.as_deref(), Some("active"));
    assert_eq!(bom.configuration.table_id.as_deref(), Some("table-1"));
}

#[test]
fn configuration_without_context_suppresses_all_reused_native_instances() {
    let mut source = configured_source();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .occurrence_id = None;
    let bom = generate(&source, Options::default()).unwrap();
    assert!(bom.parts.is_empty());
    assert_eq!(
        (
            bom.physical.mass,
            bom.physical.body_instances,
            bom.excluded.len()
        ),
        (0.0, 0, 2)
    );
}

#[test]
fn inactive_test_rows_and_visibility_never_suppress_a_bom_instance() {
    let mut source = configured_source();
    let table = source.configuration.table.as_mut().unwrap();
    table.active_row_id = "other".into();
    table.active_row_name = "Other".into();
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!((bom.parts[0].quantity, bom.physical.mass), (2, 3.0));
    assert!(bom.excluded.is_empty());
    assert_eq!(bom.configuration_suppression[0].row_id, "other");
}

#[test]
fn timeline_suppression_takes_precedence_and_non_occurrence_sources_are_not_instance_filters() {
    let mut source = configured_source();
    source.occurrences[0].timeline_suppressed = Some(true);
    assert_eq!(
        generate(&source, Options::default()).unwrap().excluded[0].reason,
        "suppressed"
    );
    let mut source = configured_source();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .object_type = "adsk::fusion::ExtrudeFeature".into();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .entity_id = "observed-feature-source".into();
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!(bom.parts[0].quantity, 2);
    assert_eq!(bom.physical.mass, 3.0);
}

#[test]
fn incomplete_unsupported_generated_and_external_configuration_scopes_are_refused() {
    let base = configured_source();
    let mut cases = Vec::new();
    let mut source = base.clone();
    source.configuration.complete = false;
    cases.push(source);
    let mut source = base.clone();
    source
        .configuration
        .unsupported
        .push("custom column".into());
    cases.push(source);
    let mut source = base.clone();
    source.configuration.read_errors.push("read failed".into());
    cases.push(source);
    let mut source = base.clone();
    source.configuration.is_configuration = true;
    cases.push(source);
    let mut source = base.clone();
    source
        .configuration
        .table
        .as_mut()
        .unwrap()
        .has_custom_columns = true;
    cases.push(source);
    let mut source = base.clone();
    source
        .configuration
        .table
        .as_mut()
        .unwrap()
        .theme_table_ids
        .push("materials".into());
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().rows[0]
        .child_ids
        .push("other".into());
    cases.push(source);
    let mut source = base;
    source
        .configuration
        .configured_occurrences
        .push(ConfiguredOccurrence {
            occurrence_id: "part-1".into(),
            path: "Part:1".into(),
            table_id: "foreign-table".into(),
            row_id: "row".into(),
            row_name: "Row".into(),
            source_file_id: "cloud-id".into(),
            source_version: 3,
        });
    cases.push(source);
    for source in cases {
        assert_eq!(
            generate(&source, Options::default()).unwrap_err().code,
            "configuration_scope_unsupported"
        );
    }
}

#[test]
fn configuration_matrix_addresses_types_flags_and_provenance_are_checked() {
    let base = configured_source();
    let mut cases = Vec::new();
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().cells.pop();
    cases.push(source);
    let mut source = base.clone();
    let table = source.configuration.table.as_mut().unwrap();
    table.cells[1] = table.cells[0].clone();
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().cells[0].scope = RowScope::Test;
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().cells[0].value =
        CellValue::ConfigurationVisibilityCell { is_visible: false };
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().columns[0].source = None;
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().active_row_name = "stale-name".into();
    cases.push(source);
    let mut source = base.clone();
    let table = source.configuration.table.as_mut().unwrap();
    table.active_row_id = "test".into();
    table.active_row_name = "Reserved".into();
    cases.push(source);
    let mut source = base.clone();
    source.configuration.configured = false;
    cases.push(source);
    for source in cases {
        assert_eq!(
            generate(&source, Options::default()).unwrap_err().code,
            "configuration_read_failed"
        );
    }
    let mut source = base;
    source.configuration.target.revision = "foreign-rev".into();
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "configuration_source_mismatch"
    );
}

#[test]
fn json_roundtrip_keeps_exact_float_observations_and_typed_booleans() {
    let mut source = simple();
    source.components[1].direct_bodies[0].mass_kg = f64::from_bits(0x402d_ffff_ffff_fffc);
    source.occurrences[0].direct_bodies = source.components[1].direct_bodies.clone();
    let bytes = serde_json::to_vec(&source).unwrap();
    let parsed: AssemblySnapshot = serde_json::from_slice(&bytes).unwrap();
    let bom = generate(&parsed, Options::default()).unwrap();
    assert_eq!(bom.physical.mass.to_bits(), 0x402d_ffff_ffff_fffc);
    let mut json = serde_json::to_value(source).unwrap();
    json["occurrences"][0]["timelineSuppressed"] = serde_json::json!(1);
    assert!(serde_json::from_value::<AssemblySnapshot>(json).is_err());
}

#[test]
fn flat_csv_has_exact_columns_context_and_basic_numeric_spelling() {
    let bom = generate(&simple(), Options::default()).unwrap();
    let csv = String::from_utf8(csv_bytes(&bom, CsvMode::Flat).unwrap()).unwrap();
    assert_eq!(csv, concat!(
        "componentId,partNumber,name,description,quantity,supplier,supplierPartNumber,sourceUrl,sha256,unitMass,unitVolume,scope,documentId,revision,configurationTableId,configurationRowId,configurationRowName\r\n",
        "part,PN-part,part,,1,,,,,1.5,1500.0,direct_component_bodies,doc-1,rev-1,,,\r\n"));
}

#[test]
fn structured_csv_has_containers_quantity_one_effective_purchase_flag_and_context() {
    let bom = generate(&configured_source(), Options::default()).unwrap();
    let csv = String::from_utf8(csv_bytes(&bom, CsvMode::Structured).unwrap()).unwrap();
    assert_eq!(csv, concat!(
        "path,parentPath,depth,componentId,name,partNumber,description,quantity,bodyCount,childCount,asPurchased,supplier,supplierPartNumber,sourceUrl,sha256,documentId,revision,configurationTableId,configurationRowId,configurationRowName\r\n",
        "<root>,,0,root,root,PN-root,,1,0,2,False,,,,,doc-1,rev-1,table-1,active,Working\r\n",
        "Part:2,<root>,1,part,part,PN-part,,1,1,0,False,,,,,doc-1,rev-1,table-1,active,Working\r\n"));
    let bom = generate(&purchasing_fixture(), Options::default()).unwrap();
    let collapsed = String::from_utf8(csv_bytes(&bom, CsvMode::Structured).unwrap()).unwrap();
    assert!(collapsed.contains("Kit:1,<root>,1,kit,kit,PN-kit,,1,1,2,True,"));
    let bom = generate(
        &purchasing_fixture(),
        Options {
            explode_purchased: true,
        },
    )
    .unwrap();
    let exploded = String::from_utf8(csv_bytes(&bom, CsvMode::Structured).unwrap()).unwrap();
    assert!(exploded.contains("Kit:1,<root>,1,kit,kit,PN-kit,,1,1,2,False,"));
}

#[test]
fn csv_escapes_quotes_commas_multiline_unicode_and_neutralizes_formulas() {
    let mut source = simple();
    let metadata = &mut source.components[1].metadata;
    metadata.name = "=HYPERLINK(\"bad\",\"Ö\")".into();
    metadata.part_number = "PN,7".into();
    metadata.description = "First\r\nSecond \"quoted\" 🦀".into();
    let bom = generate(&source, Options::default()).unwrap();
    let csv = String::from_utf8(csv_bytes(&bom, CsvMode::Flat).unwrap()).unwrap();
    assert!(csv.contains("part,\"PN,7\",\"'=HYPERLINK(\"\"bad\"\",\"\"Ö\"\")\",\"First\r\nSecond \"\"quoted\"\" 🦀\",1,"));
    for value in [
        "=1+1",
        "+1",
        "-123",
        "@SUM(A1)",
        "  =SUM(A1)",
        "\t=1",
        "\n@x",
        "\u{feff}=1",
    ] {
        let mut bom = bom.clone();
        bom.parts[0].metadata.name = value.into();
        let csv = String::from_utf8(csv_bytes(&bom, CsvMode::Flat).unwrap()).unwrap();
        assert!(
            csv.contains(&format!("'{value}")),
            "not neutralized: {value:?}"
        );
    }
}

#[test]
fn csv_does_not_rewrite_regular_text_and_refuses_invalid_or_oversized_output() {
    let bom = generate(&simple(), Options::default()).unwrap();
    let mut regular = bom.clone();
    regular.parts[0].metadata.name = "A-1 @ workshop".into();
    let csv = String::from_utf8(csv_bytes(&regular, CsvMode::Flat).unwrap()).unwrap();
    assert!(csv.contains(",A-1 @ workshop,"));
    let mut invalid = bom.clone();
    invalid.parts[0].unit_mass = f64::NAN;
    assert_eq!(
        csv_bytes(&invalid, CsvMode::Flat).unwrap_err().code,
        "invalid_physical_properties"
    );
    let mut incomplete = bom.clone();
    incomplete.complete = false;
    assert_eq!(
        csv_bytes(&incomplete, CsvMode::Flat).unwrap_err().code,
        "source_incomplete"
    );
    let mut large = bom;
    large.parts[0].metadata.name = "\"".repeat(MAX_CSV_BYTES / 2);
    assert_eq!(
        csv_bytes(&large, CsvMode::Flat).unwrap_err().code,
        "bom_budget"
    );
}

#[test]
fn repeated_large_metadata_is_bounded_before_result_materialization() {
    let mut source = simple();
    // The input contains this string once. A part plus its structured instance
    // would duplicate it past the 32 MiB output budget without the early charge.
    source.components[1].metadata.name = "x".repeat(17 * 1024 * 1024);
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "bom_budget"
    );
}

#[test]
fn configuration_parameter_sources_must_belong_to_the_observed_design() {
    let mut source = configured_source();
    let table = source.configuration.table.as_mut().unwrap();
    table.columns.push(ConfigurationColumn {
        id: "parameter".into(),
        kind: ColumnKind::Parameter,
        source: Some(SourceLink {
            entity_id: "parameter-id".into(),
            object_type: "adsk::fusion::UserParameter".into(),
            owner_id: "design-1".into(),
            owner_scope: OwnerScope::Design,
            occurrence_id: None,
        }),
    });
    for row in &table.rows {
        table.cells.push(ConfigurationCell {
            row_id: row.id.clone(),
            column_id: "parameter".into(),
            scope: row.scope,
            value: CellValue::ConfigurationParameterCell {
                expression: "12 mm".into(),
            },
        });
    }
    assert!(generate(&source, Options::default()).is_ok());
    source.configuration.table.as_mut().unwrap().columns[2]
        .source
        .as_mut()
        .unwrap()
        .owner_id = "foreign-design".into();
    assert_eq!(
        generate(&source, Options::default()).unwrap_err().code,
        "configuration_source_mismatch"
    );
}

#[test]
fn suppression_sources_reconcile_exact_type_and_canonical_owner_with_observed_instances() {
    let base = configured_source();
    let mut source = base.clone();
    source.components[1]
        .aliases
        .push("proven-owner-alias".into());
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .owner_id = "proven-owner-alias".into();
    let bom = generate(&source, Options::default()).unwrap();
    assert_eq!(bom.parts[0].occurrence_paths, vec!["Part:2"]);

    let mut cases = Vec::new();
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .owner_id = "root".into();
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .object_type = "fake::Occurrence".into();
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .object_type = "adsk::core::Occurrence".into();
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .object_type = "adsk::fusion::ExtrudeFeature".into();
    cases.push(source);
    let mut source = base.clone();
    let controlled = source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap();
    controlled.owner_scope = OwnerScope::Design;
    controlled.owner_id = "design-1".into();
    cases.push(source);
    let mut source = base.clone();
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .entity_id = "unobserved-entity".into();
    cases.push(source);
    let mut source = base;
    source.configuration.table.as_mut().unwrap().columns[0]
        .source
        .as_mut()
        .unwrap()
        .occurrence_id = Some("unobserved-context".into());
    cases.push(source);
    for source in cases {
        assert_eq!(
            generate(&source, Options::default()).unwrap_err().code,
            "configuration_source_mismatch"
        );
    }
}