use super::*;

fn strip_xml_comments(input: &str) -> String {
    let mut out = String::with_capacity(input.len());
    let mut pos = 0usize;
    while pos < input.len() {
        let Some(rel) = input[pos..].find('<') else {
            out.push_str(&input[pos..]);
            break;
        };
        let start = pos + rel;
        out.push_str(&input[pos..start]);
        if (input[start..].starts_with("<!--") || input[start..].starts_with("<!---"))
            && let Some(close) = input[start..].find("-->")
        {
            pos = start + close + 3;
            continue;
        }
        if let Some(tag_end) = input[start..].find('>') {
            out.push_str(&input[start..start + tag_end + 1]);
            pos = start + tag_end + 1;
        } else {
            out.push_str(&input[start..]);
            break;
        }
    }
    out
}

#[test]
fn normalizes_numeric_rdf_ids() {
    let input = "<owl:Class rdf:ID=\"1.0\"/><owl:someValuesFrom rdf:resource=\"#1.0\"/>";
    let out = normalize_invalid_rdf_ids(input);
    assert!(out.contains("rdf:ID=\"_1_0\""));
    assert!(out.contains("rdf:resource=\"#_1_0\""));
    assert!(!out.contains("1.0"));
}

#[test]
fn expands_single_quoted_entity() {
    let input = r#"<?xml version="1.0"?>
<!DOCTYPE rdf:RDF [
    <!ENTITY owl 'http://www.w3.org/2002/07/owl#'>
]>
<rdf:RDF xmlns:owl="&owl;"><owl:Ontology rdf:about=""/></rdf:RDF>"#;
    let expanded = expand_xml_entities(input, 1_000_000).expect("expand");
    assert!(!expanded.contains("<!ENTITY"));
    assert!(!expanded.contains("<!DOCTYPE"));
    assert!(expanded.contains("http://www.w3.org/2002/07/owl#"));
}

#[test]
fn expands_simple_entity() {
    let xml = r#"<!ENTITY ex "http://example.org/">"#;
    let input = format!(
        r#"<?xml version="1.0"?>
<!DOCTYPE rdf:RDF [{xml}]>
<rdf:RDF>&ex;a</rdf:RDF>"#
    );
    let expanded = expand_xml_entities(&input, 1_000_000).expect("expand");
    assert!(expanded.contains("http://example.org/"));
    assert!(!expanded.contains("&ex;"));
}

#[test]
fn expansion_limit_rejects_blowup() {
    let xml = r#"<!ENTITY a "aaaaaaaaaa"><!ENTITY b "&a;&a;">"#;
    let input = format!(
        r#"<?xml version="1.0"?>
<!DOCTYPE rdf:RDF [{xml}]>
<rdf:RDF>&b;</rdf:RDF>"#
    );
    let err = expand_xml_entities_with_limit(&input, 64).expect_err("limit");
    assert!(err.to_string().contains("exceeds limit"));
}

#[test]
fn expansion_rejects_deep_entity_nesting() {
    let mut decls = String::from("<!ENTITY e0 \"x\">\n");
    for i in 0..40 {
        decls.push_str(&format!("<!ENTITY e{} \"&e{};\">\n", i + 1, i));
    }
    let input = format!(
        r#"<?xml version="1.0"?>
<!DOCTYPE rdf:RDF [{decls}]>
<rdf:RDF>&e40;</rdf:RDF>"#
    );
    let err = expand_xml_entities_with_limit(&input, 1_000_000).expect_err("nesting");
    assert!(err.to_string().contains("nesting depth"));
}

#[test]
fn expansion_rejects_excessive_replacement_count() {
    let repeats = "&x;".repeat(10_001);
    let input = format!(
        r#"<?xml version="1.0"?>
<!DOCTYPE rdf:RDF [<!ENTITY x "a">]>
<rdf:RDF>{repeats}</rdf:RDF>"#
    );
    let err = expand_xml_entities_with_limit(&input, 50_000_000).expect_err("replacements");
    assert!(err.to_string().contains("replacement count"));
}

#[test]
fn dedupe_removes_second_rdf_id_element() {
    let input = r##"<?xml version="1.0"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:owl="http://www.w3.org/2002/07/owl#">
  <owl:Class rdf:ID="Wine"><rdfs:label>first</rdfs:label></owl:Class>
  <owl:Class rdf:ID="Wine"><owl:equivalentClass rdf:resource="#Wine"/></owl:Class>
</rdf:RDF>"##;
    let out = dedupe_rdf_xml_ids(input);
    assert_eq!(out.matches("rdf:ID=\"Wine\"").count(), 1);
    assert!(out.contains("first"));
    assert!(!out.contains("equivalentClass"));
}

#[test]
fn expand_disjoint_classes_injects_pairwise_axioms() {
    let input = r#"<rdf:RDF xmlns:owl="http://www.w3.org/2002/07/owl#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <owl:AllDisjointClasses>
    <owl:members rdf:parseType="Collection">
      <rdf:Description rdf:about="http://ex.org#c1"/>
      <rdf:Description rdf:about="http://ex.org#c2"/>
    </owl:members>
  </owl:AllDisjointClasses>
</rdf:RDF>"#;
    let out = expand_all_disjoint_collections(input);
    assert!(out.contains("owl:disjointWith"));
    assert!(!out.contains("AllDisjointClasses"));
}

#[test]
fn normalize_class_intersection_wraps_direct_intersection_of() {
    let input = r#"<rdf:RDF xmlns:owl="http://www.w3.org/2002/07/owl#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
 <owl:Class rdf:about="http://ex.org#C">
  <owl:intersectionOf rdf:parseType="Collection">
   <owl:Class rdf:about="http://ex.org#A"/>
   <owl:Class rdf:about="http://ex.org#B"/>
  </owl:intersectionOf>
 </owl:Class>
</rdf:RDF>"#;
    let out = normalize_class_intersection_definitions(input);
    assert!(out.contains("<owl:equivalentClass>"));
    assert!(!out.contains("</owl:equivalentClass>\n  <owl:intersectionOf"));
}

#[test]
fn normalize_class_same_as_to_equivalent_class() {
    let input = r#"<rdf:RDF xmlns:owl="http://www.w3.org/2002/07/owl#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xml:base="http://www.w3.org/2002/03owlt/I4.6/premises003">
 <owl:Class rdf:ID="C1">
  <owl:sameAs>
   <owl:Class rdf:ID="C2"/>
  </owl:sameAs>
 </owl:Class>
</rdf:RDF>"#;
    let out = normalize_class_same_as(input);
    assert!(out.contains("<owl:equivalentClass>"));
    assert!(out.contains("rdf:about=\"http://www.w3.org/2002/03owlt/I4.6/premises003#C2\""));
    assert!(!out.contains("owl:sameAs"));
}

#[test]
fn materialize_typed_node_element_adds_class_assertion() {
    let input = r#"<rdf:RDF xmlns:oiled="http://oiled.man.example.net/test#"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xml:base="http://ex.org/">
 <oiled:Unsatisfiable/>
</rdf:RDF>"#;
    let out = materialize_typed_node_elements(input);
    assert!(out.contains("rdf:about=\"http://ex.org#_:tn1\""));
    assert!(out.contains("rdf:resource=\"http://oiled.man.example.net/test#Unsatisfiable\""));
    assert!(!out.contains("<oiled:Unsatisfiable/>"));
}

