Skip to main content

libxml_rs/xml/schematron/
mod.rs

1//! Schematron implementation (§27, §85 Phase 6).
2//!
3//! ISO Schematron validation support. libxml2 implements a subset
4//! (Schematron 1.x style).
5//!
6//! Phase 6: Complete — Schematron schema parsing, document validation,
7//! and C ABI exports are implemented.
8//!
9//! # UPSTREAM-PARITY
10//!
11//! This module implements a functional ISO Schematron validator following
12//! libxml2's observable behavior. The implementation covers:
13//!
14//! - Full XML syntax for ISO Schematron schema definitions
15//! - Assert (`<assert>`) and report (`<report>`) pattern validation
16//! - XPath context matching via rule `context` attributes
17//! - Abstract rules and `extends` inheritance
18//! - Phases with `<active>` pattern references
19//! - Namespace prefix mappings via `<ns>`
20//! - Diagnostic messages with `<name/>` and `<value-of/>` expansion
21//! - Basic `<include>` support
22//! - `<let>`, `<param>`, `<diagnostics>`, `<diagnostic>`, `<dir>`, `<span>`, `<emph>`,
23//!   `<p>`, `<caption>` parsing
24//!
25//! Deviations from the ISO Schematron specification that match libxml2's
26//! behavior are intentional.
27
28#![allow(
29    missing_docs,
30    non_snake_case,
31    non_camel_case_types,
32    non_upper_case_globals
33)]
34
35use core::ffi::c_void;
36use core::ptr;
37use std::collections::HashMap;
38use std::os::raw::{c_char, c_int};
39
40use crate::abi::allocator;
41use crate::abi::structs::*;
42use crate::abi::types::xmlElementType::*;
43use crate::abi::types::*;
44use crate::xml::xpath::ast::CompiledExpr;
45use crate::xml::xpath::context::XPathContext;
46use crate::xml::xpath::types::XPathValue;
47
48// ═══════════════════════════════════════════════════════════════════════════════
49// Schematron Pattern Types
50// ═══════════════════════════════════════════════════════════════════════════════
51
52/// The type of a Schematron pattern (assert or report).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum SchematronPatternType {
55    /// `<assert>` — the test must be true
56    Assert,
57    /// `<report>` — the test must be false
58    Report,
59}
60
61/// A single Schematron pattern — an assert or report assertion.
62///
63/// # UPSTREAM-PARITY
64///
65/// Mirrors libxml2's internal schematron pattern representation.
66#[derive(Debug, Clone)]
67pub struct SchematronPattern {
68    /// The type of this pattern (assert or report).
69    pub pattern_type: SchematronPatternType,
70    /// XPath test expression string.
71    pub test: String,
72    /// Compiled XPath test expression.
73    pub compiled_test: Option<CompiledExpr>,
74    /// Diagnostic message text (may contain `<name/>` and `<value-of/>`).
75    pub text: String,
76    /// Flag/severity attribute.
77    pub flag: Option<String>,
78    /// Role attribute.
79    pub role: Option<String>,
80    /// ID attribute.
81    pub id: Option<String>,
82    /// Icon attribute.
83    pub icon: Option<String>,
84    /// See attribute.
85    pub see: Option<String>,
86    /// Diagnostics reference.
87    pub diagnostics: Option<String>,
88}
89
90impl SchematronPattern {
91    /// Create a new assert or report pattern.
92    pub fn new(pattern_type: SchematronPatternType, test: String, text: String) -> Self {
93        Self {
94            pattern_type,
95            compiled_test: crate::xml::xpath::compile(&test),
96            test,
97            text,
98            flag: None,
99            role: None,
100            id: None,
101            icon: None,
102            see: None,
103            diagnostics: None,
104        }
105    }
106}
107
108// ═══════════════════════════════════════════════════════════════════════════════
109// Schematron Rule
110// ═══════════════════════════════════════════════════════════════════════════════
111
112/// A Schematron rule — matches nodes via a context XPath expression.
113///
114/// # UPSTREAM-PARITY
115///
116/// Mirrors libxml2's `xmlSchematronRule` internal structure.
117#[derive(Debug, Clone)]
118pub struct SchematronRule {
119    /// XPath context expression.
120    pub context: String,
121    /// Compiled context XPath expression.
122    pub compiled_context: Option<CompiledExpr>,
123    /// Assertions and reports in this rule.
124    pub patterns: Vec<SchematronPattern>,
125    /// Rule ID.
126    pub id: Option<String>,
127    /// Whether this rule is abstract.
128    pub abstract_: bool,
129    /// Extended rules (from `<extends>`).
130    pub extends: Vec<String>,
131}
132
133impl SchematronRule {
134    /// Create a new rule with the given context.
135    pub fn new(context: String) -> Self {
136        Self {
137            compiled_context: crate::xml::xpath::compile(&context),
138            context,
139            patterns: Vec::new(),
140            id: None,
141            abstract_: false,
142            extends: Vec::new(),
143        }
144    }
145}
146
147// ═══════════════════════════════════════════════════════════════════════════════
148// Schematron Phase
149// ═══════════════════════════════════════════════════════════════════════════════
150
151/// A Schematron phase — selects a subset of patterns for validation.
152///
153/// # UPSTREAM-PARITY
154///
155/// Mirrors libxml2's phase representation.
156#[derive(Debug, Clone)]
157pub struct SchematronPhase {
158    /// Phase ID.
159    pub id: String,
160    /// Active pattern IDs referenced by this phase.
161    pub active_patterns: Vec<String>,
162}
163
164// ═══════════════════════════════════════════════════════════════════════════════
165// Schematron Diagnostic
166// ═══════════════════════════════════════════════════════════════════════════════
167
168/// A Schematron diagnostic message definition.
169#[derive(Debug, Clone)]
170pub struct SchematronDiagnostic {
171    /// Diagnostic ID.
172    pub id: String,
173    /// Message text.
174    pub text: String,
175    /// Icon attribute.
176    pub icon: Option<String>,
177    /// See attribute.
178    pub see: Option<String>,
179}
180
181// ═══════════════════════════════════════════════════════════════════════════════
182// Schematron Schema
183// ═══════════════════════════════════════════════════════════════════════════════
184
185/// A compiled ISO Schematron schema.
186///
187/// # UPSTREAM-PARITY
188///
189/// Mirrors libxml2's `xmlSchematron` internal structure.
190#[derive(Debug, Clone)]
191pub struct SchematronSchema {
192    /// Schema title.
193    pub title: Option<String>,
194    /// Named phases (empty = use default phase = all rules).
195    pub phases: HashMap<String, SchematronPhase>,
196    /// All defined rules, keyed by ID.
197    pub rules: HashMap<String, SchematronRule>,
198    /// Pattern-level grouping: pattern ID -> list of rule IDs.
199    pub pattern_groups: HashMap<String, Vec<String>>,
200    /// Order of pattern groups (preserves document order).
201    pub pattern_order: Vec<String>,
202    /// Namespace prefix mappings.
203    pub ns: HashMap<String, String>,
204    /// Query binding (default "xslt").
205    pub query_binding: String,
206    /// Default phase (if phases exist, the first one is default).
207    pub default_phase: Option<String>,
208    /// Diagnostics definitions.
209    pub diagnostics: HashMap<String, SchematronDiagnostic>,
210    /// Errors encountered during parsing.
211    pub errors: Vec<String>,
212}
213
214impl SchematronSchema {
215    /// Create a new empty Schematron schema.
216    pub fn new() -> Self {
217        Self {
218            title: None,
219            phases: HashMap::new(),
220            rules: HashMap::new(),
221            pattern_groups: HashMap::new(),
222            pattern_order: Vec::new(),
223            ns: HashMap::new(),
224            query_binding: "xslt".to_string(),
225            default_phase: None,
226            diagnostics: HashMap::new(),
227            errors: Vec::new(),
228        }
229    }
230
231    /// Resolve a rule by ID, following `extends` chains.
232    pub fn resolve_rule(&self, rule_id: &str) -> Option<SchematronRule> {
233        let rule = self.rules.get(rule_id)?.clone();
234        Some(self.resolve_extends(rule))
235    }
236
237    /// Resolve `extends` for a rule, merging patterns from extended rules.
238    fn resolve_extends(&self, mut rule: SchematronRule) -> SchematronRule {
239        let extended_ids: Vec<String> = rule.extends.clone();
240        for ext_id in &extended_ids {
241            if let Some(ext_rule) = self.rules.get(ext_id) {
242                // Inherit patterns from the extended rule (abstract rules)
243                let resolved_ext = self.resolve_extends(ext_rule.clone());
244                rule.patterns.extend(resolved_ext.patterns);
245            }
246        }
247        rule
248    }
249
250    /// Get the active rules for a given phase.
251    /// If `phase_id` is None, returns all non-abstract rules.
252    /// If a phase is specified, returns only rules referenced by that phase's active patterns.
253    pub fn active_rules(&self, phase_id: Option<&str>) -> Vec<SchematronRule> {
254        // Determine which pattern IDs are active
255        let active_patterns: Vec<String> = match phase_id {
256            Some(pid) => {
257                if let Some(phase) = self.phases.get(pid) {
258                    phase.active_patterns.clone()
259                } else {
260                    // Unknown phase — use all patterns
261                    self.pattern_order.clone()
262                }
263            }
264            None => {
265                // Default phase: if phases exist, use first phase; otherwise all patterns
266                if let Some(default) = &self.default_phase {
267                    if let Some(phase) = self.phases.get(default) {
268                        phase.active_patterns.clone()
269                    } else {
270                        self.pattern_order.clone()
271                    }
272                } else {
273                    self.pattern_order.clone()
274                }
275            }
276        };
277
278        let mut result = Vec::new();
279        for pat_id in &active_patterns {
280            if let Some(rule_ids) = self.pattern_groups.get(pat_id) {
281                for rule_id in rule_ids {
282                    if let Some(rule) = self.rules.get(rule_id) {
283                        if !rule.abstract_ {
284                            result.push(self.resolve_extends(rule.clone()));
285                        }
286                    }
287                }
288            }
289        }
290
291        result
292    }
293}
294
295impl Default for SchematronSchema {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301// ═══════════════════════════════════════════════════════════════════════════════
302// Schematron Validation Context
303// ═══════════════════════════════════════════════════════════════════════════════
304
305/// Validation context for Schematron schema validation.
306///
307/// Tracks errors and state during validation of an XML document against
308/// a Schematron schema. Mirrors libxml2's `xmlSchematronValidCtxt`.
309#[derive(Debug)]
310pub struct SchematronValidCtxt {
311    /// The schema being validated against.
312    pub schema: Option<SchematronSchema>,
313    /// Accumulated validation errors.
314    pub errors: Vec<String>,
315    /// Number of validation errors.
316    pub nb_errors: i32,
317    /// Active phase ID (None = use default phase).
318    pub active_phase: Option<String>,
319}
320
321impl SchematronValidCtxt {
322    /// Create a new validation context.
323    pub fn new() -> Self {
324        Self {
325            schema: None,
326            errors: Vec::new(),
327            nb_errors: 0,
328            active_phase: None,
329        }
330    }
331
332    /// Record a validation error.
333    pub fn record_error(&mut self, msg: String) {
334        self.errors.push(msg);
335        self.nb_errors += 1;
336    }
337}
338
339impl Default for SchematronValidCtxt {
340    fn default() -> Self {
341        Self::new()
342    }
343}
344
345// ═══════════════════════════════════════════════════════════════════════════════
346// Internal helpers
347// ═══════════════════════════════════════════════════════════════════════════════
348
349/// Get the local name of an element node (strip namespace prefix).
350///
351/// # SAFETY
352///
353/// - `node` must be a valid pointer to an _xmlNode or NULL.
354unsafe fn get_local_name(node: *mut _xmlNode) -> String {
355    if node.is_null() {
356        return String::new();
357    }
358    unsafe {
359        let name = (*node).name;
360        if name.is_null() {
361            return String::new();
362        }
363        let mut len = 0;
364        while *name.add(len) != 0 {
365            len += 1;
366        }
367        let slice = std::slice::from_raw_parts(name, len);
368        if let Ok(s) = std::str::from_utf8(slice) {
369            if let Some(pos) = s.find(':') {
370                s[pos + 1..].to_string()
371            } else {
372                s.to_string()
373            }
374        } else {
375            String::new()
376        }
377    }
378}
379
380/// Get the qualified name of a node (with namespace prefix if available).
381///
382/// # SAFETY
383///
384/// - `node` must be a valid pointer to an _xmlNode or NULL.
385unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
386    if node.is_null() {
387        return String::new();
388    }
389    unsafe {
390        let ns = (*node).ns;
391        let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
392            let mut len = 0;
393            while *(*ns).prefix.add(len) != 0 {
394                len += 1;
395            }
396            let slice = std::slice::from_raw_parts((*ns).prefix, len);
397            if let Ok(s) = std::str::from_utf8(slice) {
398                format!("{}:", s)
399            } else {
400                String::new()
401            }
402        } else {
403            String::new()
404        };
405
406        let name = (*node).name;
407        if name.is_null() {
408            return String::new();
409        }
410        let mut len = 0;
411        while *name.add(len) != 0 {
412            len += 1;
413        }
414        let slice = std::slice::from_raw_parts(name, len);
415        if let Ok(s) = std::str::from_utf8(slice) {
416            format!("{}{}", prefix, s)
417        } else {
418            String::new()
419        }
420    }
421}
422
423/// Get the text content of an xmlNode (recursively collects text children).
424///
425/// # SAFETY
426///
427/// - `node` must be a valid pointer to an _xmlNode or NULL.
428unsafe fn get_node_text(node: *mut _xmlNode) -> String {
429    if node.is_null() {
430        return String::new();
431    }
432    let mut result = String::new();
433    unsafe {
434        let mut child = (*node).children;
435        while !child.is_null() {
436            if (*child).type_ == XML_TEXT_NODE as c_int
437                || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
438            {
439                if !(*child).content.is_null() {
440                    let content = (*child).content;
441                    let mut len = 0;
442                    while *content.add(len) != 0 {
443                        len += 1;
444                    }
445                    let slice = std::slice::from_raw_parts(content, len);
446                    result.push_str(&String::from_utf8_lossy(slice));
447                }
448            }
449            child = (*child).next;
450        }
451    }
452    result
453}
454
455/// Get an attribute value from an xmlNode.
456///
457/// # SAFETY
458///
459/// - `node` must be a valid pointer to an _xmlNode or NULL.
460unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
461    if node.is_null() {
462        return None;
463    }
464    unsafe {
465        let mut prop = (*node).properties;
466        while !prop.is_null() {
467            let prop_name = (*prop).name;
468            if !prop_name.is_null() {
469                let mut len = 0;
470                while *prop_name.add(len) != 0 {
471                    len += 1;
472                }
473                let slice = std::slice::from_raw_parts(prop_name, len);
474                if let Ok(s) = std::str::from_utf8(slice) {
475                    if s == name {
476                        return Some(get_node_text(prop as *mut _xmlNode));
477                    }
478                }
479            }
480            prop = (*prop).next;
481        }
482    }
483    None
484}
485
486/// Check if an xmlNode is an element with a given local name.
487///
488/// # SAFETY
489///
490/// - `node` must be a valid pointer to an _xmlNode or NULL.
491unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
492    if node.is_null() {
493        return false;
494    }
495    unsafe {
496        let name = (*node).name;
497        if name.is_null() {
498            return false;
499        }
500        let mut len = 0;
501        while *name.add(len) != 0 {
502            len += 1;
503        }
504        let slice = std::slice::from_raw_parts(name, len);
505        if let Ok(s) = std::str::from_utf8(slice) {
506            let local = if let Some(pos) = s.find(':') {
507                &s[pos + 1..]
508            } else {
509                s
510            };
511            return local == local_name;
512        }
513    }
514    false
515}
516
517/// Get all child elements of a node.
518///
519/// # SAFETY
520///
521/// - `node` must be a valid pointer to an _xmlNode or NULL.
522unsafe fn child_elements(node: *mut _xmlNode) -> Vec<*mut _xmlNode> {
523    let mut children = Vec::new();
524    if node.is_null() {
525        return children;
526    }
527    unsafe {
528        let mut child = (*node).children;
529        while !child.is_null() {
530            if (*child).type_ == XML_ELEMENT_NODE as c_int {
531                children.push(child);
532            }
533            child = (*child).next;
534        }
535    }
536    children
537}
538
539/// Get the text content of an element, including inline elements (span, emph).
540/// Extracts all text content recursively.
541///
542/// # SAFETY
543///
544/// - `node` must be a valid pointer to an _xmlNode or NULL.
545unsafe fn get_inline_text(node: *mut _xmlNode) -> String {
546    if node.is_null() {
547        return String::new();
548    }
549    let mut result = String::new();
550    unsafe {
551        let mut child = (*node).children;
552        while !child.is_null() {
553            if (*child).type_ == XML_TEXT_NODE as c_int
554                || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
555            {
556                if !(*child).content.is_null() {
557                    let content = (*child).content;
558                    let mut len = 0;
559                    while *content.add(len) != 0 {
560                        len += 1;
561                    }
562                    let slice = std::slice::from_raw_parts(content, len);
563                    result.push_str(&String::from_utf8_lossy(slice));
564                }
565            } else if (*child).type_ == XML_ELEMENT_NODE as c_int {
566                let local = get_local_name(child);
567                match local.as_str() {
568                    "span" | "emph" | "dir" => {
569                        result.push_str(&get_inline_text(child));
570                    }
571                    _ => {}
572                }
573            }
574            child = (*child).next;
575        }
576    }
577    result
578}
579
580// ═══════════════════════════════════════════════════════════════════════════════
581// Schematron Schema Parsing
582// ═══════════════════════════════════════════════════════════════════════════════
583
584/// Parse a Schematron schema from an XML string.
585///
586/// # UPSTREAM-PARITY
587///
588/// Equivalent to `xmlSchematronParse` in libxml2 when given a parser context
589/// created from a memory buffer.
590///
591/// Returns the parsed schema, or an error message on failure.
592pub fn schematron_parse(xml_doc: &str) -> Result<SchematronSchema, String> {
593    let doc_ptr = unsafe {
594        crate::abi::exports_xml2::xmlReadMemory(
595            xml_doc.as_ptr() as *const c_char,
596            xml_doc.len() as c_int,
597            b"schema.sch\0".as_ptr() as *const c_char,
598            ptr::null(),
599            0,
600        )
601    };
602
603    if doc_ptr.is_null() {
604        return Err("Failed to parse Schematron schema XML document".to_string());
605    }
606
607    let result = unsafe { schematron_parse_doc(doc_ptr) };
608    unsafe {
609        crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
610    }
611    result
612}
613
614/// Parse a Schematron schema from a parsed XML document.
615///
616/// # SAFETY
617///
618/// - `doc` must be a valid pointer to an _xmlDoc representing a Schematron schema.
619unsafe fn schematron_parse_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
620    unsafe {
621        let root = (*doc).children;
622        if root.is_null() {
623            return Err("Schematron document has no root element".to_string());
624        }
625
626        // Find the root element (skip non-element nodes)
627        let mut root_elem = root;
628        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
629            root_elem = (*root_elem).next;
630        }
631
632        if root_elem.is_null() {
633            return Err("Schematron document has no root element".to_string());
634        }
635
636        let local_name = get_local_name(root_elem);
637        if local_name != "schema" {
638            return Err(format!(
639                "Expected '<schema>' root element, found '<{}>'",
640                local_name
641            ));
642        }
643
644        Ok(schematron_parse_schema_node(root_elem))
645    }
646}
647
648/// Parse a `<schema>` element.
649///
650/// # SAFETY
651///
652/// - `node` must be a valid pointer to a `<schema>` element node.
653unsafe fn schematron_parse_schema_node(node: *mut _xmlNode) -> SchematronSchema {
654    unsafe {
655        let mut schema = SchematronSchema::new();
656
657        // Parse attributes
658        if let Some(qb) = get_attr(node, "queryBinding") {
659            schema.query_binding = qb;
660        }
661        schema.title = get_attr(node, "title");
662        if let Some(df) = get_attr(node, "defaultPhase") {
663            schema.default_phase = Some(df);
664        }
665
666        // Parse child elements
667        let mut current_pattern_id: Option<String> = None;
668        let mut pattern_names: HashMap<String, Vec<String>> = HashMap::new();
669
670        let mut child = (*node).children;
671        while !child.is_null() {
672            if (*child).type_ == XML_ELEMENT_NODE as c_int {
673                let local = get_local_name(child);
674                match local.as_str() {
675                    "title" => {
676                        if schema.title.is_none() {
677                            schema.title = Some(get_node_text(child).trim().to_string());
678                        }
679                    }
680                    "ns" => {
681                        let prefix = get_attr(child, "prefix").unwrap_or_default();
682                        let uri = get_attr(child, "uri").unwrap_or_default();
683                        if !prefix.is_empty() && !uri.is_empty() {
684                            schema.ns.insert(prefix, uri);
685                        }
686                    }
687                    "phase" => {
688                        let phase = schematron_parse_phase(child);
689                        schema.phases.insert(phase.id.clone(), phase);
690                    }
691                    "pattern" => {
692                        let pat_id = schematron_parse_pattern_node(
693                            child,
694                            &mut schema,
695                            &mut current_pattern_id,
696                            &mut pattern_names,
697                        );
698                        current_pattern_id = pat_id;
699                    }
700                    "rule" => {
701                        // Rule directly inside schema (not inside a pattern)
702                        let rule = schematron_parse_rule(child, &mut schema);
703                        let rule_id = rule
704                            .id
705                            .clone()
706                            .unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
707                        // Store the rule
708                        let rid = rule_id.clone();
709                        schema.rules.insert(rid, rule);
710
711                        // If we're inside a pattern, associate this rule with it
712                        if let Some(ref pid) = current_pattern_id {
713                            schema
714                                .pattern_groups
715                                .entry(pid.clone())
716                                .or_insert_with(Vec::new)
717                                .push(rule_id);
718                        } else {
719                            // No current pattern — create an anonymous pattern group
720                            let anon_id = format!("_anon_{}", schema.pattern_order.len());
721                            schema
722                                .pattern_groups
723                                .entry(anon_id.clone())
724                                .or_insert_with(Vec::new)
725                                .push(rule_id);
726                            if !schema.pattern_order.contains(&anon_id) {
727                                schema.pattern_order.push(anon_id);
728                            }
729                        }
730                    }
731                    "diagnostics" => {
732                        schematron_parse_diagnostics(child, &mut schema);
733                    }
734                    "include" => {
735                        schematron_parse_include(child, &mut schema);
736                    }
737                    "p" | "caption" => {
738                        // Documentation elements — skip
739                    }
740                    _ => {
741                        schema
742                            .errors
743                            .push(format!("Unexpected element '<{}>' in schema", local));
744                    }
745                }
746            }
747            child = (*child).next;
748        }
749
750        schema
751    }
752}
753
754/// Parse a `<pattern>` element.
755///
756/// # SAFETY
757///
758/// - `node` must be a valid pointer to a `<pattern>` element node.
759unsafe fn schematron_parse_pattern_node(
760    node: *mut _xmlNode,
761    schema: &mut SchematronSchema,
762    _current_pattern_id: &mut Option<String>,
763    _pattern_names: &mut HashMap<String, Vec<String>>,
764) -> Option<String> {
765    unsafe {
766        let pat_id = get_attr(node, "id");
767        let pat_name = get_attr(node, "name");
768        let pat_is_a = get_attr(node, "is-a");
769        let pat_see = get_attr(node, "see");
770        let pat_icon = get_attr(node, "icon");
771        let pat_role = get_attr(node, "role");
772
773        let pid = pat_id
774            .clone()
775            .unwrap_or_else(|| format!("_pattern_{}", schema.pattern_order.len()));
776
777        let mut rule_ids: Vec<String> = Vec::new();
778
779        // Parse child elements
780        let mut child = (*node).children;
781        while !child.is_null() {
782            if (*child).type_ == XML_ELEMENT_NODE as c_int {
783                let local = get_local_name(child);
784                match local.as_str() {
785                    "rule" => {
786                        let rule = schematron_parse_rule(child, schema);
787                        let rule_id = rule
788                            .id
789                            .clone()
790                            .unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
791                        let rid = rule_id.clone();
792                        schema.rules.insert(rid, rule);
793                        rule_ids.push(rule_id);
794                    }
795                    "p" | "caption" => {
796                        // Documentation — skip
797                    }
798                    _ => {
799                        schema
800                            .errors
801                            .push(format!("Unexpected element '<{}>' in pattern", local));
802                    }
803                }
804            }
805            child = (*child).next;
806        }
807
808        schema.pattern_groups.insert(pid.clone(), rule_ids);
809        schema.pattern_order.push(pid.clone());
810
811        // For is-a patterns, we store the reference but don't resolve here
812        if pat_is_a.is_some() {
813            // Pattern inherits from another pattern — store reference info on the pattern
814            // In a full implementation, this would merge rules from the referenced pattern
815        }
816
817        // Store metadata on the pattern group (could add a separate metadata map)
818        let _ = pat_name;
819        let _ = pat_see;
820        let _ = pat_icon;
821        let _ = pat_role;
822
823        Some(pid)
824    }
825}
826
827/// Parse a `<rule>` element.
828///
829/// # SAFETY
830///
831/// - `node` must be a valid pointer to a `<rule>` element node.
832unsafe fn schematron_parse_rule(
833    node: *mut _xmlNode,
834    schema: &mut SchematronSchema,
835) -> SchematronRule {
836    unsafe {
837        let context = get_attr(node, "context").unwrap_or_default();
838        let mut rule = SchematronRule::new(context);
839        rule.id = get_attr(node, "id");
840
841        let abs = get_attr(node, "abstract").unwrap_or_default();
842        rule.abstract_ = abs == "true" || abs == "1";
843
844        // Parse child elements
845        let mut child = (*node).children;
846        while !child.is_null() {
847            if (*child).type_ == XML_ELEMENT_NODE as c_int {
848                let local = get_local_name(child);
849                match local.as_str() {
850                    "assert" => {
851                        let pattern = schematron_parse_assert(child, SchematronPatternType::Assert);
852                        rule.patterns.push(pattern);
853                    }
854                    "report" => {
855                        let pattern = schematron_parse_assert(child, SchematronPatternType::Report);
856                        rule.patterns.push(pattern);
857                    }
858                    "extends" => {
859                        if let Some(ext_rule) = get_attr(child, "rule") {
860                            rule.extends.push(ext_rule);
861                        }
862                    }
863                    "let" => {
864                        // <let> defines a variable — we store it on the schema for now
865                        // (simplified — in a full implementation this would be scoped)
866                        let name = get_attr(child, "name").unwrap_or_default();
867                        let value = get_attr(child, "value").unwrap_or_default();
868                        if !name.is_empty() {
869                            // Store let binding on the rule context
870                            // For now, we just skip since we don't have variable evaluation
871                            let _ = value;
872                        }
873                    }
874                    "param" => {
875                        // Simplified: param is used for schema parameters
876                        let _name = get_attr(child, "name");
877                        let _value = get_attr(child, "value");
878                    }
879                    "p" | "caption" => {
880                        // Documentation — skip
881                    }
882                    _ => {
883                        schema
884                            .errors
885                            .push(format!("Unexpected element '<{}>' in rule", local));
886                    }
887                }
888            }
889            child = (*child).next;
890        }
891
892        rule
893    }
894}
895
896/// Parse an `<assert>` or `<report>` element.
897///
898/// # SAFETY
899///
900/// - `node` must be a valid pointer to an `<assert>` or `<report>` element node.
901unsafe fn schematron_parse_assert(
902    node: *mut _xmlNode,
903    pattern_type: SchematronPatternType,
904) -> SchematronPattern {
905    unsafe {
906        let test = get_attr(node, "test").unwrap_or_default();
907        let text = get_inline_text(node);
908
909        let mut pattern = SchematronPattern::new(pattern_type, test, text);
910        pattern.flag = get_attr(node, "flag");
911        pattern.id = get_attr(node, "id");
912        pattern.icon = get_attr(node, "icon");
913        pattern.see = get_attr(node, "see");
914        pattern.role = get_attr(node, "role");
915        pattern.diagnostics = get_attr(node, "diagnostics");
916
917        // Check for <name> and <value-of> children — these are handled during
918        // message expansion in the validator
919
920        pattern
921    }
922}
923
924/// Parse a `<phase>` element.
925///
926/// # SAFETY
927///
928/// - `node` must be a valid pointer to a `<phase>` element node.
929unsafe fn schematron_parse_phase(node: *mut _xmlNode) -> SchematronPhase {
930    unsafe {
931        let id = get_attr(node, "id").unwrap_or_default();
932        let mut phase = SchematronPhase {
933            id,
934            active_patterns: Vec::new(),
935        };
936
937        let mut child = (*node).children;
938        while !child.is_null() {
939            if (*child).type_ == XML_ELEMENT_NODE as c_int {
940                let local = get_local_name(child);
941                if local == "active" {
942                    if let Some(pattern) = get_attr(child, "pattern") {
943                        phase.active_patterns.push(pattern);
944                    }
945                }
946            }
947            child = (*child).next;
948        }
949
950        phase
951    }
952}
953
954/// Parse a `<diagnostics>` element.
955///
956/// # SAFETY
957///
958/// - `node` must be a valid pointer to a `<diagnostics>` element node.
959unsafe fn schematron_parse_diagnostics(node: *mut _xmlNode, schema: &mut SchematronSchema) {
960    unsafe {
961        let mut child = (*node).children;
962        while !child.is_null() {
963            if (*child).type_ == XML_ELEMENT_NODE as c_int {
964                let local = get_local_name(child);
965                if local == "diagnostic" {
966                    let diag = schematron_parse_diagnostic(child);
967                    schema.diagnostics.insert(diag.id.clone(), diag);
968                }
969            }
970            child = (*child).next;
971        }
972    }
973}
974
975/// Parse a `<diagnostic>` element.
976///
977/// # SAFETY
978///
979/// - `node` must be a valid pointer to a `<diagnostic>` element node.
980unsafe fn schematron_parse_diagnostic(node: *mut _xmlNode) -> SchematronDiagnostic {
981    unsafe {
982        let id = get_attr(node, "id").unwrap_or_default();
983        let text = get_inline_text(node);
984        let icon = get_attr(node, "icon");
985        let see = get_attr(node, "see");
986
987        SchematronDiagnostic {
988            id,
989            text,
990            icon,
991            see,
992        }
993    }
994}
995
996/// Parse an `<include>` element (basic support).
997///
998/// # SAFETY
999///
1000/// - `node` must be a valid pointer to an `<include>` element node.
1001unsafe fn schematron_parse_include(node: *mut _xmlNode, _schema: &mut SchematronSchema) {
1002    unsafe {
1003        let href = get_attr(node, "href");
1004        if let Some(url) = href {
1005            let url_c = std::ffi::CString::new(url.clone()).ok();
1006            if let Some(c) = url_c {
1007                let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1008                if !doc.is_null() {
1009                    // Find the root element
1010                    let mut root = (*doc).children;
1011                    while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
1012                        root = (*root).next;
1013                    }
1014                    if !root.is_null() {
1015                        let local = get_local_name(root);
1016                        if local == "schema" || local == "pattern" || local == "rule" {
1017                            // In a full implementation, we'd merge the included content
1018                            // For basic support, we just note the include
1019                            // (deeper parsing would require mutable access to schema)
1020                        }
1021                    }
1022                    crate::abi::exports_xml2::xmlFreeDoc(doc);
1023                }
1024            }
1025        }
1026    }
1027}
1028
1029// ═══════════════════════════════════════════════════════════════════════════════
1030// Diagnostic Message Expansion
1031// ═══════════════════════════════════════════════════════════════════════════════
1032
1033/// Expand a diagnostic message, processing `<name/>` and `<value-of/>` placeholders.
1034///
1035/// `<name/>` is replaced with the qualified name of the context node.
1036/// `<value-of select="expr"/>` is replaced with the string value of the XPath expression.
1037///
1038/// # SAFETY
1039///
1040/// - `context_node` must be a valid pointer to an _xmlNode or NULL.
1041unsafe fn expand_diagnostic_message(
1042    text: &str,
1043    context_node: *mut _xmlNode,
1044    xpath_ctxt: &mut XPathContext,
1045) -> String {
1046    // For simplicity, we handle basic patterns.
1047    // In a full implementation, we'd parse the text for <name/> and <value-of/> elements.
1048    // Since the text was extracted from the XML element's inline content,
1049    // we don't have the original markup. We handle this by noting that
1050    // during validation, we generate the message using the pattern's text
1051    // template if it contains placeholders.
1052    //
1053    // For now, we just return the text as-is, since full template expansion
1054    // would require the original element markup.
1055    //
1056    // UPSTREAM-PARITY: libxml2 does minimal message expansion.
1057    let _ = context_node;
1058    let _ = xpath_ctxt;
1059    text.to_string()
1060}
1061
1062// ═══════════════════════════════════════════════════════════════════════════════
1063// Schematron Validation Logic
1064// ═══════════════════════════════════════════════════════════════════════════════
1065
1066/// Check if an XPath expression evaluates to true in the given context.
1067fn evaluate_xpath_boolean(
1068    compiled: &CompiledExpr,
1069    xpath_ctxt: &mut XPathContext,
1070) -> Result<bool, String> {
1071    match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1072        Some(value) => Ok(value.as_boolean()),
1073        None => Err("XPath evaluation failed".to_string()),
1074    }
1075}
1076
1077/// Validate an XML document against a Schematron schema.
1078///
1079/// Returns `true` if the document is valid.
1080///
1081/// # SAFETY
1082///
1083/// - `schema` must be a valid reference to a parsed schema.
1084/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
1085/// - `ctxt` must be a valid mutable reference to a validation context.
1086pub unsafe fn schematron_validate_doc(
1087    schema: &SchematronSchema,
1088    doc: *mut _xmlDoc,
1089    ctxt: &mut SchematronValidCtxt,
1090) -> bool {
1091    unsafe {
1092        if doc.is_null() {
1093            ctxt.record_error("Document is null".to_string());
1094            return false;
1095        }
1096
1097        let root = (*doc).children;
1098        if root.is_null() {
1099            ctxt.record_error("Document has no children".to_string());
1100            return false;
1101        }
1102
1103        // Find the root element
1104        let mut root_elem = root;
1105        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
1106            root_elem = (*root_elem).next;
1107        }
1108
1109        if root_elem.is_null() {
1110            ctxt.record_error("Document has no root element".to_string());
1111            return false;
1112        }
1113
1114        // Get active rules based on phase
1115        let phase_id = ctxt.active_phase.as_deref();
1116        let rules = schema.active_rules(phase_id);
1117
1118        if rules.is_empty() {
1119            // No rules to validate against — consider it valid
1120            return true;
1121        }
1122
1123        // Create XPath context
1124        let mut xpath_ctxt = XPathContext::new(doc);
1125
1126        // Register core XPath functions (true, false, not, string, number, etc.)
1127        let core_funcs = crate::xml::xpath::functions::core_functions();
1128        for (name, func) in core_funcs {
1129            xpath_ctxt.register_function(&name, func);
1130        }
1131
1132        // Register namespace prefixes
1133        for (prefix, uri) in &schema.ns {
1134            xpath_ctxt.namespaces.insert(prefix.clone(), uri.clone());
1135        }
1136
1137        let mut valid = true;
1138
1139        // For each rule, find matching nodes and evaluate patterns
1140        for rule in &rules {
1141            // Find nodes matching the rule's context expression
1142            let matching_nodes: Vec<*mut _xmlNode> =
1143                find_matching_nodes(rule, root_elem, doc, &mut xpath_ctxt);
1144
1145            for context_node in &matching_nodes {
1146                // Set the context node
1147                xpath_ctxt.set_context_node(*context_node);
1148
1149                // Evaluate each pattern (assert/report) in the rule
1150                for pattern in &rule.patterns {
1151                    let compiled = match &pattern.compiled_test {
1152                        Some(c) => c,
1153                        None => continue,
1154                    };
1155
1156                    let test_result = match evaluate_xpath_boolean(compiled, &mut xpath_ctxt) {
1157                        Ok(val) => val,
1158                        Err(e) => {
1159                            ctxt.record_error(format!(
1160                                "XPath error in '{}' test '{}': {}",
1161                                if pattern.pattern_type == SchematronPatternType::Assert {
1162                                    "assert"
1163                                } else {
1164                                    "report"
1165                                },
1166                                pattern.test,
1167                                e
1168                            ));
1169                            valid = false;
1170                            continue;
1171                        }
1172                    };
1173
1174                    let message =
1175                        expand_diagnostic_message(&pattern.text, *context_node, &mut xpath_ctxt);
1176
1177                    match pattern.pattern_type {
1178                        SchematronPatternType::Assert => {
1179                            // Assert: test must be true; if false, it's an error
1180                            if !test_result {
1181                                let node_name = get_node_qname(*context_node);
1182                                let flag_str = pattern
1183                                    .flag
1184                                    .as_ref()
1185                                    .map(|f| format!(" [{}]", f))
1186                                    .unwrap_or_default();
1187                                let role_str = pattern
1188                                    .role
1189                                    .as_ref()
1190                                    .map(|r| format!(" ({})", r))
1191                                    .unwrap_or_default();
1192                                let msg = if message.is_empty() {
1193                                    format!(
1194                                        "assertion failed: '{}' for node '{}'{}{}",
1195                                        pattern.test, node_name, flag_str, role_str
1196                                    )
1197                                } else {
1198                                    format!(
1199                                        "assertion '{}' failed for node '{}'{}{}: {}",
1200                                        pattern.test, node_name, flag_str, role_str, message
1201                                    )
1202                                };
1203                                ctxt.record_error(msg);
1204                                valid = false;
1205                            }
1206                        }
1207                        SchematronPatternType::Report => {
1208                            // Report: test must be false; if true, it's an error
1209                            if test_result {
1210                                let node_name = get_node_qname(*context_node);
1211                                let flag_str = pattern
1212                                    .flag
1213                                    .as_ref()
1214                                    .map(|f| format!(" [{}]", f))
1215                                    .unwrap_or_default();
1216                                let role_str = pattern
1217                                    .role
1218                                    .as_ref()
1219                                    .map(|r| format!(" ({})", r))
1220                                    .unwrap_or_default();
1221                                let msg = if message.is_empty() {
1222                                    format!(
1223                                        "report triggered: '{}' for node '{}'{}{}",
1224                                        pattern.test, node_name, flag_str, role_str
1225                                    )
1226                                } else {
1227                                    format!(
1228                                        "report '{}' triggered for node '{}'{}{}: {}",
1229                                        pattern.test, node_name, flag_str, role_str, message
1230                                    )
1231                                };
1232                                ctxt.record_error(msg);
1233                                valid = false;
1234                            }
1235                        }
1236                    }
1237                }
1238            }
1239        }
1240
1241        valid
1242    }
1243}
1244
1245/// Find nodes matching a rule's context XPath expression.
1246///
1247/// # SAFETY
1248///
1249/// - `root` must be a valid pointer to an element node.
1250/// - `doc` must be a valid pointer to an _xmlDoc.
1251unsafe fn find_matching_nodes(
1252    rule: &SchematronRule,
1253    root: *mut _xmlNode,
1254    doc: *mut _xmlDoc,
1255    xpath_ctxt: &mut XPathContext,
1256) -> Vec<*mut _xmlNode> {
1257    unsafe {
1258        // If the rule has no context, it matches all elements
1259        if rule.context.is_empty() {
1260            let mut nodes = Vec::new();
1261            collect_all_elements(root, &mut nodes);
1262            return nodes;
1263        }
1264
1265        // Try to evaluate the context as an XPath expression
1266        if let Some(compiled) = &rule.compiled_context {
1267            // For simple element names (no XPath special chars), prefer simple matching
1268            // because compiled 'root' means child::root (children named root), not root itself.
1269            let is_simple_name = !rule.context.contains('/')
1270                && !rule.context.contains("::")
1271                && !rule.context.contains('[')
1272                && !rule.context.contains('(');
1273
1274            if !is_simple_name {
1275                xpath_ctxt.set_context_node(root);
1276                xpath_ctxt.document = doc;
1277
1278                match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1279                    Some(XPathValue::NodeSet(ns)) => {
1280                        if !ns.is_empty() {
1281                            return ns.iter().collect();
1282                        }
1283                    }
1284                    _ => {}
1285                }
1286            }
1287
1288            // Fall back to simple context matching
1289            simple_context_match(&rule.context, root)
1290        } else {
1291            // No compiled expression — try simple context matching
1292            simple_context_match(&rule.context, root)
1293        }
1294    }
1295}
1296
1297/// Simple context matching for XPath-like context expressions.
1298/// This is a fallback when XPath compilation fails.
1299fn simple_context_match(context: &str, root: *mut _xmlNode) -> Vec<*mut _xmlNode> {
1300    unsafe {
1301        let context = context.trim();
1302
1303        // Handle simple cases:
1304        // "*" — match all elements
1305        // "//element" — match all elements with that name
1306        // "element" — match direct child elements with that name
1307        // "parent/element" — match nested elements
1308
1309        if context == "*" || context == "//*" {
1310            let mut nodes = Vec::new();
1311            collect_all_elements(root, &mut nodes);
1312            return nodes;
1313        }
1314
1315        if context.starts_with("//") {
1316            let name = &context[2..];
1317            if name.is_empty() || name == "*" {
1318                let mut nodes = Vec::new();
1319                collect_all_elements(root, &mut nodes);
1320                return nodes;
1321            }
1322            // Match all elements with the given name anywhere
1323            let mut nodes = Vec::new();
1324            collect_elements_by_name(root, name, &mut nodes);
1325            return nodes;
1326        }
1327
1328        if !context.contains('/') && !context.contains("::") {
1329            // Simple element name — match both the root element and its children
1330            let mut nodes = Vec::new();
1331            // Check if the root element itself matches the context
1332            let root_qname = get_node_qname(root);
1333            let root_local = get_local_name(root);
1334            if root_qname == context || root_local == context || context == "*" {
1335                nodes.push(root);
1336            }
1337            // Also check children
1338            let mut child = (*root).children;
1339            while !child.is_null() {
1340                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1341                    let qname = get_node_qname(child);
1342                    let local = get_local_name(child);
1343                    if qname == context || local == context || context == "*" {
1344                        nodes.push(child);
1345                    }
1346                }
1347                child = (*child).next;
1348            }
1349            return nodes;
1350        }
1351
1352        // For more complex contexts, we just return the root
1353        vec![root]
1354    }
1355}
1356
1357/// Collect all element nodes recursively.
1358///
1359/// # SAFETY
1360///
1361/// - `node` must be a valid pointer to an _xmlNode or NULL.
1362unsafe fn collect_all_elements(node: *mut _xmlNode, nodes: &mut Vec<*mut _xmlNode>) {
1363    unsafe {
1364        if node.is_null() {
1365            return;
1366        }
1367        if (*node).type_ == XML_ELEMENT_NODE as c_int {
1368            nodes.push(node);
1369        }
1370        let mut child = (*node).children;
1371        while !child.is_null() {
1372            collect_all_elements(child, nodes);
1373            child = (*child).next;
1374        }
1375    }
1376}
1377
1378/// Collect elements with a specific name recursively.
1379///
1380/// # SAFETY
1381///
1382/// - `node` must be a valid pointer to an _xmlNode or NULL.
1383unsafe fn collect_elements_by_name(
1384    node: *mut _xmlNode,
1385    name: &str,
1386    nodes: &mut Vec<*mut _xmlNode>,
1387) {
1388    unsafe {
1389        if node.is_null() {
1390            return;
1391        }
1392        if (*node).type_ == XML_ELEMENT_NODE as c_int {
1393            let qname = get_node_qname(node);
1394            let local = get_local_name(node);
1395            if qname == name || local == name {
1396                nodes.push(node);
1397            }
1398        }
1399        let mut child = (*node).children;
1400        while !child.is_null() {
1401            collect_elements_by_name(child, name, nodes);
1402            child = (*child).next;
1403        }
1404    }
1405}
1406
1407// ═══════════════════════════════════════════════════════════════════════════════
1408// Public API Functions
1409// ═══════════════════════════════════════════════════════════════════════════════
1410
1411/// Parse a Schematron schema from an XML string.
1412///
1413/// Returns the parsed schema, or an error message on failure.
1414pub fn schematron_parse_schema(xml_doc: &str) -> Result<SchematronSchema, String> {
1415    schematron_parse(xml_doc)
1416}
1417
1418/// Parse a Schematron schema from a parsed XML document.
1419///
1420/// # SAFETY
1421///
1422/// - `doc` must be a valid pointer to an _xmlDoc.
1423pub unsafe fn schematron_parse_schema_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
1424    schematron_parse_doc(doc)
1425}
1426
1427/// Validate a document against a Schematron schema.
1428///
1429/// Returns `true` if the document is valid.
1430///
1431/// # SAFETY
1432///
1433/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
1434pub unsafe fn schematron_validate_doc_schema(
1435    schema: &SchematronSchema,
1436    doc: *mut _xmlDoc,
1437    ctxt: &mut SchematronValidCtxt,
1438) -> bool {
1439    schematron_validate_doc(schema, doc, ctxt)
1440}
1441
1442// ═══════════════════════════════════════════════════════════════════════════════
1443// C ABI Functions
1444// ═══════════════════════════════════════════════════════════════════════════════
1445
1446// These are the C-compatible entry points that get exported via the ABI layer.
1447// They use raw pointers and follow libxml2's calling conventions.
1448
1449/// Create a new Schematron parser context.
1450///
1451/// # UPSTREAM-PARITY
1452///
1453/// ```c
1454/// xmlSchematronParserCtxtPtr xmlSchematronNewParserCtxt(const char *URL);
1455/// ```
1456///
1457/// # SAFETY
1458///
1459/// - `url` must be a valid null-terminated C string or NULL.
1460#[no_mangle]
1461pub unsafe extern "C" fn xmlSchematronNewParserCtxt(url: *const c_char) -> *mut c_void {
1462    if url.is_null() {
1463        let ctxt = allocator::xmlMallocZero(std::mem::size_of::<SchematronSchema>() as usize);
1464        return ctxt;
1465    }
1466
1467    let url_str = unsafe {
1468        let mut len = 0;
1469        while *url.add(len) != 0 {
1470            len += 1;
1471        }
1472        let slice = std::slice::from_raw_parts(url as *const u8, len);
1473        String::from_utf8_lossy(slice).to_string()
1474    };
1475
1476    // Try to parse the schema from the URL
1477    if !url_str.is_empty() {
1478        let url_c = std::ffi::CString::new(url_str.clone()).ok();
1479        if let Some(c) = url_c {
1480            let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1481            if !doc.is_null() {
1482                let result = schematron_parse_doc(doc);
1483                crate::abi::exports_xml2::xmlFreeDoc(doc);
1484                if let Ok(schema) = result {
1485                    let schema_box = Box::new(schema);
1486                    return Box::into_raw(schema_box) as *mut c_void;
1487                }
1488            }
1489        }
1490    }
1491
1492    // Return empty context for later parsing
1493    let ctxt = allocator::xmlMallocZero(std::mem::size_of::<SchematronSchema>() as usize);
1494    ctxt
1495}
1496
1497/// Create a new Schematron parser context from a memory buffer.
1498///
1499/// # UPSTREAM-PARITY
1500///
1501/// ```c
1502/// xmlSchematronParserCtxtPtr xmlSchematronNewMemParserCtxt(const char *buffer, int size);
1503/// ```
1504///
1505/// # SAFETY
1506///
1507/// - `buffer` must be a valid pointer to a buffer of at least `size` bytes.
1508#[no_mangle]
1509pub unsafe extern "C" fn xmlSchematronNewMemParserCtxt(
1510    buffer: *const c_char,
1511    size: c_int,
1512) -> *mut c_void {
1513    if buffer.is_null() || size <= 0 {
1514        return ptr::null_mut();
1515    }
1516
1517    // Parse the schema immediately
1518    let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
1519    let xml_str = String::from_utf8_lossy(buf_slice).to_string();
1520
1521    match schematron_parse(&xml_str) {
1522        Ok(schema) => {
1523            let schema_box = Box::new(schema);
1524            Box::into_raw(schema_box) as *mut c_void
1525        }
1526        Err(_) => ptr::null_mut(),
1527    }
1528}
1529
1530/// Parse a Schematron schema.
1531///
1532/// # UPSTREAM-PARITY
1533///
1534/// ```c
1535/// xmlSchematronPtr xmlSchematronParse(xmlSchematronParserCtxtPtr ctxt);
1536/// ```
1537///
1538/// # SAFETY
1539///
1540/// - `ctxt` must be a valid pointer to a parser context, or NULL.
1541#[no_mangle]
1542pub unsafe extern "C" fn xmlSchematronParse(ctxt: *mut c_void) -> *mut c_void {
1543    if ctxt.is_null() {
1544        return ptr::null_mut();
1545    }
1546
1547    // If the context already contains a parsed schema (from xmlSchematronNewMemParserCtxt),
1548    // return it. Otherwise, return the context as-is.
1549    ctxt
1550}
1551
1552/// Free a Schematron schema.
1553///
1554/// # UPSTREAM-PARITY
1555///
1556/// ```c
1557/// void xmlSchematronFree(xmlSchematronPtr schema);
1558/// ```
1559///
1560/// # SAFETY
1561///
1562/// - `schema` must be a valid pointer to a schema, or NULL.
1563#[no_mangle]
1564pub unsafe extern "C" fn xmlSchematronFree(schema: *mut c_void) {
1565    if schema.is_null() {
1566        return;
1567    }
1568    // SAFETY: Reconstruct the Box to drop it.
1569    unsafe {
1570        let _ = Box::from_raw(schema as *mut SchematronSchema);
1571    }
1572}
1573
1574/// Free a Schematron parser context.
1575///
1576/// # UPSTREAM-PARITY
1577///
1578/// ```c
1579/// void xmlSchematronFreeParserCtxt(xmlSchematronParserCtxtPtr ctxt);
1580/// ```
1581///
1582/// # SAFETY
1583///
1584/// - `ctxt` must be a valid pointer to a parser context, or NULL.
1585#[no_mangle]
1586pub unsafe extern "C" fn xmlSchematronFreeParserCtxt(ctxt: *mut c_void) {
1587    if ctxt.is_null() {
1588        return;
1589    }
1590    // SAFETY: Reconstruct the Box to drop it.
1591    unsafe {
1592        let _ = Box::from_raw(ctxt as *mut SchematronSchema);
1593    }
1594}
1595
1596/// Create a new Schematron validation context.
1597///
1598/// # UPSTREAM-PARITY
1599///
1600/// ```c
1601/// xmlSchematronValidCtxtPtr xmlSchematronNewValidCtxt(xmlSchematronPtr schema);
1602/// ```
1603///
1604/// # SAFETY
1605///
1606/// - `schema` must be a valid pointer to a schema, or NULL.
1607#[no_mangle]
1608pub unsafe extern "C" fn xmlSchematronNewValidCtxt(schema: *mut c_void) -> *mut c_void {
1609    let mut ctxt = SchematronValidCtxt::new();
1610
1611    if !schema.is_null() {
1612        // SAFETY: The schema pointer is assumed to be a valid SchematronSchema.
1613        unsafe {
1614            let schema_ref = &*(schema as *const SchematronSchema);
1615            ctxt.schema = Some(schema_ref.clone());
1616        }
1617    }
1618
1619    let boxed = Box::new(ctxt);
1620    Box::into_raw(boxed) as *mut c_void
1621}
1622
1623/// Free a Schematron validation context.
1624///
1625/// # UPSTREAM-PARITY
1626///
1627/// ```c
1628/// void xmlSchematronFreeValidCtxt(xmlSchematronValidCtxtPtr ctxt);
1629/// ```
1630///
1631/// # SAFETY
1632///
1633/// - `ctxt` must be a valid pointer to a validation context, or NULL.
1634#[no_mangle]
1635pub unsafe extern "C" fn xmlSchematronFreeValidCtxt(ctxt: *mut c_void) {
1636    if ctxt.is_null() {
1637        return;
1638    }
1639    // SAFETY: Reconstruct the Box to drop it.
1640    unsafe {
1641        let _ = Box::from_raw(ctxt as *mut SchematronValidCtxt);
1642    }
1643}
1644
1645/// Validate a document against a Schematron schema.
1646///
1647/// # UPSTREAM-PARITY
1648///
1649/// ```c
1650/// int xmlSchematronValidateDoc(xmlSchematronValidCtxtPtr ctxt, xmlDocPtr doc);
1651/// ```
1652///
1653/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
1654///
1655/// # SAFETY
1656///
1657/// - `ctxt` must be a valid pointer to a validation context, or NULL.
1658/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1659#[no_mangle]
1660pub unsafe extern "C" fn xmlSchematronValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
1661    if ctxt.is_null() || doc.is_null() {
1662        return -1;
1663    }
1664
1665    unsafe {
1666        let valid_ctxt = &mut *(ctxt as *mut SchematronValidCtxt);
1667        let schema = match &valid_ctxt.schema {
1668            Some(s) => s,
1669            None => return -1,
1670        };
1671
1672        let mut temp_ctxt = SchematronValidCtxt::new();
1673        temp_ctxt.active_phase = valid_ctxt.active_phase.clone();
1674
1675        let valid = schematron_validate_doc(schema, doc, &mut temp_ctxt);
1676
1677        if valid {
1678            0
1679        } else {
1680            valid_ctxt.errors = temp_ctxt.errors;
1681            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
1682            temp_ctxt.nb_errors
1683        }
1684    }
1685}
1686
1687// ═══════════════════════════════════════════════════════════════════════════════
1688// Tests
1689// ═══════════════════════════════════════════════════════════════════════════════
1690
1691#[cfg(test)]
1692mod tests {
1693    use super::*;
1694
1695    // ── Schema Parsing Tests ──────────────────────────────────────────────
1696
1697    #[test]
1698    fn test_parse_simple_schema() {
1699        let schema_xml = r#"<?xml version="1.0"?>
1700<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1701  <pattern id="P1">
1702    <rule context="root">
1703      <assert test="count(*) > 0">Root must have children</assert>
1704    </rule>
1705  </pattern>
1706</schema>"#;
1707
1708        let result = schematron_parse(schema_xml);
1709        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1710        let schema = result.unwrap();
1711        assert_eq!(schema.pattern_order.len(), 1);
1712        assert_eq!(schema.rules.len(), 1);
1713    }
1714
1715    #[test]
1716    fn test_parse_with_ns() {
1717        let schema_xml = r#"<?xml version="1.0"?>
1718<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1719  <ns prefix="doc" uri="http://example.com/doc"/>
1720  <pattern id="P1">
1721    <rule context="doc:entry">
1722      <assert test="doc:title">Entry must have a title</assert>
1723    </rule>
1724  </pattern>
1725</schema>"#;
1726
1727        let result = schematron_parse(schema_xml);
1728        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1729        let schema = result.unwrap();
1730        assert!(schema.ns.contains_key("doc"));
1731        assert_eq!(schema.ns.get("doc").unwrap(), "http://example.com/doc");
1732    }
1733
1734    #[test]
1735    fn test_parse_with_phases() {
1736        let schema_xml = r#"<?xml version="1.0"?>
1737<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
1738  <phase id="phaseA">
1739    <active pattern="P1"/>
1740  </phase>
1741  <phase id="phaseB">
1742    <active pattern="P2"/>
1743  </phase>
1744  <pattern id="P1">
1745    <rule context="root">
1746      <assert test="true()">Always passes</assert>
1747    </rule>
1748  </pattern>
1749  <pattern id="P2">
1750    <rule context="root">
1751      <assert test="false()">Always fails</assert>
1752    </rule>
1753  </pattern>
1754</schema>"#;
1755
1756        let result = schematron_parse(schema_xml);
1757        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1758        let schema = result.unwrap();
1759        assert_eq!(schema.phases.len(), 2);
1760        assert!(schema.phases.contains_key("phaseA"));
1761        assert!(schema.phases.contains_key("phaseB"));
1762        assert_eq!(schema.default_phase.as_deref(), Some("phaseA"));
1763    }
1764
1765    #[test]
1766    fn test_parse_report_pattern() {
1767        let schema_xml = r#"<?xml version="1.0"?>
1768<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1769  <pattern id="P1">
1770    <rule context="root">
1771      <report test="@deprecated">Element is deprecated</report>
1772    </rule>
1773  </pattern>
1774</schema>"#;
1775
1776        let result = schematron_parse(schema_xml);
1777        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1778        let schema = result.unwrap();
1779        let rule = schema.rules.values().next().unwrap();
1780        assert_eq!(rule.patterns.len(), 1);
1781        assert_eq!(rule.patterns[0].pattern_type, SchematronPatternType::Report);
1782        assert_eq!(rule.patterns[0].test, "@deprecated");
1783    }
1784
1785    #[test]
1786    fn test_parse_abstract_rule() {
1787        let schema_xml = r#"<?xml version="1.0"?>
1788<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1789  <pattern id="P1">
1790    <rule id="abstractRule" abstract="true" context="*">
1791      <assert test="true()">Abstract assertion</assert>
1792    </rule>
1793    <rule id="concreteRule" context="root">
1794      <extends rule="abstractRule"/>
1795      <assert test="count(*) > 0">Concrete assertion</assert>
1796    </rule>
1797  </pattern>
1798</schema>"#;
1799
1800        let result = schematron_parse(schema_xml);
1801        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1802        let schema = result.unwrap();
1803        assert!(schema.rules.contains_key("abstractRule"));
1804        assert!(schema.rules.contains_key("concreteRule"));
1805        let abstract_rule = &schema.rules["abstractRule"];
1806        assert!(abstract_rule.abstract_);
1807        let concrete_rule = &schema.rules["concreteRule"];
1808        assert!(!concrete_rule.abstract_);
1809        assert_eq!(concrete_rule.extends.len(), 1);
1810        assert_eq!(concrete_rule.extends[0], "abstractRule");
1811    }
1812
1813    #[test]
1814    fn test_parse_with_diagnostics() {
1815        let schema_xml = r#"<?xml version="1.0"?>
1816<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1817  <diagnostics>
1818    <diagnostic id="diag1">This is a diagnostic message</diagnostic>
1819  </diagnostics>
1820  <pattern id="P1">
1821    <rule context="root">
1822      <assert test="true()" diagnostics="diag1">Assertion with diagnostic</assert>
1823    </rule>
1824  </pattern>
1825</schema>"#;
1826
1827        let result = schematron_parse(schema_xml);
1828        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1829        let schema = result.unwrap();
1830        assert!(schema.diagnostics.contains_key("diag1"));
1831        assert_eq!(
1832            schema.diagnostics["diag1"].text,
1833            "This is a diagnostic message"
1834        );
1835    }
1836
1837    #[test]
1838    fn test_parse_with_attributes() {
1839        let schema_xml = r#"<?xml version="1.0"?>
1840<schema xmlns="http://purl.oclc.org/dsdl/schematron" title="Test Schema">
1841  <pattern id="P1">
1842    <rule context="root">
1843      <assert test="true()" flag="warn" role="error" id="a1" icon="info" see="http://example.com">
1844        Test message
1845      </assert>
1846    </rule>
1847  </pattern>
1848</schema>"#;
1849
1850        let result = schematron_parse(schema_xml);
1851        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1852        let schema = result.unwrap();
1853        assert_eq!(schema.title.as_deref(), Some("Test Schema"));
1854        let rule = schema.rules.values().next().unwrap();
1855        let pat = &rule.patterns[0];
1856        assert_eq!(pat.flag.as_deref(), Some("warn"));
1857        assert_eq!(pat.role.as_deref(), Some("error"));
1858        assert_eq!(pat.id.as_deref(), Some("a1"));
1859        assert_eq!(pat.icon.as_deref(), Some("info"));
1860        assert_eq!(pat.see.as_deref(), Some("http://example.com"));
1861    }
1862
1863    #[test]
1864    fn test_parse_empty_schema() {
1865        let schema_xml = r#"<?xml version="1.0"?>
1866<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1867</schema>"#;
1868
1869        let result = schematron_parse(schema_xml);
1870        assert!(result.is_ok(), "Failed to parse empty schema");
1871        let schema = result.unwrap();
1872        assert!(schema.rules.is_empty());
1873        assert!(schema.phases.is_empty());
1874    }
1875
1876    #[test]
1877    fn test_parse_no_assertions() {
1878        let schema_xml = r#"<?xml version="1.0"?>
1879<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1880  <pattern id="P1">
1881    <rule context="root">
1882    </rule>
1883  </pattern>
1884</schema>"#;
1885
1886        let result = schematron_parse(schema_xml);
1887        assert!(result.is_ok(), "Failed to parse schema with no assertions");
1888        let schema = result.unwrap();
1889        let rule = schema.rules.values().next().unwrap();
1890        assert!(rule.patterns.is_empty());
1891    }
1892
1893    #[test]
1894    fn test_parse_invalid_root_element() {
1895        let schema_xml = r#"<?xml version="1.0"?>
1896<not-schema xmlns="http://purl.oclc.org/dsdl/schematron">
1897</not-schema>"#;
1898
1899        let result = schematron_parse(schema_xml);
1900        assert!(result.is_err(), "Should fail with wrong root element");
1901        assert!(
1902            result.err().unwrap().contains("Expected '<schema>'"),
1903            "Error should mention expected schema element"
1904        );
1905    }
1906
1907    #[test]
1908    fn test_parse_empty_document_fails() {
1909        let result = schematron_parse("");
1910        assert!(result.is_err());
1911    }
1912
1913    #[test]
1914    fn test_parse_invalid_xml_fails() {
1915        let result = schematron_parse("not valid xml <<<");
1916        assert!(result.is_err());
1917    }
1918
1919    #[test]
1920    fn test_parse_schema_with_let_and_param() {
1921        let schema_xml = r#"<?xml version="1.0"?>
1922<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1923  <pattern id="P1">
1924    <rule context="root">
1925      <let name="x" value="42"/>
1926      <param name="debug" value="true"/>
1927      <assert test="true()">Test with let and param</assert>
1928    </rule>
1929  </pattern>
1930</schema>"#;
1931
1932        let result = schematron_parse(schema_xml);
1933        assert!(
1934            result.is_ok(),
1935            "Failed to parse schema with let/param: {:?}",
1936            result.err()
1937        );
1938        let schema = result.unwrap();
1939        assert_eq!(schema.rules.len(), 1);
1940    }
1941
1942    #[test]
1943    fn test_parse_schema_with_documentation() {
1944        let schema_xml = r#"<?xml version="1.0"?>
1945<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1946  <p>This is documentation</p>
1947  <caption>Table caption</caption>
1948  <pattern id="P1">
1949    <p>Pattern documentation</p>
1950    <rule context="root">
1951      <p>Rule documentation</p>
1952      <assert test="true()">Real assertion</assert>
1953    </rule>
1954  </pattern>
1955</schema>"#;
1956
1957        let result = schematron_parse(schema_xml);
1958        assert!(result.is_ok(), "Failed to parse schema with documentation");
1959        let schema = result.unwrap();
1960        assert_eq!(schema.rules.len(), 1);
1961    }
1962
1963    // ── Validation Tests ──────────────────────────────────────────────────
1964
1965    #[test]
1966    fn test_validate_assert_pass() {
1967        let schema_xml = r#"<?xml version="1.0"?>
1968<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1969  <pattern id="P1">
1970    <rule context="root">
1971      <assert test="true()">Always passes</assert>
1972    </rule>
1973  </pattern>
1974</schema>"#;
1975
1976        let doc_xml = r#"<?xml version="1.0"?>
1977<root>Hello</root>"#;
1978
1979        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
1980
1981        let doc = unsafe {
1982            crate::abi::exports_xml2::xmlReadMemory(
1983                doc_xml.as_ptr() as *const c_char,
1984                doc_xml.len() as c_int,
1985                b"test.xml\0".as_ptr() as *const c_char,
1986                ptr::null(),
1987                0,
1988            )
1989        };
1990        assert!(!doc.is_null(), "Failed to parse document");
1991
1992        let mut ctxt = SchematronValidCtxt::new();
1993        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
1994        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
1995
1996        assert!(valid, "Validation failed: {:?}", ctxt.errors);
1997    }
1998
1999    #[test]
2000    fn test_validate_assert_fail() {
2001        let schema_xml = r#"<?xml version="1.0"?>
2002<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2003  <pattern id="P1">
2004    <rule context="root">
2005      <assert test="false()">Always fails</assert>
2006    </rule>
2007  </pattern>
2008</schema>"#;
2009
2010        let doc_xml = r#"<?xml version="1.0"?>
2011<root>Hello</root>"#;
2012
2013        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2014
2015        let doc = unsafe {
2016            crate::abi::exports_xml2::xmlReadMemory(
2017                doc_xml.as_ptr() as *const c_char,
2018                doc_xml.len() as c_int,
2019                b"test.xml\0".as_ptr() as *const c_char,
2020                ptr::null(),
2021                0,
2022            )
2023        };
2024        assert!(!doc.is_null());
2025
2026        let mut ctxt = SchematronValidCtxt::new();
2027        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2028        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2029
2030        assert!(
2031            !valid,
2032            "Validation should have failed, errors: {:?}",
2033            ctxt.errors
2034        );
2035        assert!(ctxt.nb_errors > 0);
2036    }
2037
2038    #[test]
2039    fn test_validate_report_pass() {
2040        let schema_xml = r#"<?xml version="1.0"?>
2041<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2042  <pattern id="P1">
2043    <rule context="root">
2044      <report test="false()">Report should not trigger</report>
2045    </rule>
2046  </pattern>
2047</schema>"#;
2048
2049        let doc_xml = r#"<?xml version="1.0"?>
2050<root>Hello</root>"#;
2051
2052        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2053
2054        let doc = unsafe {
2055            crate::abi::exports_xml2::xmlReadMemory(
2056                doc_xml.as_ptr() as *const c_char,
2057                doc_xml.len() as c_int,
2058                b"test.xml\0".as_ptr() as *const c_char,
2059                ptr::null(),
2060                0,
2061            )
2062        };
2063        assert!(!doc.is_null());
2064
2065        let mut ctxt = SchematronValidCtxt::new();
2066        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2067        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2068
2069        assert!(valid, "Report should not trigger: {:?}", ctxt.errors);
2070    }
2071
2072    #[test]
2073    fn test_validate_report_fail() {
2074        let schema_xml = r#"<?xml version="1.0"?>
2075<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2076  <pattern id="P1">
2077    <rule context="root">
2078      <report test="true()">Report should trigger</report>
2079    </rule>
2080  </pattern>
2081</schema>"#;
2082
2083        let doc_xml = r#"<?xml version="1.0"?>
2084<root>Hello</root>"#;
2085
2086        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2087
2088        let doc = unsafe {
2089            crate::abi::exports_xml2::xmlReadMemory(
2090                doc_xml.as_ptr() as *const c_char,
2091                doc_xml.len() as c_int,
2092                b"test.xml\0".as_ptr() as *const c_char,
2093                ptr::null(),
2094                0,
2095            )
2096        };
2097        assert!(!doc.is_null());
2098
2099        let mut ctxt = SchematronValidCtxt::new();
2100        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2101        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2102
2103        assert!(!valid, "Report should have triggered");
2104        assert!(ctxt.nb_errors > 0);
2105    }
2106
2107    #[test]
2108    fn test_validate_context_matching() {
2109        let schema_xml = r#"<?xml version="1.0"?>
2110<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2111  <pattern id="P1">
2112    <rule context="child">
2113      <assert test="true()">Child matches</assert>
2114    </rule>
2115  </pattern>
2116</schema>"#;
2117
2118        let doc_xml = r#"<?xml version="1.0"?>
2119<root>
2120  <child>A</child>
2121  <child>B</child>
2122</root>"#;
2123
2124        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2125
2126        let doc = unsafe {
2127            crate::abi::exports_xml2::xmlReadMemory(
2128                doc_xml.as_ptr() as *const c_char,
2129                doc_xml.len() as c_int,
2130                b"test.xml\0".as_ptr() as *const c_char,
2131                ptr::null(),
2132                0,
2133            )
2134        };
2135        assert!(!doc.is_null());
2136
2137        let mut ctxt = SchematronValidCtxt::new();
2138        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2139        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2140
2141        assert!(valid, "Context matching failed: {:?}", ctxt.errors);
2142    }
2143
2144    #[test]
2145    fn test_validate_multiple_rules() {
2146        let schema_xml = r#"<?xml version="1.0"?>
2147<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2148  <pattern id="P1">
2149    <rule context="root">
2150      <assert test="true()">Root passes</assert>
2151    </rule>
2152  </pattern>
2153  <pattern id="P2">
2154    <rule context="child">
2155      <assert test="true()">Child passes</assert>
2156    </rule>
2157  </pattern>
2158</schema>"#;
2159
2160        let doc_xml = r#"<?xml version="1.0"?>
2161<root>
2162  <child>Content</child>
2163</root>"#;
2164
2165        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2166
2167        let doc = unsafe {
2168            crate::abi::exports_xml2::xmlReadMemory(
2169                doc_xml.as_ptr() as *const c_char,
2170                doc_xml.len() as c_int,
2171                b"test.xml\0".as_ptr() as *const c_char,
2172                ptr::null(),
2173                0,
2174            )
2175        };
2176        assert!(!doc.is_null());
2177
2178        let mut ctxt = SchematronValidCtxt::new();
2179        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2180        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2181
2182        assert!(valid, "Multiple rules failed: {:?}", ctxt.errors);
2183    }
2184
2185    #[test]
2186    fn test_validate_with_phase_filtering() {
2187        let schema_xml = r#"<?xml version="1.0"?>
2188<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
2189  <phase id="phaseA">
2190    <active pattern="P1"/>
2191  </phase>
2192  <phase id="phaseB">
2193    <active pattern="P2"/>
2194  </phase>
2195  <pattern id="P1">
2196    <rule context="root">
2197      <assert test="true()">Always passes</assert>
2198    </rule>
2199  </pattern>
2200  <pattern id="P2">
2201    <rule context="root">
2202      <assert test="false()">Always fails</assert>
2203    </rule>
2204  </pattern>
2205</schema>"#;
2206
2207        let doc_xml = r#"<?xml version="1.0"?>
2208<root>Hello</root>"#;
2209
2210        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2211
2212        let doc = unsafe {
2213            crate::abi::exports_xml2::xmlReadMemory(
2214                doc_xml.as_ptr() as *const c_char,
2215                doc_xml.len() as c_int,
2216                b"test.xml\0".as_ptr() as *const c_char,
2217                ptr::null(),
2218                0,
2219            )
2220        };
2221        assert!(!doc.is_null());
2222
2223        // Default phase (phaseA) should only include P1 which passes
2224        let mut ctxt = SchematronValidCtxt::new();
2225        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2226        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2227
2228        assert!(
2229            valid,
2230            "Phase filtering should make validation pass: {:?}",
2231            ctxt.errors
2232        );
2233    }
2234
2235    #[test]
2236    fn test_validate_no_rules() {
2237        let schema_xml = r#"<?xml version="1.0"?>
2238<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2239</schema>"#;
2240
2241        let doc_xml = r#"<?xml version="1.0"?>
2242<root>Hello</root>"#;
2243
2244        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2245
2246        let doc = unsafe {
2247            crate::abi::exports_xml2::xmlReadMemory(
2248                doc_xml.as_ptr() as *const c_char,
2249                doc_xml.len() as c_int,
2250                b"test.xml\0".as_ptr() as *const c_char,
2251                ptr::null(),
2252                0,
2253            )
2254        };
2255        assert!(!doc.is_null());
2256
2257        let mut ctxt = SchematronValidCtxt::new();
2258        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2259        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2260
2261        assert!(valid, "Empty schema should pass validation");
2262    }
2263
2264    #[test]
2265    fn test_validate_extends_resolution() {
2266        let schema_xml = r#"<?xml version="1.0"?>
2267<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2268  <pattern id="P1">
2269    <rule id="base" abstract="true" context="*">
2270      <assert test="true()">Base assertion</assert>
2271    </rule>
2272    <rule id="derived" context="root">
2273      <extends rule="base"/>
2274      <assert test="true()">Derived assertion</assert>
2275    </rule>
2276  </pattern>
2277</schema>"#;
2278
2279        let doc_xml = r#"<?xml version="1.0"?>
2280<root>Hello</root>"#;
2281
2282        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2283
2284        // Test extends resolution
2285        let resolved = schema.resolve_rule("derived");
2286        assert!(resolved.is_some());
2287        let resolved = resolved.unwrap();
2288        // The resolved rule should have patterns from both base and derived
2289        assert_eq!(
2290            resolved.patterns.len(),
2291            2,
2292            "Should have inherited the base pattern"
2293        );
2294
2295        let doc = unsafe {
2296            crate::abi::exports_xml2::xmlReadMemory(
2297                doc_xml.as_ptr() as *const c_char,
2298                doc_xml.len() as c_int,
2299                b"test.xml\0".as_ptr() as *const c_char,
2300                ptr::null(),
2301                0,
2302            )
2303        };
2304        assert!(!doc.is_null());
2305
2306        let mut ctxt = SchematronValidCtxt::new();
2307        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2308        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2309
2310        assert!(valid, "Extends resolution failed: {:?}", ctxt.errors);
2311    }
2312
2313    #[test]
2314    fn test_validate_assert_with_flag() {
2315        let schema_xml = r#"<?xml version="1.0"?>
2316<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2317  <pattern id="P1">
2318    <rule context="root">
2319      <assert test="false()" flag="warn">Warning message</assert>
2320    </rule>
2321  </pattern>
2322</schema>"#;
2323
2324        let doc_xml = r#"<?xml version="1.0"?>
2325<root>Hello</root>"#;
2326
2327        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2328
2329        let doc = unsafe {
2330            crate::abi::exports_xml2::xmlReadMemory(
2331                doc_xml.as_ptr() as *const c_char,
2332                doc_xml.len() as c_int,
2333                b"test.xml\0".as_ptr() as *const c_char,
2334                ptr::null(),
2335                0,
2336            )
2337        };
2338        assert!(!doc.is_null());
2339
2340        let mut ctxt = SchematronValidCtxt::new();
2341        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2342        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2343
2344        assert!(!valid);
2345        assert!(ctxt.nb_errors > 0);
2346        // The error message should include the flag
2347        assert!(
2348            ctxt.errors[0].contains("[warn]"),
2349            "Error should include flag"
2350        );
2351    }
2352
2353    // ── C ABI Lifecycle Tests ─────────────────────────────────────────────
2354
2355    #[test]
2356    fn test_c_abi_new_free_parser_ctxt() {
2357        let ctxt = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2358        assert!(!ctxt.is_null());
2359        unsafe { xmlSchematronFreeParserCtxt(ctxt) };
2360        // Should not crash
2361    }
2362
2363    #[test]
2364    fn test_c_abi_new_free_valid_ctxt() {
2365        let schema = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2366        assert!(!schema.is_null());
2367
2368        let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2369        assert!(!valid_ctxt.is_null());
2370
2371        unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2372        unsafe { xmlSchematronFreeParserCtxt(schema) };
2373        // Should not crash
2374    }
2375
2376    #[test]
2377    fn test_c_abi_parse_free() {
2378        let schema_xml = r#"<?xml version="1.0"?>
2379<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2380  <pattern id="P1">
2381    <rule context="root">
2382      <assert test="true()">Test</assert>
2383    </rule>
2384  </pattern>
2385</schema>"#;
2386
2387        let ctxt = unsafe {
2388            xmlSchematronNewMemParserCtxt(
2389                schema_xml.as_ptr() as *const c_char,
2390                schema_xml.len() as c_int,
2391            )
2392        };
2393        assert!(!ctxt.is_null());
2394
2395        let schema = unsafe { xmlSchematronParse(ctxt) };
2396        assert!(!schema.is_null());
2397
2398        unsafe { xmlSchematronFree(schema) };
2399        // Should not crash
2400    }
2401
2402    #[test]
2403    fn test_c_abi_validate_doc() {
2404        let schema_xml = r#"<?xml version="1.0"?>
2405<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2406  <pattern id="P1">
2407    <rule context="root">
2408      <assert test="true()">Always passes</assert>
2409    </rule>
2410  </pattern>
2411</schema>"#;
2412
2413        let doc_xml = r#"<?xml version="1.0"?>
2414<root>Hello</root>"#;
2415
2416        let ctxt = unsafe {
2417            xmlSchematronNewMemParserCtxt(
2418                schema_xml.as_ptr() as *const c_char,
2419                schema_xml.len() as c_int,
2420            )
2421        };
2422        assert!(!ctxt.is_null());
2423
2424        let schema = unsafe { xmlSchematronParse(ctxt) };
2425        assert!(!schema.is_null());
2426
2427        let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2428        assert!(!valid_ctxt.is_null());
2429
2430        let doc = unsafe {
2431            crate::abi::exports_xml2::xmlReadMemory(
2432                doc_xml.as_ptr() as *const c_char,
2433                doc_xml.len() as c_int,
2434                b"test.xml\0".as_ptr() as *const c_char,
2435                ptr::null(),
2436                0,
2437            )
2438        };
2439        assert!(!doc.is_null());
2440
2441        let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2442        assert_eq!(result, 0, "Validation should pass (return 0)");
2443
2444        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2445        unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2446        unsafe { xmlSchematronFree(schema) };
2447    }
2448
2449    #[test]
2450    fn test_c_abi_validate_fail() {
2451        let schema_xml = r#"<?xml version="1.0"?>
2452<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2453  <pattern id="P1">
2454    <rule context="root">
2455      <assert test="false()">Always fails</assert>
2456    </rule>
2457  </pattern>
2458</schema>"#;
2459
2460        let doc_xml = r#"<?xml version="1.0"?>
2461<root>Hello</root>"#;
2462
2463        let ctxt = unsafe {
2464            xmlSchematronNewMemParserCtxt(
2465                schema_xml.as_ptr() as *const c_char,
2466                schema_xml.len() as c_int,
2467            )
2468        };
2469        assert!(!ctxt.is_null());
2470
2471        let schema = unsafe { xmlSchematronParse(ctxt) };
2472        assert!(!schema.is_null());
2473
2474        let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2475        assert!(!valid_ctxt.is_null());
2476
2477        let doc = unsafe {
2478            crate::abi::exports_xml2::xmlReadMemory(
2479                doc_xml.as_ptr() as *const c_char,
2480                doc_xml.len() as c_int,
2481                b"test.xml\0".as_ptr() as *const c_char,
2482                ptr::null(),
2483                0,
2484            )
2485        };
2486        assert!(!doc.is_null());
2487
2488        let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2489        assert!(result > 0, "Validation should fail (return > 0)");
2490
2491        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2492        unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2493        unsafe { xmlSchematronFree(schema) };
2494    }
2495
2496    #[test]
2497    fn test_c_abi_null_handling() {
2498        // All free functions should handle NULL gracefully
2499        unsafe { xmlSchematronFree(ptr::null_mut()) };
2500        unsafe { xmlSchematronFreeParserCtxt(ptr::null_mut()) };
2501        unsafe { xmlSchematronFreeValidCtxt(ptr::null_mut()) };
2502
2503        // Parse with NULL should return NULL
2504        let result = unsafe { xmlSchematronParse(ptr::null_mut()) };
2505        assert!(result.is_null());
2506
2507        // Validate with NULL should return -1
2508        let result = unsafe { xmlSchematronValidateDoc(ptr::null_mut(), ptr::null_mut()) };
2509        assert_eq!(result, -1);
2510    }
2511
2512    // ── Edge Case Tests ───────────────────────────────────────────────────
2513
2514    #[test]
2515    fn test_validate_null_doc() {
2516        let schema = SchematronSchema::new();
2517        let mut ctxt = SchematronValidCtxt::new();
2518        let valid = unsafe { schematron_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
2519        assert!(!valid);
2520        assert!(ctxt.nb_errors > 0);
2521    }
2522
2523    #[test]
2524    fn test_active_rules_default_phase() {
2525        let mut schema = SchematronSchema::new();
2526
2527        let rule = SchematronRule::new("root".to_string());
2528        schema.rules.insert("r1".to_string(), rule);
2529
2530        schema
2531            .pattern_groups
2532            .insert("p1".to_string(), vec!["r1".to_string()]);
2533        schema.pattern_order.push("p1".to_string());
2534
2535        let rules = schema.active_rules(None);
2536        assert_eq!(rules.len(), 1);
2537    }
2538
2539    #[test]
2540    fn test_active_rules_unknown_phase() {
2541        let mut schema = SchematronSchema::new();
2542
2543        let rule = SchematronRule::new("root".to_string());
2544        schema.rules.insert("r1".to_string(), rule);
2545
2546        schema
2547            .pattern_groups
2548            .insert("p1".to_string(), vec!["r1".to_string()]);
2549        schema.pattern_order.push("p1".to_string());
2550
2551        let rules = schema.active_rules(Some("nonexistent"));
2552        assert_eq!(rules.len(), 1, "Unknown phase should use all patterns");
2553    }
2554
2555    #[test]
2556    fn test_parse_schema_with_span_and_emph() {
2557        let schema_xml = r#"<?xml version="1.0"?>
2558<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2559  <pattern id="P1">
2560    <rule context="root">
2561      <assert test="true()">Message with <span class="x">inline</span> and <emph>emphasis</emph></assert>
2562    </rule>
2563  </pattern>
2564</schema>"#;
2565
2566        let result = schematron_parse(schema_xml);
2567        assert!(
2568            result.is_ok(),
2569            "Failed to parse schema with span/emph: {:?}",
2570            result.err()
2571        );
2572        let schema = result.unwrap();
2573        let rule = schema.rules.values().next().unwrap();
2574        let pat = &rule.patterns[0];
2575        // The text should include the inline content of span and emph
2576        assert!(
2577            pat.text.contains("inline"),
2578            "Text should include span content"
2579        );
2580        assert!(
2581            pat.text.contains("emphasis"),
2582            "Text should include emph content"
2583        );
2584    }
2585
2586    #[test]
2587    fn test_schematron_pattern_new_assert() {
2588        let pat = SchematronPattern::new(
2589            SchematronPatternType::Assert,
2590            "true()".to_string(),
2591            "Test message".to_string(),
2592        );
2593        assert_eq!(pat.pattern_type, SchematronPatternType::Assert);
2594        assert_eq!(pat.test, "true()");
2595        assert_eq!(pat.text, "Test message");
2596        assert!(pat.compiled_test.is_some());
2597    }
2598
2599    #[test]
2600    fn test_schematron_pattern_new_report() {
2601        let pat = SchematronPattern::new(
2602            SchematronPatternType::Report,
2603            "false()".to_string(),
2604            "Report message".to_string(),
2605        );
2606        assert_eq!(pat.pattern_type, SchematronPatternType::Report);
2607        assert!(pat.compiled_test.is_some());
2608    }
2609
2610    #[test]
2611    fn test_schematron_rule_new() {
2612        let rule = SchematronRule::new("root".to_string());
2613        assert_eq!(rule.context, "root");
2614        assert!(rule.patterns.is_empty());
2615        assert!(!rule.abstract_);
2616    }
2617
2618    #[test]
2619    fn test_schematron_schema_new() {
2620        let schema = SchematronSchema::new();
2621        assert_eq!(schema.query_binding, "xslt");
2622        assert!(schema.rules.is_empty());
2623        assert!(schema.phases.is_empty());
2624        assert!(schema.ns.is_empty());
2625    }
2626
2627    #[test]
2628    fn test_schematron_valid_ctxt_new() {
2629        let ctxt = SchematronValidCtxt::new();
2630        assert!(ctxt.errors.is_empty());
2631        assert_eq!(ctxt.nb_errors, 0);
2632        assert!(ctxt.active_phase.is_none());
2633    }
2634
2635    #[test]
2636    fn test_validate_assert_with_child_count() {
2637        let schema_xml = r#"<?xml version="1.0"?>
2638<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2639  <pattern id="P1">
2640    <rule context="root">
2641      <assert test="count(*) > 0">Root must have at least one child element</assert>
2642    </rule>
2643  </pattern>
2644</schema>"#;
2645
2646        let doc_xml = r#"<?xml version="1.0"?>
2647<root>
2648  <child>Content</child>
2649</root>"#;
2650
2651        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2652
2653        let doc = unsafe {
2654            crate::abi::exports_xml2::xmlReadMemory(
2655                doc_xml.as_ptr() as *const c_char,
2656                doc_xml.len() as c_int,
2657                b"test.xml\0".as_ptr() as *const c_char,
2658                ptr::null(),
2659                0,
2660            )
2661        };
2662        assert!(!doc.is_null());
2663
2664        let mut ctxt = SchematronValidCtxt::new();
2665        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2666        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2667
2668        assert!(valid, "Child count check failed: {:?}", ctxt.errors);
2669    }
2670}