#[test]
fn named_description_element_end_finds_outer_close() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/Datatype-2DFloat-2DDiscrete-2D001/premise.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let start = intersections
        .find("<rdf:Description rdf:about=\"a\">")
        .unwrap_or_else(|| intersections.find("<rdf:Description").unwrap());
    let end = named_description_element_end(&intersections, start).expect("end");
    let block = &intersections[start..end];
    assert!(
        block.contains("</rdf:type>"),
        "block missing type close: {block}"
    );
    assert!(block.ends_with("</rdf:Description>"));
}

#[test]
fn float_discrete_horned_emits_class_assertion() {
    use crate::Format;
    use crate::limits::ParseLimits;
    use crate::map::map_to_core;
    use crate::read::read_horned_owl_from_reader;
    use std::io::Cursor;

    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/Datatype-2DFloat-2DDiscrete-2D001/premise.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let named = materialize_named_individual_descriptions(&intersections);
    let individuals = materialize_anonymous_individual_descriptions(&named);
    assert!(
        !individuals.contains("rdf:about=\"urn:ontologos:anon:"),
        "facet collection nodes must stay blank"
    );
    let parsed = read_horned_owl_from_reader(
        &mut Cursor::new(individuals.as_bytes()),
        Format::RdfXml,
        ParseLimits::default(),
    )
    .unwrap();
    let (ont, report) = map_to_core(&parsed, ParseLimits::default()).unwrap();
    assert!(
        ont.dl().axiom_count() > 0,
        "dl axioms skipped={}",
        report.meta.skipped_axiom_count
    );
}

#[test]
fn anonymous_individual_materialization_skips_facet_collections() {
    let input = r#"<rdf:RDF xml:base="http://ex.org/"
 xmlns:owl="http://www.w3.org/2002/07/owl#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema#">
 <owl:withRestrictions rdf:parseType="Collection">
  <rdf:Description>
   <xsd:minExclusive rdf:datatype="http://www.w3.org/2001/XMLSchema#float">0.0</xsd:minExclusive>
  </rdf:Description>
 </owl:withRestrictions>
 <rdf:Description>
  <rdf:type rdf:resource="http://ex.org/C"/>
 </rdf:Description>
</rdf:RDF>"#;
    let out = materialize_anonymous_individual_descriptions(input);
    assert!(out.contains("rdf:about=\"http://ex.org#_:1\""));
    assert!(out.contains("<rdf:Description>\n   <xsd:minExclusive"));
}

#[test]
fn tagged_element_end_self_close_with_iri_attr() {
    let s = r#"<owl:assertionProperty rdf:resource="http://www.example.org#p"/>"#;
    assert!(
        tagged_element_end(s, 0, "owl:assertionProperty").is_some(),
        "self-close with IRI attr"
    );
}

#[test]
fn collect_reified_data_npa_minimal() {
    let text = r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:owl="http://www.w3.org/2002/07/owl#" xmlns:ex="http://www.example.org#">
  <rdf:Description rdf:about="http://www.example.org#z">
    <owl:sourceIndividual>
      <rdf:Description rdf:about="http://www.example.org#s">
        <ex:p>data</ex:p>
      </rdf:Description>
    </owl:sourceIndividual>
    <owl:assertionProperty rdf:resource="http://www.example.org#p"/>
    <owl:targetValue>data</owl:targetValue>
  </rdf:Description>
</rdf:RDF>"#;
    let start = text.find("<rdf:Description").unwrap();
    let end =
        element_block_end(text, start, "rdf:Description", "</rdf:Description>").expect("block end");
    let block = &text[start..end];
    let open_end = block.find('>').unwrap();
    let close_idx = block.rfind("</rdf:Description>").unwrap();
    let inner = &block[open_end + 1..close_idx];
    assert!(inner.contains("owl:targetValue"), "inner={inner}");
    assert!(
        find_top_level_element_bounds(inner, "owl:sourceIndividual").is_some(),
        "sourceIndividual missing"
    );
    assert!(
        element_text_content(inner, "owl:targetValue").is_some(),
        "targetValue missing"
    );
    let collected = collect_reified_data_npas(text);
    assert_eq!(collected.len(), 1, "{collected:?}");
}

#[test]
fn misc203_dpa_supplement_ofn_parses() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2Dmiscellaneous-2D203/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 1_000_000).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let rdfs_classes = normalize_rdfs_class_elements(&relative_uris);
    let injected = inject_rdf_based_punning_declarations(&rdfs_classes);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named_individuals = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named_individuals);
    let normalized = normalize_all_different_members(&individuals);
    let disjoint = expand_all_disjoint_collections(&normalized);
    let collected = collect_direct_data_literal_assertions(&disjoint);
    let dpa = &collected[0];
    let (lexical, datatype_iri) = if dpa.value_literal.contains("^^") {
        let mut parts = dpa.value_literal.splitn(2, "^^");
        let lex = parts.next().unwrap().trim_matches('"').to_string();
        let dt = parts.next().unwrap().trim_matches(|c| c == '<' || c == '>');
        (lex, dt.to_string())
    } else {
        panic!("unexpected literal");
    };
    let (extra_prefixes, lit, dt_decl) =
        qualify_typed_literal_for_supplement(&lexical, &datatype_iri);
    let dt_decl_line = dt_decl.map(|d| format!("\n       {d}")).unwrap_or_default();
    let body = format!(
        "Declaration(NamedIndividual(<{}>))\n\
             Declaration(DataProperty(<{}>))\n\
             ClassAssertion(owl:Thing <{}>){dt_decl_line}\n\
             DataPropertyAssertion(<{}> <{}> {lit})",
        dpa.subject, dpa.property, dpa.subject, dpa.property, dpa.subject
    );
    let ofn = format!(
        "Prefix(owl:=<http://www.w3.org/2002/07/owl#>)\n\
             Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)\n\
             Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)\n\
             {extra_prefixes}\n\
             Ontology(<http://example.org/thing-data-literal-supplement>\n{body}\n)"
    );
    eprintln!("{ofn}");
    let ont = crate::load_ofn_from_str_with_limits(&ofn, crate::ParseLimits::default())
        .expect("supplement ofn");
    assert!(
        ont.dl()
            .axioms()
            .any(|a| { matches!(a, ontologos_core::DlAxiom::DataPropertyAssertion { .. }) })
    );
}

#[test]
fn misc203_full_load_has_data_assertions() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2Dmiscellaneous-2D203/premise.rdf",
    );
    let ont = crate::load_ontology(&path).expect("load misc203");
    let dpas: Vec<_> = ont
        .dl()
        .axioms()
        .filter(|a| matches!(a, ontologos_core::DlAxiom::DataPropertyAssertion { .. }))
        .collect();
    eprintln!("dpas={dpas:?}");
    assert_eq!(
        dpas.len(),
        2,
        "all axioms: {:?}",
        ont.dl().axioms().collect::<Vec<_>>()
    );
}

#[test]
fn collect_direct_data_literals_from_misc203_after_full_preprocess() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2Dmiscellaneous-2D203/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 1_000_000).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let rdfs_classes = normalize_rdfs_class_elements(&relative_uris);
    let injected = inject_rdf_based_punning_declarations(&rdfs_classes);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named_individuals = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named_individuals);
    let normalized = normalize_all_different_members(&individuals);
    let disjoint = expand_all_disjoint_collections(&normalized);
    let collected = collect_direct_data_literal_assertions(&disjoint);
    eprintln!("after full preprocess collected={collected:?}");
    assert_eq!(collected.len(), 2, "{collected:?}");
}

#[test]
fn collect_direct_data_literals_from_misc203() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2Dmiscellaneous-2D203/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 1_000_000).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let injected = inject_rdf_based_punning_declarations(&relative_uris);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let collected = collect_direct_data_literal_assertions(&typed_nodes);
    eprintln!("collected={collected:?}");
    assert_eq!(collected.len(), 2, "{collected:?}");
}

#[test]
fn xml_literal_data_property_supplement_loads() {
    let ofn = "Prefix(owl:=<http://www.w3.org/2002/07/owl#>)\n\
             Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)\n\
             Ontology(<http://example.org/xml-lit-test>\n\
               Declaration(NamedIndividual(<http://example.org/i>))\n\
               Declaration(DataProperty(<http://example.org/p>))\n\
               DataPropertyAssertion(<http://example.org/p> <http://example.org/i> \"<b>x</b>\"^^rdf:XMLLiteral)\n\
             )";
    let ont = crate::load_ofn_from_str_with_limits(ofn, crate::ParseLimits::default())
        .expect("load xml literal dpa");
    let has_dpa = ont
        .dl()
        .axioms()
        .any(|a| matches!(a, ontologos_core::DlAxiom::DataPropertyAssertion { .. }));
    assert!(
        has_dpa,
        "axioms: {:?}",
        ont.dl().axioms().collect::<Vec<_>>()
    );
}

#[test]
fn collect_reified_data_npa_from_fixture() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Dnpa-2Ddat-2Dfw/premise.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let collected = collect_reified_data_npas(&text);
    assert_eq!(collected.len(), 1, "{collected:?}");
    assert_eq!(collected[0].value_literal, "data");
}

#[test]
fn named_description_skips_inverse_functional_property_typing() {
    let input = r#"<rdf:RDF xml:base="http://ex.org/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
 <rdf:Description rdf:about="http://ex.org/p">
  <rdf:type rdf:resource="http://www.w3.org/2002/07/owl#DatatypeProperty"/>
 </rdf:Description>
</rdf:RDF>"#;
    let out = materialize_named_individual_descriptions(input);
    assert!(out.contains("<rdf:Description rdf:about=\"http://ex.org/p\">"));
    assert!(!out.contains("NamedIndividual"));
}

#[test]
fn manual_rewrite_once_on_float() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/Datatype-2DFloat-2DDiscrete-2D001/premise.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let start = intersections
        .find("<rdf:Description rdf:about=\"a\">")
        .unwrap();
    let end = named_description_element_end(&intersections, start).unwrap();
    let block = &intersections[start..end];
    assert!(
        block.ends_with("</rdf:Description>"),
        "block end: {:?}",
        &block[block.len().saturating_sub(40)..]
    );
    let rewritten = rewrite_description_to_named_individual(block);
    assert!(
        !rewritten.contains("rdf:ty</owl:NamedIndividual>"),
        "{rewritten}"
    );
}

#[test]
fn float_discrete_preprocess_produces_valid_xml() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/Datatype-2DFloat-2DDiscrete-2D001/premise.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let named = materialize_named_individual_descriptions(&intersections);
    if named.contains("rdf:ty</owl:NamedIndividual>") {
        panic!("bad output:\n{named}");
    }
    assert!(named.contains("<owl:NamedIndividual"));
}

#[test]
fn materialize_named_description_to_individual() {
    let input = r#"<rdf:RDF xml:base="http://ex.org/ontology/"
 xmlns:owl="http://www.w3.org/2002/07/owl#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
 <rdf:Description rdf:about="a">
  <rdf:type>
   <owl:Restriction>
    <owl:onProperty rdf:resource="dp"/>
   </owl:Restriction>
  </rdf:type>
 </rdf:Description>
</rdf:RDF>"#;
    let out = materialize_named_individual_descriptions(input);
    assert!(out.contains("<owl:NamedIndividual"));
    assert!(!out.contains("<rdf:Description rdf:about=\"a\">"));
}

#[test]
fn horned_parses_object_restriction_via_equivalent_class() {
    use crate::Format;
    use crate::limits::ParseLimits;
    use crate::read::read_horned_owl_from_reader;
    use std::io::Cursor;

    let mini = r#"<?xml version="1.0"?>
<rdf:RDF xmlns:owl="http://www.w3.org/2002/07/owl#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xml:base="http://example.org/">
<owl:Ontology/>
<owl:Class rdf:about="probe">
  <owl:equivalentClass>
    <owl:Restriction>
      <owl:onProperty rdf:resource="http://www.w3.org/2002/07/owl#bottomObjectProperty" />
      <owl:someValuesFrom rdf:resource="http://www.w3.org/2002/07/owl#Thing" />
    </owl:Restriction>
  </owl:equivalentClass>
</owl:Class>
</rdf:RDF>"#;
    let parsed = read_horned_owl_from_reader(
        &mut Cursor::new(mini.as_bytes()),
        Format::RdfXml,
        ParseLimits::default(),
    )
    .unwrap();
    for annotated in parsed.iter() {
        eprintln!("{:?}", annotated.component);
    }
}

#[test]
fn horned_components_bottom_vs_float() {
    use crate::Format;
    use crate::limits::ParseLimits;
    use crate::read::read_horned_owl_from_reader;
    use std::io::Cursor;

    for case in [
        "New-2DFeature-2DBottomObjectProperty-2D001",
        "Datatype-2DFloat-2DDiscrete-2D001",
    ] {
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../benchmarks/data/hermit/wg")
            .join(case)
            .join("premise.rdf");
        let text = std::fs::read_to_string(&path).unwrap();
        let deduped = dedupe_rdf_xml_ids(&text);
        let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
        let injected = inject_rdf_based_punning_declarations(&expanded);
        let typed = materialize_typed_node_elements(&injected);
        let intersections = normalize_class_intersection_definitions(&typed);
        let same_as = normalize_class_same_as(&intersections);
        let named = materialize_named_individual_descriptions(&same_as);
        let individuals = materialize_anonymous_individual_descriptions(&named);
        let parsed = read_horned_owl_from_reader(
            &mut Cursor::new(individuals.as_bytes()),
            Format::RdfXml,
            ParseLimits::default(),
        )
        .unwrap();
        eprintln!("\n=== {case} ===");
        for annotated in parsed.iter() {
            eprintln!("  {:?}", annotated.component);
        }
    }
}

#[test]
fn restriction_006_union_ofn_debug() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DRestriction-2D006/conclusion.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    for (name, stage) in [
        ("raw", text.as_str()),
        ("deduped", deduped.as_str()),
        ("expanded", expanded.as_str()),
        ("injected", injected.as_str()),
        ("typed", typed.as_str()),
        ("intersections", intersections.as_str()),
        ("same_as", same_as.as_str()),
        ("named", named.as_str()),
    ] {
        let has_restriction = stage.contains("<owl:Restriction");
        let has_union_child = stage.contains("unionOf") && stage.contains("<owl:Class");
        eprintln!("{name}: restriction={has_restriction} union_child={has_union_child}");
    }
    let base = parse_xml_base(&text);
    let dt_props = declared_datatype_property_iris(&text);
    let node_lists = build_rdf_collection_node_map(&text, &base, &dt_props);
    let mut pos = 0usize;
    let mut ce_block = String::new();
    while pos < named.len() {
        let next_desc = named[pos..].find("<rdf:Description");
        let next_ind = named[pos..].find("<owl:NamedIndividual");
        let next = match (next_desc, next_ind) {
            (Some(d), Some(i)) => Some(pos + d.min(i)),
            (Some(d), None) => Some(pos + d),
            (None, Some(i)) => Some(pos + i),
            (None, None) => None,
        };
        let Some(start) = next else { break };
        if named[start..].starts_with("</") {
            pos = start + 1;
            continue;
        }
        let (open_tag, close_tag) = if named[start..].starts_with("<owl:NamedIndividual") {
            ("owl:NamedIndividual", "</owl:NamedIndividual>")
        } else {
            ("rdf:Description", "</rdf:Description>")
        };
        let Some(end) = element_block_end(&named, start, open_tag, close_tag) else {
            break;
        };
        let block = &named[start..end];
        if block.contains("premises006#x") {
            let open_end = block.find('>').unwrap();
            let close_start = block.rfind(close_tag).unwrap();
            let inner = &block[open_end + 1..close_start];
            if let Some((_, _, type_block)) = find_top_level_element_bounds(inner, "rdf:type") {
                ce_block =
                    unwrap_typed_class_expression_block(&element_inner(type_block, "rdf:type"));
            }
            break;
        }
        pos = end;
    }
    let union_ofn = boolean_operator_ofn(
        ce_block.trim(),
        "owl:unionOf",
        "ObjectUnionOf",
        &base,
        &dt_props,
        &node_lists,
    );
    assert!(
        union_ofn
            .as_deref()
            .is_some_and(|u| u.contains("ObjectUnionOf")),
        "union_ofn={union_ofn:?}"
    );
}

#[test]
fn restriction_006_conclusion_emits_union_class_assertion() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DRestriction-2D006/conclusion.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let collected = collect_object_class_assertions(&named);
    assert!(
        collected.iter().any(|(_, ce)| ce.contains("ObjectUnionOf")),
        "{collected:?}"
    );
}

#[test]
fn i5_1_premise_loads() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DI5.1-2D001/premise.rdf");
    load_ontology(&path).expect("load I5.1 premise");
}

#[test]
fn transitive_property_conclusion_emits_class_assertion() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
            "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DTransitiveProperty-2D002/conclusion.rdf",
        );
    let ont = load_ontology(&path).expect("load");
    assert!(
        ont.dl()
            .axioms()
            .any(|a| matches!(a, ontologos_core::DlAxiom::ClassAssertion { .. })),
        "expected DL ClassAssertion"
    );
}

#[test]
fn collect_object_class_assertion_restriction_with_nested_class_type() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
            "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DTransitiveProperty-2D002/conclusion.rdf",
        );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let collected = collect_object_class_assertions(&named);
    assert_eq!(collected.len(), 1, "{collected:?}");
    assert!(collected[0].1.contains("ObjectSomeValuesFrom"));
}

#[test]
fn collect_object_class_assertion_bottom_property() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/New-2DFeature-2DBottomObjectProperty-2D001/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    let intersections = normalize_class_intersection_definitions(&typed);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let collected = collect_object_class_assertions(&named);
    assert_eq!(collected.len(), 1);
    assert!(collected[0].1.contains("bottomObjectProperty"));
}

#[test]
fn rational003_oneof_has_two_literals() {
    let avf = r#"<owl:Restriction>
      <owl:onProperty rdf:resource="dp" />
      <owl:allValuesFrom>
        <rdfs:Datatype>
          <owl:oneOf>
            <rdf:Description>
              <rdf:first rdf:datatype="http://www.w3.org/2001/XMLSchema#decimal">0.3333333333333333</rdf:first>
              <rdf:rest>
                <rdf:Description>
                  <rdf:first rdf:datatype="http://www.w3.org/2002/07/owl#rational">1/3</rdf:first>
                  <rdf:rest rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#"/>
                </rdf:Description>
              </rdf:rest>
            </rdf:Description>
          </owl:oneOf>
        </rdfs:Datatype>
      </owl:allValuesFrom>
    </owl:Restriction>"#;
    let base = "http://example.org/";
    let mut dt = std::collections::HashSet::new();
    dt.insert("http://example.org/dp".to_string());
    let ofn = data_restriction_ce_to_ofn(avf, base, &dt).expect("ofn");
    assert!(ofn.contains("DataOneOf"), "{ofn}");
    assert!(ofn.contains("0.3333333333333333"), "{ofn}");
    assert!(ofn.contains("1/3"), "{ofn}");

    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/New-2DFeature-2DRational-2D003/premise.rdf");
    let rdf = std::fs::read_to_string(&path).expect("read");
    let deduped = dedupe_rdf_xml_ids(&rdf);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let relative_uris = normalize_relative_owl_uris(&normalized_ids);
    let injected = inject_rdf_based_punning_declarations(&relative_uris);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let collected = collect_object_class_assertions(&named);
    assert_eq!(collected.len(), 2, "{collected:?}");
    assert!(
        collected
            .iter()
            .any(|(_, ce)| ce.contains("DataOneOf") && ce.contains("1/3")),
        "{collected:?}"
    );
    assert!(
        collected
            .iter()
            .any(|(_, ce)| ce.contains("DataMinCardinality")),
        "{collected:?}"
    );
}

#[test]
fn anonymous_restriction_subclass_axiom_ofn() {
    let rdf = r#"<rdf:RDF xml:base="file:/c:/temp/test.owl"
 xmlns:owl="http://www.w3.org/2002/07/owl#"
 xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<owl:Class rdf:about="file:/c:/temp/test.owl#d"/>
<owl:ObjectProperty rdf:about="file:/c:/temp/test.owl#r"/>
<owl:ObjectProperty rdf:about="file:/c:/temp/test.owl#s"/>
<owl:Class rdf:about="file:/c:/temp/test.owl#c"/>
<owl:Restriction>
  <rdfs:subClassOf rdf:resource="file:/c:/temp/test.owl#d"/>
  <owl:onProperty rdf:resource="file:/c:/temp/test.owl#r"/>
  <owl:someValuesFrom>
    <owl:Restriction>
      <owl:onProperty rdf:resource="file:/c:/temp/test.owl#s"/>
      <owl:someValuesFrom rdf:resource="file:/c:/temp/test.owl#c"/>
    </owl:Restriction>
  </owl:someValuesFrom>
</owl:Restriction>
</rdf:RDF>"#;
    let axioms = collect_anonymous_restriction_subclass_axioms(rdf);
    assert_eq!(axioms.len(), 1);
    assert!(axioms[0].contains("SubClassOf("));
    assert!(axioms[0].contains("ObjectSomeValuesFrom"));
    assert!(axioms[0].contains("file:/c:/temp/test.owl#d"));
}

#[test]
fn rational002_allvaluesfrom_oneof_ofn() {
    let avf = r#"<owl:Restriction>
      <owl:onProperty rdf:resource="dp" />
      <owl:allValuesFrom>
        <rdfs:Datatype>
          <owl:oneOf>
            <rdf:Description>
              <rdf:first rdf:datatype="http://www.w3.org/2001/XMLSchema#decimal">0.5</rdf:first>
              <rdf:rest>
                <rdf:Description>
                  <rdf:first rdf:datatype="http://www.w3.org/2002/07/owl#rational">1/2</rdf:first>
                  <rdf:rest rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#"/>
                </rdf:Description>
              </rdf:rest>
            </rdf:Description>
          </owl:oneOf>
        </rdfs:Datatype>
      </owl:allValuesFrom>
    </owl:Restriction>"#;
    let base = "http://example.org/";
    let mut dt = std::collections::HashSet::new();
    dt.insert("http://example.org/dp".to_string());
    let ofn = data_restriction_ce_to_ofn(avf, base, &dt);
    assert!(ofn.is_some_and(|s| s.contains("DataAllValuesFrom")));
}

#[test]
fn minimal_subclass_restriction_harvest_preprocessed() {
    let rdf = std::fs::read_to_string(
        std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/minimal_subclass.rdf"),
    )
    .expect("read");
    let deduped = dedupe_rdf_xml_ids(&rdf);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 50 * 1024 * 1024).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let rdfs_classes = normalize_rdfs_class_elements(&relative_uris);
    let injected = inject_rdf_based_punning_declarations(&rdfs_classes);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named);
    let normalized = normalize_all_different_members(&individuals);
    let disjoint = expand_all_disjoint_collections(&normalized);
    let property_usage = inject_object_property_declarations_from_usage(&disjoint);
    let preprocessed = normalize_property_same_as(&property_usage);
    let collected = collect_restriction_subclasses(&preprocessed);
    assert!(
        collected
            .iter()
            .any(|(_, ce)| ce.contains("ObjectSomeValuesFrom")),
        "expected someValuesFrom supplement on preprocessed RDF, got {collected:?}"
    );
}

#[test]
fn nominals2_max_cardinality_restriction_ofn() {
    let block = r#"<owl:Restriction>
                        <owl:onProperty rdf:resource="file:/c:/temp/test.owl#r"/>
                        <owl:onClass rdf:resource="file:/c:/temp/test.owl#d"/>
                        <owl:maxCardinality rdf:datatype="http://www.w3.org/2001/XMLSchema#nonNegativeInteger">2</owl:maxCardinality>
                    </owl:Restriction>"#;
    let base = "file:/c:/temp/test.owl";
    let dt = std::collections::HashSet::new();
    let ofn = restriction_ce_to_ofn(block.trim(), base, &dt);
    assert!(ofn.is_some_and(|s| s.contains("ObjectMaxCardinality")));
}

#[test]
fn nominals2_union_subclass_ofn_direct() {
    let sub_inner = r#"<owl:Class>
                <owl:unionOf rdf:parseType="Collection">
                    <rdf:Description rdf:about="file:/c:/temp/test.owl#e"/>
                    <owl:Restriction>
                        <owl:onProperty rdf:resource="file:/c:/temp/test.owl#r"/>
                        <owl:onClass rdf:resource="file:/c:/temp/test.owl#d"/>
                        <owl:maxCardinality rdf:datatype="http://www.w3.org/2001/XMLSchema#nonNegativeInteger">2</owl:maxCardinality>
                    </owl:Restriction>
                </owl:unionOf>
            </owl:Class>"#;
    let base = "file:/c:/temp/test.owl";
    let dt = std::collections::HashSet::new();
    let nl = std::collections::HashMap::new();
    let ofn = superclass_ce_ofn_from_subclass_inner(sub_inner.trim(), base, &dt, &nl);
    assert!(
        ofn.as_ref()
            .is_some_and(|s| s.contains("ObjectUnionOf") && s.contains("ObjectMaxCardinality")),
        "got {ofn:?}"
    );
}

#[test]
fn nominals2_union_subclass_loads() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/structural/nominals-2-input.xml");
    let rdf = std::fs::read_to_string(&path).expect("read");
    let deduped = dedupe_rdf_xml_ids(&rdf);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 50 * 1024 * 1024).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let rdfs_classes = normalize_rdfs_class_elements(&relative_uris);
    let injected = inject_rdf_based_punning_declarations(&rdfs_classes);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named);
    let normalized = normalize_all_different_members(&individuals);
    let disjoint = expand_all_disjoint_collections(&normalized);
    let property_usage = inject_object_property_declarations_from_usage(&disjoint);
    let preprocessed = normalize_property_same_as(&property_usage);
    let collected = collect_restriction_subclasses(&preprocessed);
    assert!(
        collected
            .iter()
            .any(|(_, ce)| ce.contains("ObjectUnionOf") && ce.contains("ObjectMaxCardinality")),
        "expected union supplement, got {collected:?}"
    );
}

#[test]
fn data_restriction_min_cardinality_ofn() {
    let ce = r#"<owl:Restriction>
    <owl:onProperty rdf:resource="dp"/>
    <owl:minCardinality>2</owl:minCardinality>
  </owl:Restriction>"#;
    let base = "http://example.org/";
    let mut dt = std::collections::HashSet::new();
    dt.insert("http://example.org/dp".to_string());
    let ofn = data_restriction_ce_to_ofn(ce, base, &dt);
    assert_eq!(
        ofn.as_deref(),
        Some("DataMinCardinality(2 <http://example.org/dp>)")
    );
}

#[test]
fn rational002_collects_both_data_restrictions() {
    let inline_min = r#"<rdf:RDF xml:base="http://example.org/"
 xmlns:owl="http://www.w3.org/2002/07/owl#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<owl:DatatypeProperty rdf:about="dp"/>
<rdf:Description rdf:about="a">
  <owl:onProperty rdf:resource="dp"/>
  <owl:minCardinality>2</owl:minCardinality>
</rdf:Description>
</rdf:RDF>"#;
    let inline = collect_object_class_assertions(inline_min);
    assert_eq!(inline.len(), 1, "inline={inline:?}");

    let typed_min = r#"<rdf:RDF xml:base="http://example.org/"
 xmlns:owl="http://www.w3.org/2002/07/owl#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<owl:DatatypeProperty rdf:about="dp"/>
<rdf:Description rdf:about="a">
  <rdf:type><owl:Restriction>
    <owl:onProperty rdf:resource="dp"/>
    <owl:minCardinality>2</owl:minCardinality>
  </owl:Restriction></rdf:type>
</rdf:Description>
</rdf:RDF>"#;
    let typed = collect_object_class_assertions(typed_min);
    assert_eq!(typed.len(), 1, "typed={typed:?}");

    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/New-2DFeature-2DRational-2D002/premise.rdf");
    let rdf = std::fs::read_to_string(&path).expect("read");
    let deduped = dedupe_rdf_xml_ids(&rdf);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let relative_uris = normalize_relative_owl_uris(&normalized_ids);
    let injected = inject_rdf_based_punning_declarations(&relative_uris);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let collected = collect_object_class_assertions(&named);
    assert_eq!(collected.len(), 2, "{collected:?}");
    assert!(
        collected
            .iter()
            .any(|(_, ce)| ce.contains("DataAllValuesFrom")),
        "{collected:?}"
    );
    assert!(
        collected
            .iter()
            .any(|(_, ce)| ce.contains("DataMinCardinality")),
        "{collected:?}"
    );
}

#[test]
fn rational002_full_load_parses() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/New-2DFeature-2DRational-2D002/premise.rdf");
    let ont = load_ontology(&path).expect("load rational002");
    assert!(ont.dl().axiom_count() > 0);
}

#[test]
fn bottom_property_emits_class_assertion() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/New-2DFeature-2DBottomObjectProperty-2D001/premise.rdf",
    );
    let ont = load_ontology(&path).expect("load");
    assert!(ont.dl().axiom_count() > 0);
}

#[test]
fn materialize_named_description_skips_thing_subclass() {
    let input = r#"<rdf:RDF xmlns:owl="http://www.w3.org/2002/07/owl#"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
 <rdf:Description rdf:about="http://www.w3.org/2002/07/owl#Thing">
  <rdfs:subClassOf>
   <owl:Restriction>
    <owl:onProperty rdf:resource="http://ex.org#f"/>
   </owl:Restriction>
  </rdfs:subClassOf>
 </rdf:Description>
</rdf:RDF>"#;
    let out = materialize_named_individual_descriptions(input);
    assert!(out.contains("<rdf:Description"));
    assert!(!out.contains("<owl:NamedIndividual"));
}

#[test]
fn materialize_anonymous_description_adds_about() {
    let input = r#"<rdf:RDF xml:base="http://ex.org/">
  <rdf:Description>
    <rdf:type rdf:resource="http://ex.org#C"/>
  </rdf:Description>
</rdf:RDF>"#;
    let out = materialize_anonymous_individual_descriptions(input);
    assert!(out.contains("rdf:about=\"http://ex.org#_:1\""));
}

#[test]
fn description_element_end_parses_member_descriptions() {
    let inner = r#"
      <rdf:Description rdf:about="http://ex.org#w1">
        <owl:sameAs rdf:resource="http://ex.org#w2"/>
      </rdf:Description>
      <rdf:Description rdf:about="http://ex.org#w2"/>
    "#;
    let start = inner.find("<rdf:Description").unwrap();
    let end = super::description_element_end(inner, start).expect("first desc end");
    assert!(inner[start..end].contains("sameAs"));
    let start2 = inner[end..].find("<rdf:Description").unwrap() + end;
    let end2 = super::description_element_end(inner, start2).expect("second desc end");
    assert!(inner[start2..end2].contains("w2"));
}

#[test]
fn normalize_all_different_members_tag() {
    let input = r#"<rdf:RDF xmlns:owl="http://www.w3.org/2002/07/owl#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <owl:AllDifferent rdf:about="http://ex.org#z">
    <owl:members rdf:parseType="Collection">
      <rdf:Description rdf:about="http://ex.org#w1">
        <owl:sameAs rdf:resource="http://ex.org#w2"/>
      </rdf:Description>
      <rdf:Description rdf:about="http://ex.org#w2"/>
    </owl:members>
  </owl:AllDifferent>
</rdf:RDF>"#;
    let out = normalize_all_different_members(input);
    assert!(out.contains("owl:distinctMembers"));
    assert!(out.contains("<owl:NamedIndividual rdf:about=\"http://ex.org#w1\"/>"));
    assert!(!out.contains("AllDifferent rdf:about"));
    assert!(out.contains("owl:sameAs"));
}

#[test]
fn injects_nested_class_declarations_for_subst_fixture() {
    let input = include_str!(
        "../../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Deqdis-2Deqclass-2Dsubst/premise.rdf"
    );
    let out = inject_rdf_based_punning_declarations(input);
    assert!(out.contains("<owl:Class rdf:about=\"http://www.example.org#c1\"/>"));
    assert!(out.contains("<owl:Class rdf:about=\"http://www.example.org#d1\"/>"));
}

#[test]
fn injects_class_declarations_for_rdf_based_equivalent_class() {
    let input = include_str!(
        "../../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Deqdis-2Deqclass-2Dinst/premise.rdf"
    );
    let out = inject_rdf_based_punning_declarations(input);
    assert!(out.contains("<owl:Class rdf:about=\"http://www.example.org#c1\"/>"));
    assert!(out.contains("<owl:Class rdf:about=\"http://www.example.org#c2\"/>"));
}

#[test]
fn injects_property_declarations_for_rdf_based_equivalent_property() {
    let input = include_str!(
        "../../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Deqdis-2Deqprop-2Dinst/premise.rdf"
    );
    let out = inject_rdf_based_punning_declarations(input);
    assert!(out.contains("<owl:ObjectProperty rdf:about=\"http://www.example.org#p1\"/>"));
    assert!(out.contains("<owl:ObjectProperty rdf:about=\"http://www.example.org#p2\"/>"));
}

#[test]
fn injects_datatype_property_for_rdf_based_rflxv_conclusion() {
    let input = include_str!(
        "../../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Deqdis-2Deqprop-2Drflxv/conclusion.rdf"
    );
    let out = inject_rdf_based_punning_declarations(input);
    assert!(out.contains("<owl:DatatypeProperty rdf:about=\"http://www.example.org#dp\"/>"));
    assert!(out.contains("<owl:ObjectProperty rdf:about=\"http://www.example.org#op\"/>"));
}

#[test]
fn injects_datatype_peer_for_equivalent_property_target() {
    let input = r#"<rdf:RDF xml:base="http://example.com/owl/families/"
  xmlns="http://example.com/owl/families/"
  xmlns:owl="http://www.w3.org/2002/07/owl#"
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <owl:DatatypeProperty rdf:about="hasAge">
    <owl:equivalentProperty rdf:resource="http://example.org/otherOntologies/families/age"/>
  </owl:DatatypeProperty>
</rdf:RDF>"#;
    let out = inject_rdf_based_punning_declarations(input);
    assert!(
        out.contains(
            "<owl:DatatypeProperty rdf:about=\"http://example.org/otherOntologies/families/age\"/>"
        ),
        "expected imported age peer as datatype property, got:\n{out}"
    );
    assert!(
        !out.contains(
            "<owl:ObjectProperty rdf:about=\"http://example.org/otherOntologies/families/age\"/>"
        ),
        "must not inject object property for datatype equivalent peer:\n{out}"
    );
}

#[test]
fn wine_fixture_has_no_duplicate_ids_after_dedupe() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/reasoner/res/wine.xml");
    let input = std::fs::read_to_string(&path).expect("wine.xml");
    let deduped = dedupe_rdf_xml_ids(&input);
    let without_comments = strip_xml_comments(&deduped);
    let mut seen = HashSet::new();
    let mut dupes = Vec::new();
    for line in without_comments.lines() {
        if let Some(idx) = line.find("rdf:ID=\"") {
            let rest = &line[idx + 8..];
            if let Some(end) = rest.find('"') {
                let id = &rest[..end];
                if !seen.insert(id.to_owned()) {
                    dupes.push(id.to_owned());
                }
            }
        }
    }
    assert!(dupes.is_empty(), "duplicate ids remain: {dupes:?}");
}

#[test]
fn functional_property_004_conclusion_loads_functional_axiom() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
            "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DFunctionalProperty-2D004/conclusion.rdf",
        );
    let ont = load_ontology(&path).expect("load");
    let has_functional = ont
        .axioms()
        .iter()
        .any(|(_, ax)| matches!(ax, ontologos_core::Axiom::FunctionalObjectProperty(_)));
    eprintln!(
        "conclusion axioms: {:?}",
        ont.axioms().iter().collect::<Vec<_>>()
    );
    eprintln!("conclusion dl: {:?}", ont.dl().axioms().collect::<Vec<_>>());
    assert!(
        has_functional,
        "expected FunctionalObjectProperty in conclusion"
    );
}

#[test]
fn functional_property_004_oneof_supplement() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DFunctionalProperty-2D004/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let root = normalize_multiline_rdf_root_tag(&text);
    let deduped = dedupe_rdf_xml_ids(&root);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 10_000_000).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let rdfs_classes = normalize_rdfs_class_elements(&relative_uris);
    let injected = inject_rdf_based_punning_declarations(&rdfs_classes);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named);
    let normalized = normalize_all_different_members(&individuals);
    let preprocessed = expand_all_disjoint_collections(&normalized);
    let equivs = collect_boolean_class_equivalences(&preprocessed);
    assert!(
        equivs.iter().any(|(_, ce)| ce.contains("ObjectOneOf")),
        "expected singleton oneOf equivalence, got {equivs:?}\npreprocessed:\n{preprocessed}"
    );
    let ont = load_ontology(&path).expect("load");
    assert!(
        ont.dl().axiom_count() > 0,
        "dl axioms: {:?}",
        ont.dl().axioms().collect::<Vec<_>>()
    );
}

#[test]
fn normalize_multiline_rdf_root_tag_collapses_functional_property_fixture() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DFunctionalProperty-2D004/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let collapsed = normalize_multiline_rdf_root_tag(&text);
    assert!(
        !collapsed.contains("\n  xmlns:owl"),
        "root tag should be collapsed: {}",
        &collapsed[..collapsed.find('>').unwrap_or(collapsed.len())]
    );
    let loaded = crate::load_ontology(&path).expect("load functional property premise");
    assert!(loaded.axiom_count() > 0 || loaded.dl().axiom_count() > 0);
}

#[test]
fn parse_xmlns_multiline_root_with_trailing_angle() {
    let input = r#"<rdf:RDF 
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:oiled="http://oiled.man.example.net/test#">
 <oiled:Unsatisfiable/>
</rdf:RDF>"#;
    let xmlns = parse_xmlns(input);
    assert_eq!(
        xmlns.get("oiled"),
        Some(&"http://oiled.man.example.net/test#".to_owned())
    );
    let out = materialize_typed_node_elements(input);
    assert!(out.contains("#_:tn1"));
    assert!(!out.contains("<oiled:Unsatisfiable/>"));
}

#[test]
fn dl035_materialize_typed_node_on_fixture() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2Ddescription-2Dlogic-2D035/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let out = materialize_typed_node_elements(&expanded);
    assert!(out.contains("#_:tn1"));
    assert!(!out.contains("<oiled:Unsatisfiable/>"));
}

#[test]
fn dl035_preprocess_retains_typed_node_and_spy_individual() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2Ddescription-2Dlogic-2D035/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let injected = inject_rdf_based_punning_declarations(&expanded);
    let typed = materialize_typed_node_elements(&injected);
    assert!(
        typed.contains("#_:tn1"),
        "typed node not materialized:\n{typed}"
    );
    assert!(!typed.contains("<oiled:Unsatisfiable/>"));
    let intersections = normalize_class_intersection_definitions(&typed);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named);
    assert!(individuals.contains("test#spy"));
    assert!(individuals.contains("owl:NamedIndividual"));
}

#[test]
fn equivalent_class_007_binary_equivalence_ofn() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DequivalentClass-2D007/conclusion.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let expanded = expand_xml_entities_with_limit(&deduped, 1_000_000).unwrap();
    let relative = normalize_relative_owl_uris(&expanded);
    let rdfs = normalize_rdfs_class_elements(&relative);
    let injected = inject_rdf_based_punning_declarations(&rdfs);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named);
    let normalized = normalize_all_different_members(&individuals);
    let disjoint = expand_all_disjoint_collections(&normalized);
    let preprocessed = normalize_property_same_as(&disjoint);
    let pairs = collect_boolean_binary_equivalences(&preprocessed);
    assert_eq!(pairs.len(), 1, "pairs={pairs:?}");
    let left = &pairs[0].0;
    let right = &pairs[0].1;
    assert!(left.contains("ObjectComplementOf"), "left={left}");
    assert!(right.contains("ObjectComplementOf"), "right={right}");
    assert!(left.contains("ObjectIntersectionOf"), "left={left}");
    assert!(right.contains("ObjectUnionOf"), "right={right}");
    let conc = load_ontology(&path).unwrap();
    assert!(
        conc.dl()
            .axioms()
            .any(|a| matches!(a, ontologos_core::DlAxiom::EquivalentClasses(_))),
        "expected EquivalentClasses in DL"
    );
}

#[test]
fn wg_nothing_001_loads_class_assertion() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DNothing-2D001/premise.rdf");
    let ont = load_ontology(&path).unwrap();
    assert!(
        ont.dl().axiom_count() > 0,
        "expected ClassAssertion for owl:Nothing typed node"
    );
}

#[test]
fn intersection_of_001_supplementation() {
    use crate::load_ontology;
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DintersectionOf-2D001/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let equivs = collect_boolean_class_equivalences(&text);
    assert!(
        equivs.len() >= 2,
        "expected boolean equivalences, got {equivs:?}"
    );
    let ont = load_ontology(&path).expect("load");
    assert!(
        ont.dl().axiom_count() >= 3,
        "axiom_count={}",
        ont.dl().axiom_count()
    );
}

#[test]
fn collect_rdfs_domain_from_domain_cond() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Drdfs-2Ddomain-2Dcond/premise.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 10_000_000).expect("expand");
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let injected = inject_rdf_based_punning_declarations(&relative_uris);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let anon = materialize_anonymous_individual_descriptions(&named);
    let normalized = normalize_all_different_members(&anon);
    let final_out = expand_all_disjoint_collections(&normalized);
    let domains = collect_rdfs_object_property_domains(&final_out);
    assert!(
        !domains.is_empty(),
        "expected rdfs:domain after full pipeline, got {domains:?}\nfinal:\n{final_out}"
    );
}

#[test]
fn wg_somevalues_inst_subj_preprocess_no_nested_description() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
            "../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Drestrict-2Dsomevalues-2Dinst-2Dsubj/premise.rdf",
        );
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 10_000_000).expect("expand");
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let injected = inject_rdf_based_punning_declarations(&relative_uris);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let anon = materialize_anonymous_individual_descriptions(&named);
    let normalized = normalize_all_different_members(&anon);
    let final_out = expand_all_disjoint_collections(&normalized);
    for (label, out) in [
        ("injected", &injected),
        ("typed_about", &typed_about),
        ("typed_nodes", &typed_nodes),
        ("intersections", &intersections),
        ("same_as", &same_as),
        ("named", &named),
        ("anon", &anon),
        ("normalized", &normalized),
        ("final", &final_out),
    ] {
        if out.matches("<rdf:Description").count() > 2 {
            eprintln!(
                "=== {label} ({} descriptions) ===\n{out}\n",
                out.matches("<rdf:Description").count()
            );
        }
    }
    use crate::Format;
    use crate::limits::ParseLimits;
    use crate::read::read_horned_owl_from_reader;
    read_horned_owl_from_reader(
        &mut std::io::Cursor::new(final_out.into_bytes()),
        Format::RdfXml,
        ParseLimits::default(),
    )
    .expect("horned-owl parse");
}

#[test]
fn collect_opa_from_owl_thing_with_blank_node_object() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DallValuesFrom-2D002/conclusion.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let opas = collect_object_property_assertions(&text);
    assert_eq!(opas.len(), 1, "expected one OPA, got {opas:?}");
    assert!(opas[0].0.contains("premises002#i"));
    assert!(opas[0].1.contains("premises002#p"));
    assert!(opas[0].2.contains("#_o"));

    let deduped = dedupe_rdf_xml_ids(&text);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 1_000_000).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let injected = inject_rdf_based_punning_declarations(&relative_uris);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named_individuals = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named_individuals);
    let normalized = normalize_all_different_members(&individuals);
    let preprocessed = expand_all_disjoint_collections(&normalized);
    let preprocessed_opas = collect_object_property_assertions(&preprocessed);
    assert_eq!(
        preprocessed_opas.len(),
        1,
        "expected one OPA, got {preprocessed_opas:?}"
    );
    let loaded = crate::load_ontology_lenient(&path).expect("load conclusion");
    let has_opa = loaded
        .dl()
        .axioms()
        .any(|a| matches!(a, ontologos_core::DlAxiom::ObjectPropertyAssertion { .. }))
        || loaded
            .axioms()
            .iter()
            .any(|(_, a)| matches!(a, ontologos_core::Axiom::ObjectPropertyAssertion { .. }));
    assert!(has_opa, "loaded ontology missing OPA");
}

#[test]
fn collect_nested_opa_from_somevalues_conclusion() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DsomeValuesFrom-2D003/conclusion.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let opas = collect_object_property_assertions(&text);
    assert_eq!(opas.len(), 2, "expected two OPAs, got {opas:?}");
    assert!(opas[0].0.contains("fred"));
    assert!(opas[0].1.contains("parent"));
    let loaded = crate::load_ontology_lenient(&path).expect("load conclusion");
    let opa_count = loaded
        .axioms()
        .iter()
        .filter(|(_, a)| matches!(a, ontologos_core::Axiom::ObjectPropertyAssertion { .. }))
        .count();
    assert_eq!(opa_count, 2, "loaded conclusion should have two OPAs");
}

#[test]
fn qualify_ce_ofn_percent23_fragment_uses_hash_namespace() {
    let ce = "ObjectUnionOf(<http://www.w3.org/2002/03owlt/I5.5/premises005%23a>)";
    let (prefixes, qualified) = qualify_ce_ofn_for_supplement(ce);
    assert!(prefixes.contains("premises005#>"));
    assert_eq!(qualified, "ns1:a");
    let normalized = normalize_supplement_boolean_ce(ce);
    assert_eq!(
        normalized,
        "<http://www.w3.org/2002/03owlt/I5.5/premises005%23a>"
    );
}

#[test]
fn i5_5_005_singleton_union_supplement() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DI5.5-2D005/conclusion.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let deduped = dedupe_rdf_xml_ids(&text);
    let normalized_ids = normalize_invalid_rdf_ids(&deduped);
    let expanded = expand_xml_entities_with_limit(&normalized_ids, 10_000_000).unwrap();
    let relative_uris = normalize_relative_owl_uris(&expanded);
    let injected = inject_rdf_based_punning_declarations(&relative_uris);
    let typed_about = materialize_typed_about_elements(&injected);
    let typed_nodes = materialize_typed_node_elements(&typed_about);
    let intersections = normalize_class_intersection_definitions(&typed_nodes);
    let same_as = normalize_class_same_as(&intersections);
    let named = materialize_named_individual_descriptions(&same_as);
    let individuals = materialize_anonymous_individual_descriptions(&named);
    let normalized = normalize_all_different_members(&individuals);
    let preprocessed = expand_all_disjoint_collections(&normalized);
    let equivs = collect_boolean_class_equivalences(&preprocessed);
    assert!(
        equivs.iter().any(|(_, ce)| ce.contains("ObjectUnionOf")),
        "expected singleton union equivalence, got {equivs:?}"
    );
}

#[test]
fn disjoint_with_010_premise_loads_abox() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../benchmarks/data/hermit/wg/TestCase-3AWebOnt-2DdisjointWith-2D010/premise.rdf");
    let text = std::fs::read_to_string(&path).unwrap();
    let opas = collect_object_property_assertions(&text);
    assert_eq!(opas.len(), 1, "expected one OPA, got {opas:?}");
    let typing = collect_self_disjoint_restriction_assertions(&text);
    assert_eq!(
        typing.len(),
        1,
        "expected one class assertion, got {typing:?}"
    );
    let loaded = crate::load_ontology_lenient(&path).expect("load");
    assert!(
        loaded.dl().axiom_count() > 0 || !loaded.axioms().is_empty(),
        "loaded ontology should have axioms"
    );
}

#[test]
fn sameas_subst_premise_collects_nested_opa_and_loads() {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(
        "../../benchmarks/data/hermit/wg/Rdfbased-2Dsem-2Deqdis-2Dsameas-2Dsubst/premise.rdf",
    );
    let text = std::fs::read_to_string(&path).unwrap();
    let opas = collect_object_property_assertions(&text);
    assert!(
        opas.iter().any(|(_, prop, _)| prop.contains("#p1")),
        "expected nested sameAs OPA on p1, got {opas:?}"
    );
    let loaded = crate::load_ontology(&path).expect("load premise");
    let p1 = "http://www.example.org#p1";
    let id = loaded.lookup_entity(p1).expect("p1 entity");
    let rec = loaded.entity(id).expect("p1 record");
    assert_eq!(
        rec.kind,
        ontologos_core::EntityKind::ObjectProperty,
        "p1 should be object property, got {:?}",
        rec.kind
    );
}
