use crate::ir::{
    FallbackAnnotation, RuleFallbackAnnotation, SrcGenCompatibilitySide,
    SrcGenCompiledIndexRequirement, SrcGenCompiledSparqlRule, SrcGenComponent, SrcGenComponentKind,
    SrcGenIR, SrcGenLocalSetCompatibilityMode, SrcGenLoweredPropertyPath,
    SrcGenLoweredSparqlQueryKind, SrcGenMeta, SrcGenNodeShape, SrcGenPath, SrcGenPropertyShape,
    SrcGenRule, SrcGenRuleKind, SrcGenRuleObject, SrcGenRuleSubject, SrcGenSparqlBinding,
};
use oxigraph::model::Term;
use shifty::shacl_ir::{
    ComponentDescriptor, CustomConstraintComponentDefinition, ID, Path, PropShapeID, Rule,
    RuleCondition, RuleID, SPARQLValidator, ShapeIR, Target, TriplePatternTerm,
};
use shifty::sparql::{
    AdjacentPredicateWhitelistPlan, CompiledIndexRequirement, LocalSetCompatibilityMode,
    LocalSetCompatibilityPlan, LoweredPropertyPath, LoweredSparqlQueryKind, MissingRelatedNodePlan,
    RequiredPathSupportPlan, SparqlExecutor, compiled_sparql_index_requirements,
    compiled_sparql_rule, lowered_sparql_query_kind,
};
use std::collections::{BTreeMap, HashMap, HashSet};

fn srcgen_lowered_path(path: &LoweredPropertyPath) -> SrcGenLoweredPropertyPath {
    match path {
        LoweredPropertyPath::SelfNode => SrcGenLoweredPropertyPath::SelfNode,
        LoweredPropertyPath::NamedNode(predicate) => SrcGenLoweredPropertyPath::NamedNode {
            predicate_iri: predicate.as_str().to_string(),
        },
        LoweredPropertyPath::ReverseNamedNode(predicate) => {
            SrcGenLoweredPropertyPath::ReverseNamedNode {
                predicate_iri: predicate.as_str().to_string(),
            }
        }
        LoweredPropertyPath::ZeroOrOne(inner) => SrcGenLoweredPropertyPath::ZeroOrOne {
            inner: Box::new(srcgen_lowered_path(inner.as_ref())),
        },
        LoweredPropertyPath::ZeroOrMore(inner) => SrcGenLoweredPropertyPath::ZeroOrMore {
            inner: Box::new(srcgen_lowered_path(inner.as_ref())),
        },
        LoweredPropertyPath::OneOrMore(inner) => SrcGenLoweredPropertyPath::OneOrMore {
            inner: Box::new(srcgen_lowered_path(inner.as_ref())),
        },
        LoweredPropertyPath::Sequence(items) => SrcGenLoweredPropertyPath::Sequence {
            items: items.iter().map(srcgen_lowered_path).collect(),
        },
        LoweredPropertyPath::Alternative(items) => SrcGenLoweredPropertyPath::Alternative {
            items: items.iter().map(srcgen_lowered_path).collect(),
        },
    }
}

fn srcgen_local_set_compatibility_mode(
    mode: &LocalSetCompatibilityMode,
) -> SrcGenLocalSetCompatibilityMode {
    match mode {
        LocalSetCompatibilityMode::PurePure => SrcGenLocalSetCompatibilityMode::PurePure,
        LocalSetCompatibilityMode::CompositeVsPure { composite_side } => {
            SrcGenLocalSetCompatibilityMode::CompositeVsPure {
                composite_side: match composite_side {
                    shifty::sparql::CompatibilitySide::Left => SrcGenCompatibilitySide::Left,
                    shifty::sparql::CompatibilitySide::Right => SrcGenCompatibilitySide::Right,
                },
            }
        }
        LocalSetCompatibilityMode::CompositeVsComposite => {
            SrcGenLocalSetCompatibilityMode::CompositeVsComposite
        }
    }
}

fn srcgen_compiled_rule(rule: &shifty::sparql::CompiledSparqlRule) -> SrcGenCompiledSparqlRule {
    match rule {
        shifty::sparql::CompiledSparqlRule::PathCopy {
            construct_predicate,
            source_path,
        } => SrcGenCompiledSparqlRule::PathCopy {
            construct_predicate_iri: construct_predicate.as_str().to_string(),
            source_path: srcgen_lowered_path(source_path),
        },
        shifty::sparql::CompiledSparqlRule::EqualityConstant {
            construct_predicate,
            left_path,
            right_path,
            object,
        } => SrcGenCompiledSparqlRule::EqualityConstant {
            construct_predicate_iri: construct_predicate.as_str().to_string(),
            left_path: srcgen_lowered_path(left_path),
            right_path: srcgen_lowered_path(right_path),
            object: object.clone(),
        },
    }
}

fn srcgen_compiled_index_requirement(
    requirement: &CompiledIndexRequirement,
) -> SrcGenCompiledIndexRequirement {
    match requirement {
        CompiledIndexRequirement::OutgoingValues { predicate } => {
            SrcGenCompiledIndexRequirement::OutgoingValues {
                predicate_iri: predicate.as_str().to_string(),
            }
        }
        CompiledIndexRequirement::IncomingValues { predicate } => {
            SrcGenCompiledIndexRequirement::IncomingValues {
                predicate_iri: predicate.as_str().to_string(),
            }
        }
    }
}

fn srcgen_lowered_query_kind(query: &str, prefixes: &str) -> Option<SrcGenLoweredSparqlQueryKind> {
    let full_query = if prefixes.trim().is_empty() {
        query.to_string()
    } else {
        format!("{prefixes}\n{query}")
    };
    let algebra = shifty::sparql::SparqlServices::new()
        .algebra(&full_query)
        .ok()?;
    match lowered_sparql_query_kind(&algebra)? {
        LoweredSparqlQueryKind::AdjacentPredicateWhitelist(AdjacentPredicateWhitelistPlan {
            anchor_path,
            allowed_predicates,
        }) => Some(SrcGenLoweredSparqlQueryKind::AdjacentPredicateWhitelist {
            anchor_path: srcgen_lowered_path(&anchor_path),
            allowed_predicate_iris: allowed_predicates
                .iter()
                .map(|predicate| predicate.as_str().to_string())
                .collect(),
        }),
        LoweredSparqlQueryKind::PathValueEqualsConstant(
            shifty::sparql::PathValueEqualsConstantPlan {
                value_path,
                expected_value,
            },
        ) => Some(SrcGenLoweredSparqlQueryKind::PathValueEqualsConstant {
            value_path: srcgen_lowered_path(&value_path),
            expected_value,
        }),
        LoweredSparqlQueryKind::MissingRelatedNode(MissingRelatedNodePlan {
            related_path,
            related_variable,
            related_class,
            required_path,
        }) => Some(SrcGenLoweredSparqlQueryKind::MissingRelatedNode {
            related_path: srcgen_lowered_path(&related_path),
            related_variable,
            related_class,
            required_path: srcgen_lowered_path(&required_path),
        }),
        LoweredSparqlQueryKind::RequiredPathSupport(RequiredPathSupportPlan {
            antecedent_path,
            support_path,
            ..
        }) => {
            let SrcGenLoweredPropertyPath::NamedNode { predicate_iri } =
                srcgen_lowered_path(&antecedent_path)
            else {
                return None;
            };
            Some(SrcGenLoweredSparqlQueryKind::RequiredPathSupport {
                source_predicate_iri: predicate_iri,
                required_path: srcgen_lowered_path(&support_path),
            })
        }
        LoweredSparqlQueryKind::LocalSetCompatibility(boxed_plan) => {
            let LocalSetCompatibilityPlan {
                left_anchor_path,
                right_anchor_path,
                left_anchor_var,
                right_anchor_var,
                left_class,
                right_class,
                left_value_path,
                right_value_path,
                left_value_var,
                right_value_var,
                distinct_anchors,
                composed_of_predicate,
                constituent_path,
                mode,
            } = *boxed_plan;
            Some(SrcGenLoweredSparqlQueryKind::LocalSetCompatibility {
                left_anchor_path: srcgen_lowered_path(&left_anchor_path),
                right_anchor_path: srcgen_lowered_path(&right_anchor_path),
                left_anchor_var,
                right_anchor_var,
                left_class,
                right_class,
                left_value_path: srcgen_lowered_path(&left_value_path),
                right_value_path: srcgen_lowered_path(&right_value_path),
                left_value_var,
                right_value_var,
                distinct_anchors,
                composed_of_predicate_iri: composed_of_predicate.as_str().to_string(),
                constituent_path: constituent_path.as_ref().map(srcgen_lowered_path),
                mode: srcgen_local_set_compatibility_mode(&mode),
            })
        }
    }
}

pub fn lower_shape_ir(shape_ir: &ShapeIR) -> Result<SrcGenIR, String> {
    let mut component_ids: Vec<u64> = shape_ir.components.keys().map(|id| id.0).collect();
    component_ids.sort_unstable();

    let mut components = Vec::with_capacity(component_ids.len());
    let mut component_kinds: HashMap<u64, SrcGenComponentKind> =
        HashMap::with_capacity(component_ids.len());
    let mut fallback_annotations = Vec::new();

    for component_id in component_ids {
        let descriptor = shape_ir
            .components
            .get(&shifty::shacl_ir::ComponentID(component_id))
            .ok_or_else(|| format!("missing component {component_id} in ShapeIR"))?;

        let (kind, fallback_reason) = component_kind(shape_ir, descriptor);
        let fallback_only = fallback_reason.is_some();
        let iri = component_constraint_component_iri(descriptor).to_string();
        component_kinds.insert(component_id, kind.clone());
        components.push(SrcGenComponent {
            id: component_id,
            iri,
            kind,
            fallback_only,
        });

        if let Some(reason) = fallback_reason {
            fallback_annotations.push(FallbackAnnotation {
                component_id,
                reason,
            });
        }
    }

    let mut next_synthetic_component_id = component_kinds.keys().copied().max().unwrap_or(0) + 1;
    let mut expression_component_id_by_shape: HashMap<ID, u64> = HashMap::new();
    let mut expression_unsupported_shapes: HashSet<ID> = HashSet::new();
    let mut ordered_node_shape_ids: Vec<ID> =
        shape_ir.node_shapes.iter().map(|shape| shape.id).collect();
    ordered_node_shape_ids.sort_by_key(|id| id.0);
    for shape_id in ordered_node_shape_ids {
        match node_shape_expression_constraint_support(shape_ir, shape_id) {
            ExpressionConstraintSupport::None => {}
            ExpressionConstraintSupport::This => {
                let component_id = next_synthetic_component_id;
                next_synthetic_component_id += 1;
                component_kinds.insert(component_id, SrcGenComponentKind::ExpressionThis);
                components.push(SrcGenComponent {
                    id: component_id,
                    iri: "http://www.w3.org/ns/shacl#ExpressionConstraintComponent".to_string(),
                    kind: SrcGenComponentKind::ExpressionThis,
                    fallback_only: false,
                });
                expression_component_id_by_shape.insert(shape_id, component_id);
            }
            ExpressionConstraintSupport::Unsupported => {
                expression_unsupported_shapes.insert(shape_id);
            }
        }
    }

    let mut pending_property_shapes: Vec<PendingPropertyShape> = shape_ir
        .property_shapes
        .iter()
        .map(|shape| {
            let iri = shape_ir
                .property_shape_terms
                .get(&shape.id)
                .map(term_to_stable_string)
                .unwrap_or_else(|| format!("_:property-shape-{}", shape.id.0));
            let lowered_path = lower_supported_property_path(&shape.path);
            let (path, path_sparql, path_supported) = if let Some(path) = lowered_path {
                let path_sparql = shape
                    .path
                    .to_sparql_path()
                    .unwrap_or_else(|_| String::new());
                (path, path_sparql, true)
            } else {
                (
                    SrcGenPath::SimplePredicate {
                        predicate_iri: "urn:shifty:unsupported-path".to_string(),
                    },
                    String::new(),
                    false,
                )
            };
            let path_predicate = simple_named_path_predicate(&shape.path);
            let mut constraints: Vec<u64> = shape.constraints.iter().map(|id| id.0).collect();
            constraints.sort_unstable();
            let lowered_targets = lower_supported_node_targets(shape_ir, &shape.targets, false);
            let (
                target_classes,
                target_nodes,
                target_subjects_of,
                target_objects_of,
                targets_supported,
            ) = if let Some(targets) = lowered_targets {
                let has_supported_targets = !targets.target_classes.is_empty()
                    || !targets.target_nodes.is_empty()
                    || !targets.target_subjects_of.is_empty()
                    || !targets.target_objects_of.is_empty();
                (
                    targets.target_classes,
                    targets.target_nodes,
                    targets.target_subjects_of,
                    targets.target_objects_of,
                    has_supported_targets,
                )
            } else {
                (Vec::new(), Vec::new(), Vec::new(), Vec::new(), false)
            };

            let base_supported = if shape.deactivated {
                // Deactivated shapes are semantic no-ops and should not force runtime fallback.
                true
            } else {
                path_supported && (targets_supported || shape.targets.is_empty())
            };
            let mut supported_constraints = Vec::new();
            let mut fallback_constraints = Vec::new();
            if shape.deactivated {
                constraints.clear();
            } else {
                for component_id in &constraints {
                    let supported_component = component_kinds
                        .get(component_id)
                        .map(property_constraint_supported)
                        .unwrap_or(false);
                    if base_supported && supported_component {
                        supported_constraints.push(*component_id);
                    } else {
                        fallback_constraints.push(*component_id);
                    }
                }
            }

            PendingPropertyShape {
                original_id: shape.id,
                iri,
                target_classes,
                target_nodes,
                target_subjects_of,
                target_objects_of,
                path,
                path_term: shape.path_term.clone(),
                path_sparql,
                path_predicate,
                constraints,
                supported_constraints,
                fallback_constraints,
                supported: base_supported,
            }
        })
        .collect();

    pending_property_shapes.sort_by(|a, b| {
        a.iri
            .cmp(&b.iri)
            .then_with(|| a.original_id.0.cmp(&b.original_id.0))
    });

    let mut pending_node_shapes: Vec<PendingNodeShape> = shape_ir
        .node_shapes
        .iter()
        .map(|shape| {
            let iri = shape_ir
                .node_shape_terms
                .get(&shape.id)
                .map(term_to_stable_string)
                .unwrap_or_else(|| format!("_:node-shape-{}", shape.id.0));
            let mut constraints: Vec<u64> = shape.constraints.iter().map(|id| id.0).collect();
            constraints.sort_unstable();
            let mut property_shapes = shape.property_shapes.clone();
            property_shapes.sort_by_key(|id| id.0);
            if let Some(component_id) = expression_component_id_by_shape.get(&shape.id) {
                constraints.push(*component_id);
                constraints.sort_unstable();
            }
            let expression_supported = !expression_unsupported_shapes.contains(&shape.id);

            let lowered_targets = lower_supported_node_targets(shape_ir, &shape.targets, true);
            let (
                target_classes,
                target_nodes,
                target_subjects_of,
                target_objects_of,
                target_advanced_select_queries,
                targets_supported,
            ) = if let Some(targets) = lowered_targets {
                let has_supported_targets = !targets.target_classes.is_empty()
                    || !targets.target_nodes.is_empty()
                    || !targets.target_subjects_of.is_empty()
                    || !targets.target_objects_of.is_empty()
                    || !targets.target_advanced_select_queries.is_empty();
                (
                    targets.target_classes,
                    targets.target_nodes,
                    targets.target_subjects_of,
                    targets.target_objects_of,
                    targets.target_advanced_select_queries,
                    has_supported_targets,
                )
            } else {
                (
                    Vec::new(),
                    Vec::new(),
                    Vec::new(),
                    Vec::new(),
                    Vec::new(),
                    false,
                )
            };
            let base_supported = if shape.deactivated {
                // Deactivated shapes are semantic no-ops and should not force runtime fallback.
                true
            } else {
                (targets_supported || shape.targets.is_empty()) && expression_supported
            };
            let mut supported_constraints = Vec::new();
            let mut fallback_constraints = Vec::new();
            if shape.deactivated {
                constraints.clear();
                property_shapes.clear();
            } else {
                for component_id in &constraints {
                    let Some(kind) = component_kinds.get(component_id) else {
                        fallback_constraints.push(*component_id);
                        continue;
                    };
                    let kind_supported = node_constraint_supported(kind);
                    let context_supported = match kind {
                        SrcGenComponentKind::Sparql { requires_path, .. } => !requires_path,
                        SrcGenComponentKind::CustomSparql { requires_path, .. } => !requires_path,
                        _ => true,
                    };
                    if base_supported && kind_supported && context_supported {
                        supported_constraints.push(*component_id);
                    } else {
                        fallback_constraints.push(*component_id);
                    }
                }
            }

            PendingNodeShape {
                original_id: shape.id,
                iri,
                target_classes,
                target_nodes,
                target_subjects_of,
                target_objects_of,
                target_advanced_select_queries,
                constraints,
                supported_constraints,
                fallback_constraints,
                property_shapes,
                supported: base_supported,
            }
        })
        .collect();

    pending_node_shapes.sort_by(|a, b| {
        a.iri
            .cmp(&b.iri)
            .then_with(|| a.original_id.0.cmp(&b.original_id.0))
    });

    let mut node_id_by_original: HashMap<ID, u64> =
        HashMap::with_capacity(pending_node_shapes.len());
    let mut property_id_by_original: HashMap<PropShapeID, u64> =
        HashMap::with_capacity(pending_property_shapes.len());

    for (index, node) in pending_node_shapes.iter().enumerate() {
        node_id_by_original.insert(node.original_id, (index + 1) as u64);
    }
    let property_offset = pending_node_shapes.len() as u64;
    for (index, property) in pending_property_shapes.iter().enumerate() {
        property_id_by_original.insert(property.original_id, property_offset + (index as u64) + 1);
    }

    let property_shapes: Vec<SrcGenPropertyShape> = pending_property_shapes
        .into_iter()
        .filter_map(|shape| {
            property_id_by_original
                .get(&shape.original_id)
                .copied()
                .map(|id| SrcGenPropertyShape {
                    id,
                    iri: shape.iri,
                    target_classes: shape.target_classes,
                    target_nodes: shape.target_nodes,
                    target_subjects_of: shape.target_subjects_of,
                    target_objects_of: shape.target_objects_of,
                    path: shape.path,
                    path_term: shape.path_term,
                    path_sparql: shape.path_sparql,
                    path_predicate: shape.path_predicate,
                    constraints: shape.constraints,
                    supported_constraints: shape.supported_constraints,
                    fallback_constraints: shape.fallback_constraints,
                    supported: shape.supported,
                })
        })
        .collect();

    let property_support_by_id: HashMap<u64, bool> = property_shapes
        .iter()
        .map(|shape| (shape.id, shape.supported))
        .collect();

    let node_shapes: Vec<SrcGenNodeShape> = pending_node_shapes
        .into_iter()
        .filter_map(|shape| {
            let id = node_id_by_original.get(&shape.original_id).copied()?;
            let mut property_shapes: Vec<u64> = shape
                .property_shapes
                .iter()
                .filter_map(|prop_id| property_id_by_original.get(prop_id).copied())
                .collect();
            property_shapes.sort_unstable();

            let any_linked_properties_supported = property_shapes.iter().any(|prop_id| {
                property_support_by_id
                    .get(prop_id)
                    .copied()
                    .unwrap_or(false)
            });
            let is_noop_shape = shape.constraints.is_empty() && property_shapes.is_empty();

            let supported = shape.supported
                && (is_noop_shape
                    || !shape.supported_constraints.is_empty()
                    || any_linked_properties_supported);

            Some(SrcGenNodeShape {
                id,
                iri: shape.iri,
                target_classes: shape.target_classes,
                target_nodes: shape.target_nodes,
                target_subjects_of: shape.target_subjects_of,
                target_objects_of: shape.target_objects_of,
                target_advanced_select_queries: shape.target_advanced_select_queries,
                constraints: shape.constraints,
                supported_constraints: shape.supported_constraints,
                fallback_constraints: shape.fallback_constraints,
                property_shapes,
                supported,
            })
        })
        .collect();

    let specialization_ready = components.iter().all(|component| !component.fallback_only)
        && !node_shapes.is_empty()
        && node_shapes.iter().all(|shape| {
            shape.supported
                && shape.fallback_constraints.is_empty()
                && shape.property_shapes.iter().all(|property_id| {
                    property_support_by_id
                        .get(property_id)
                        .copied()
                        .unwrap_or(false)
                })
        })
        && property_shapes
            .iter()
            .all(|shape| shape.supported && shape.fallback_constraints.is_empty());

    let data_graph_iri = shape_ir
        .data_graph
        .as_ref()
        .map(|iri| iri.as_str().to_string())
        .unwrap_or_default();

    let mut rule_ids: Vec<u64> = shape_ir.rules.keys().map(|id| id.0).collect();
    rule_ids.sort_unstable();

    let mut rules: Vec<SrcGenRule> = Vec::new();
    let mut rule_fallback_annotations: Vec<RuleFallbackAnnotation> = Vec::new();
    for rule_id_value in rule_ids {
        let rule_id = RuleID(rule_id_value);
        let Some(rule) = shape_ir.rules.get(&rule_id) else {
            continue;
        };
        if rule.is_deactivated() {
            continue;
        }

        let (kind, targets, fallback_reason) = match rule_targets(shape_ir, rule_id) {
            Ok(targets) => {
                let (kind, reason) = rule_kind(shape_ir, rule, &targets);
                (kind, targets, reason)
            }
            Err(reason) => (
                SrcGenRuleKind::Unsupported {
                    kind: format!("{}(targets)", rule_kind_name(rule)),
                },
                LoweredRuleTargets::default(),
                Some(reason),
            ),
        };

        let fallback_only = fallback_reason.is_some();
        rules.push(SrcGenRule {
            id: rule_id_value,
            target_classes: targets.target_classes,
            target_nodes: targets.target_nodes,
            target_subjects_of: targets.target_subjects_of,
            target_objects_of: targets.target_objects_of,
            target_advanced_select_queries: targets.target_advanced_select_queries,
            kind,
            fallback_only,
        });
        if let Some(reason) = fallback_reason {
            rule_fallback_annotations.push(RuleFallbackAnnotation {
                rule_id: rule_id_value,
                reason,
            });
        }
    }

    let active_rule_count = rules.len();
    let embedded_shape_ir_bincode =
        bincode::serde::encode_to_vec(shape_ir, bincode::config::standard()).map_err(|err| {
            format!("failed to serialize ShapeIR for embedded fallback payload: {err}")
        })?;

    Ok(SrcGenIR {
        meta: SrcGenMeta {
            compiler_track: "srcgen".to_string(),
            schema_version: 1,
            shape_graph_iri: shape_ir.shape_graph.as_str().to_string(),
            data_graph_iri,
            rule_count: active_rule_count,
            specialization_ready,
        },
        node_shapes,
        property_shapes,
        components,
        rules,
        fallback_annotations,
        rule_fallback_annotations,
        embedded_shape_ir_bincode,
    })
}

#[derive(Debug)]
struct PendingNodeShape {
    original_id: ID,
    iri: String,
    target_classes: Vec<String>,
    target_nodes: Vec<Term>,
    target_subjects_of: Vec<String>,
    target_objects_of: Vec<String>,
    target_advanced_select_queries: Vec<String>,
    constraints: Vec<u64>,
    supported_constraints: Vec<u64>,
    fallback_constraints: Vec<u64>,
    property_shapes: Vec<PropShapeID>,
    supported: bool,
}

#[derive(Debug)]
struct PendingPropertyShape {
    original_id: PropShapeID,
    iri: String,
    target_classes: Vec<String>,
    target_nodes: Vec<Term>,
    target_subjects_of: Vec<String>,
    target_objects_of: Vec<String>,
    path: SrcGenPath,
    path_term: Term,
    path_sparql: String,
    path_predicate: Option<String>,
    constraints: Vec<u64>,
    supported_constraints: Vec<u64>,
    fallback_constraints: Vec<u64>,
    supported: bool,
}

#[derive(Debug, Default)]
struct LoweredNodeTargets {
    target_classes: Vec<String>,
    target_nodes: Vec<Term>,
    target_subjects_of: Vec<String>,
    target_objects_of: Vec<String>,
    target_advanced_select_queries: Vec<String>,
}

#[derive(Debug, Default)]
struct LoweredRuleTargets {
    target_classes: Vec<String>,
    target_nodes: Vec<Term>,
    target_subjects_of: Vec<String>,
    target_objects_of: Vec<String>,
    target_advanced_select_queries: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExpressionConstraintSupport {
    None,
    This,
    Unsupported,
}

fn lower_supported_node_targets(
    shape_ir: &ShapeIR,
    targets: &[Target],
    allow_advanced: bool,
) -> Option<LoweredNodeTargets> {
    let mut lowered = LoweredNodeTargets::default();

    for target in targets {
        match target {
            Target::Class(Term::NamedNode(class)) => {
                lowered.target_classes.push(class.as_str().to_string());
            }
            Target::Node(term) => lowered.target_nodes.push(term.clone()),
            Target::SubjectsOf(Term::NamedNode(predicate)) => {
                lowered
                    .target_subjects_of
                    .push(predicate.as_str().to_string());
            }
            Target::ObjectsOf(Term::NamedNode(predicate)) => {
                lowered
                    .target_objects_of
                    .push(predicate.as_str().to_string());
            }
            Target::Advanced(selector) if allow_advanced => {
                let query = resolve_advanced_target_select_query(shape_ir, selector)?;
                lowered.target_advanced_select_queries.push(query);
            }
            _ => return None,
        }
    }

    lowered.target_classes.sort();
    lowered.target_classes.dedup();

    lowered.target_nodes.sort_by_key(term_to_stable_string);
    lowered.target_nodes.dedup();

    lowered.target_subjects_of.sort();
    lowered.target_subjects_of.dedup();

    lowered.target_objects_of.sort();
    lowered.target_objects_of.dedup();

    lowered.target_advanced_select_queries.sort();
    lowered.target_advanced_select_queries.dedup();

    Some(lowered)
}

fn lower_supported_rule_targets(
    shape_ir: &ShapeIR,
    targets: &[Target],
) -> Option<LoweredRuleTargets> {
    let mut lowered = LoweredRuleTargets::default();

    for target in targets {
        match target {
            Target::Class(Term::NamedNode(class)) => {
                lowered.target_classes.push(class.as_str().to_string());
            }
            Target::Node(term) => lowered.target_nodes.push(term.clone()),
            Target::SubjectsOf(Term::NamedNode(predicate)) => {
                lowered
                    .target_subjects_of
                    .push(predicate.as_str().to_string());
            }
            Target::ObjectsOf(Term::NamedNode(predicate)) => {
                lowered
                    .target_objects_of
                    .push(predicate.as_str().to_string());
            }
            Target::Advanced(selector) => {
                let query = resolve_advanced_target_select_query(shape_ir, selector)?;
                lowered.target_advanced_select_queries.push(query);
            }
            _ => return None,
        }
    }

    lowered.target_classes.sort();
    lowered.target_classes.dedup();

    lowered.target_nodes.sort_by_key(term_to_stable_string);
    lowered.target_nodes.dedup();

    lowered.target_subjects_of.sort();
    lowered.target_subjects_of.dedup();

    lowered.target_objects_of.sort();
    lowered.target_objects_of.dedup();

    lowered.target_advanced_select_queries.sort();
    lowered.target_advanced_select_queries.dedup();

    Some(lowered)
}

fn resolve_advanced_target_select_query(shape_ir: &ShapeIR, selector: &Term) -> Option<String> {
    const SH_SELECT: &str = "http://www.w3.org/ns/shacl#select";
    let selector_key = term_to_stable_string(selector);
    let mut fallback_select: Option<String> = None;
    for quad in &shape_ir.shape_quads {
        if quad.predicate.as_str() != SH_SELECT {
            continue;
        }
        let Term::Literal(literal) = &quad.object else {
            continue;
        };
        let quad_subject_key = term_to_stable_string(&Term::from(quad.subject.clone()));
        if quad_subject_key == selector_key {
            return Some(literal.value().to_string());
        }
        if fallback_select.is_none() {
            fallback_select = Some(literal.value().to_string());
        }
    }
    fallback_select
}

fn node_shape_expression_constraint_support(
    shape_ir: &ShapeIR,
    shape_id: ID,
) -> ExpressionConstraintSupport {
    const SH_EXPRESSION: &str = "http://www.w3.org/ns/shacl#expression";
    const SH_THIS: &str = "http://www.w3.org/ns/shacl#this";

    let Some(shape_term) = shape_ir.node_shape_terms.get(&shape_id) else {
        return ExpressionConstraintSupport::None;
    };
    let shape_term_key = term_to_stable_string(shape_term);
    let mut saw_expression = false;
    let mut supported = true;

    for quad in &shape_ir.shape_quads {
        if quad.predicate.as_str() != SH_EXPRESSION {
            continue;
        }
        let subject_key = term_to_stable_string(&Term::from(quad.subject.clone()));
        if subject_key != shape_term_key {
            continue;
        }
        saw_expression = true;
        match &quad.object {
            Term::NamedNode(named_node) if named_node.as_str() == SH_THIS => {}
            _ => {
                supported = false;
                break;
            }
        }
    }

    if !saw_expression {
        ExpressionConstraintSupport::None
    } else if supported {
        ExpressionConstraintSupport::This
    } else {
        ExpressionConstraintSupport::Unsupported
    }
}

fn simple_named_path_predicate(path: &Path) -> Option<String> {
    match path {
        Path::Simple(Term::NamedNode(predicate)) => Some(predicate.as_str().to_string()),
        _ => None,
    }
}

fn lower_supported_property_path(path: &Path) -> Option<SrcGenPath> {
    match path {
        Path::Simple(Term::NamedNode(predicate)) => Some(SrcGenPath::SimplePredicate {
            predicate_iri: predicate.as_str().to_string(),
        }),
        Path::Simple(_) => None,
        Path::Inverse(inner) => Some(SrcGenPath::Inverse {
            inner: Box::new(lower_supported_property_path(inner)?),
        }),
        Path::Sequence(items) => {
            if items.is_empty() {
                return None;
            }
            let lowered: Option<Vec<SrcGenPath>> =
                items.iter().map(lower_supported_property_path).collect();
            lowered.map(|items| SrcGenPath::Sequence { items })
        }
        Path::Alternative(items) => {
            if items.is_empty() {
                return None;
            }
            let lowered: Option<Vec<SrcGenPath>> =
                items.iter().map(lower_supported_property_path).collect();
            lowered.map(|items| SrcGenPath::Alternative { items })
        }
        Path::ZeroOrMore(inner) => Some(SrcGenPath::ZeroOrMore {
            inner: Box::new(lower_supported_property_path(inner)?),
        }),
        Path::OneOrMore(inner) => Some(SrcGenPath::OneOrMore {
            inner: Box::new(lower_supported_property_path(inner)?),
        }),
        Path::ZeroOrOne(inner) => Some(SrcGenPath::ZeroOrOne {
            inner: Box::new(lower_supported_property_path(inner)?),
        }),
    }
}

fn rule_targets(shape_ir: &ShapeIR, rule_id: RuleID) -> Result<LoweredRuleTargets, String> {
    if shape_ir
        .prop_shape_rules
        .values()
        .any(|rule_ids| rule_ids.contains(&rule_id))
    {
        return Err(format!(
            "Rule {} is attached to a property shape and property-shape rule specialization is not yet supported",
            rule_id.0
        ));
    }

    let mut targets = LoweredRuleTargets::default();
    let mut matched = false;
    for node_shape in &shape_ir.node_shapes {
        let Some(rule_ids) = shape_ir.node_shape_rules.get(&node_shape.id) else {
            continue;
        };
        if !rule_ids.contains(&rule_id) {
            continue;
        }
        matched = true;
        if node_shape.deactivated {
            return Err(format!(
                "Rule {} is attached to a deactivated node shape",
                rule_id.0
            ));
        }
        let Some(node_targets) = lower_supported_rule_targets(shape_ir, &node_shape.targets) else {
            return Err(format!(
                "Rule {} has unsupported targets that are not yet specialized",
                rule_id.0
            ));
        };
        if node_targets.target_classes.is_empty()
            && node_targets.target_nodes.is_empty()
            && node_targets.target_subjects_of.is_empty()
            && node_targets.target_objects_of.is_empty()
            && node_targets.target_advanced_select_queries.is_empty()
        {
            return Err(format!(
                "Rule {} has no specialized targets to drive focus-node inference",
                rule_id.0
            ));
        }

        targets.target_classes.extend(node_targets.target_classes);
        targets.target_nodes.extend(node_targets.target_nodes);
        targets
            .target_subjects_of
            .extend(node_targets.target_subjects_of);
        targets
            .target_objects_of
            .extend(node_targets.target_objects_of);
        targets
            .target_advanced_select_queries
            .extend(node_targets.target_advanced_select_queries);
    }

    if !matched {
        return Err(format!(
            "Rule {} is not attached to any specialized node-shape target",
            rule_id.0
        ));
    }

    targets.target_classes.sort();
    targets.target_classes.dedup();

    targets.target_nodes.sort_by_key(term_to_stable_string);
    targets.target_nodes.dedup();

    targets.target_subjects_of.sort();
    targets.target_subjects_of.dedup();

    targets.target_objects_of.sort();
    targets.target_objects_of.dedup();

    targets.target_advanced_select_queries.sort();
    targets.target_advanced_select_queries.dedup();

    if targets.target_classes.is_empty()
        && targets.target_nodes.is_empty()
        && targets.target_subjects_of.is_empty()
        && targets.target_objects_of.is_empty()
        && targets.target_advanced_select_queries.is_empty()
    {
        return Err(format!("Rule {} has no specialized targets", rule_id.0));
    }
    Ok(targets)
}

fn rule_kind_name(rule: &Rule) -> &'static str {
    match rule {
        Rule::Triple(_) => "TripleRule",
        Rule::Sparql(_) => "SPARQLRule",
    }
}

fn rule_kind(
    shape_ir: &ShapeIR,
    rule: &Rule,
    targets: &LoweredRuleTargets,
) -> (SrcGenRuleKind, Option<String>) {
    let has_specialized_targets = !targets.target_classes.is_empty()
        || !targets.target_nodes.is_empty()
        || !targets.target_subjects_of.is_empty()
        || !targets.target_objects_of.is_empty()
        || !targets.target_advanced_select_queries.is_empty();
    if !has_specialized_targets {
        return (
            SrcGenRuleKind::Unsupported {
                kind: format!("{}(no-targets)", rule_kind_name(rule)),
            },
            Some(format!(
                "{} has no specialized targets",
                rule_kind_name(rule)
            )),
        );
    }

    match rule {
        Rule::Sparql(rule) => {
            let condition_shape_iris =
                match collect_rule_condition_shape_iris(shape_ir, &rule.condition_shapes) {
                    Ok(shape_iris) => shape_iris,
                    Err(reason) => {
                        return (
                            SrcGenRuleKind::Unsupported {
                                kind: "SPARQLRule(condition-shape)".to_string(),
                            },
                            Some(reason),
                        );
                    }
                };
            if rule.query.trim().is_empty() {
                return (
                    SrcGenRuleKind::Unsupported {
                        kind: "SPARQLRule(empty-query)".to_string(),
                    },
                    Some("SPARQLRule has an empty query string".to_string()),
                );
            }
            let compiled_rule = shifty::sparql::SparqlServices::new()
                .algebra(&rule.query)
                .ok()
                .and_then(|algebra| compiled_sparql_rule(&algebra));
            let compiled_query = compiled_rule.as_ref().map(srcgen_compiled_rule);
            let index_requirements = compiled_rule
                .as_ref()
                .map(|compiled| {
                    compiled_sparql_index_requirements(compiled)
                        .into_iter()
                        .map(|requirement| srcgen_compiled_index_requirement(&requirement))
                        .collect()
                })
                .unwrap_or_default();
            (
                SrcGenRuleKind::Sparql {
                    query: rule.query.clone(),
                    condition_shape_iris,
                    compiled_query,
                    index_requirements,
                },
                None,
            )
        }
        Rule::Triple(rule) => {
            let condition_shape_iris =
                match collect_rule_condition_shape_iris(shape_ir, &rule.condition_shapes) {
                    Ok(shape_iris) => shape_iris,
                    Err(reason) => {
                        return (
                            SrcGenRuleKind::Unsupported {
                                kind: "TripleRule(condition-shape)".to_string(),
                            },
                            Some(reason),
                        );
                    }
                };
            let subject = match &rule.subject {
                TriplePatternTerm::This => SrcGenRuleSubject::This,
                TriplePatternTerm::Constant(term) => {
                    if matches!(term, Term::NamedNode(_) | Term::BlankNode(_)) {
                        SrcGenRuleSubject::Constant(term.clone())
                    } else {
                        return (
                            SrcGenRuleKind::Unsupported {
                                kind: "TripleRule(subject-constant-literal)".to_string(),
                            },
                            Some(
                                "TripleRule specialization requires constant sh:subject terms to be IRIs or blank nodes"
                                    .to_string(),
                            ),
                        );
                    }
                }
                TriplePatternTerm::Path(_) => {
                    return (
                        SrcGenRuleKind::Unsupported {
                            kind: "TripleRule(subject-path)".to_string(),
                        },
                        Some(
                            "TripleRule specialization currently does not support path-based sh:subject templates"
                                .to_string(),
                        ),
                    );
                }
            };
            let object = match &rule.object {
                TriplePatternTerm::Constant(term) => SrcGenRuleObject::Constant(term.clone()),
                TriplePatternTerm::This => SrcGenRuleObject::This,
                TriplePatternTerm::Path(_) => {
                    return (
                        SrcGenRuleKind::Unsupported {
                            kind: "TripleRule(object-path)".to_string(),
                        },
                        Some(
                            "TripleRule specialization currently does not support path-based sh:object templates"
                                .to_string(),
                        ),
                    );
                }
            };
            (
                SrcGenRuleKind::Triple {
                    subject,
                    predicate_iri: rule.predicate.as_str().to_string(),
                    object,
                    condition_shape_iris,
                },
                None,
            )
        }
    }
}

fn collect_rule_condition_shape_iris(
    shape_ir: &ShapeIR,
    conditions: &[RuleCondition],
) -> Result<Vec<String>, String> {
    let mut condition_shape_iris: Vec<String> = Vec::new();
    for condition in conditions {
        let RuleCondition::NodeShape(shape_id) = condition;
        let Some(shape_iri) = resolve_shape_iri(shape_ir, *shape_id) else {
            return Err(format!(
                "Rule condition references unknown shape id {}",
                shape_id.0
            ));
        };
        condition_shape_iris.push(shape_iri);
    }
    condition_shape_iris.sort();
    condition_shape_iris.dedup();
    Ok(condition_shape_iris)
}

fn node_constraint_supported(kind: &SrcGenComponentKind) -> bool {
    matches!(
        kind,
        SrcGenComponentKind::Class { .. }
            | SrcGenComponentKind::PropertyLink { .. }
            | SrcGenComponentKind::Closed { .. }
            | SrcGenComponentKind::Datatype { .. }
            | SrcGenComponentKind::Node { .. }
            | SrcGenComponentKind::Not { .. }
            | SrcGenComponentKind::And { .. }
            | SrcGenComponentKind::Or { .. }
            | SrcGenComponentKind::Xone { .. }
            | SrcGenComponentKind::NodeKind { .. }
            | SrcGenComponentKind::MinLength { .. }
            | SrcGenComponentKind::MaxLength { .. }
            | SrcGenComponentKind::MinExclusive { .. }
            | SrcGenComponentKind::MinInclusive { .. }
            | SrcGenComponentKind::MaxExclusive { .. }
            | SrcGenComponentKind::MaxInclusive { .. }
            | SrcGenComponentKind::Pattern { .. }
            | SrcGenComponentKind::HasValue { .. }
            | SrcGenComponentKind::In { .. }
            | SrcGenComponentKind::LanguageIn { .. }
            | SrcGenComponentKind::Equals { .. }
            | SrcGenComponentKind::Disjoint { .. }
            | SrcGenComponentKind::LessThan { .. }
            | SrcGenComponentKind::LessThanOrEquals { .. }
            | SrcGenComponentKind::Sparql { .. }
            | SrcGenComponentKind::CustomSparql { .. }
            | SrcGenComponentKind::ExpressionThis
    )
}

fn property_constraint_supported(kind: &SrcGenComponentKind) -> bool {
    matches!(
        kind,
        SrcGenComponentKind::PropertyLink { .. }
            | SrcGenComponentKind::Class { .. }
            | SrcGenComponentKind::Datatype { .. }
            | SrcGenComponentKind::QualifiedValueShape { .. }
            | SrcGenComponentKind::Node { .. }
            | SrcGenComponentKind::Not { .. }
            | SrcGenComponentKind::And { .. }
            | SrcGenComponentKind::Or { .. }
            | SrcGenComponentKind::Xone { .. }
            | SrcGenComponentKind::NodeKind { .. }
            | SrcGenComponentKind::MinCount { .. }
            | SrcGenComponentKind::MaxCount { .. }
            | SrcGenComponentKind::MinLength { .. }
            | SrcGenComponentKind::MaxLength { .. }
            | SrcGenComponentKind::MinExclusive { .. }
            | SrcGenComponentKind::MinInclusive { .. }
            | SrcGenComponentKind::MaxExclusive { .. }
            | SrcGenComponentKind::MaxInclusive { .. }
            | SrcGenComponentKind::Pattern { .. }
            | SrcGenComponentKind::HasValue { .. }
            | SrcGenComponentKind::In { .. }
            | SrcGenComponentKind::LanguageIn { .. }
            | SrcGenComponentKind::UniqueLang { .. }
            | SrcGenComponentKind::Equals { .. }
            | SrcGenComponentKind::Disjoint { .. }
            | SrcGenComponentKind::LessThan { .. }
            | SrcGenComponentKind::LessThanOrEquals { .. }
            | SrcGenComponentKind::Sparql { .. }
            | SrcGenComponentKind::CustomSparql { .. }
    )
}

fn term_to_stable_string(term: &Term) -> String {
    match term {
        Term::NamedNode(node) => node.as_str().to_string(),
        _ => term.to_string(),
    }
}

fn escape_sparql_string(value: &str) -> String {
    let mut out = String::with_capacity(value.len());
    for ch in value.chars() {
        match ch {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c => out.push(c),
        }
    }
    out
}

fn term_to_sparql(term: &Term) -> String {
    match term {
        Term::NamedNode(node) => format!("<{}>", node.as_str()),
        Term::BlankNode(node) => format!("_:{}", node.as_str()),
        Term::Literal(literal) => {
            if let Some(language) = literal.language() {
                format!("\"{}\"@{}", escape_sparql_string(literal.value()), language)
            } else {
                format!(
                    "\"{}\"^^<{}>",
                    escape_sparql_string(literal.value()),
                    literal.datatype().as_str(),
                )
            }
        }
    }
}

fn sparql_query_for(shape_ir: &ShapeIR, constraint_node: &Term) -> Result<String, String> {
    let select_predicate = "http://www.w3.org/ns/shacl#select";
    for quad in &shape_ir.shape_quads {
        if quad.predicate.as_str() != select_predicate {
            continue;
        }
        let subject = Term::from(quad.subject.clone());
        if &subject != constraint_node {
            continue;
        }
        return match &quad.object {
            Term::Literal(literal) => Ok(literal.value().to_string()),
            _ => Err("sh:select value must be a literal string".to_string()),
        };
    }
    Err("SPARQL constraint is missing sh:select".to_string())
}

fn sparql_prefixes_for(shape_ir: &ShapeIR, constraint_node: &Term) -> Result<String, String> {
    let sh_prefixes = "http://www.w3.org/ns/shacl#prefixes";
    let sh_declare = "http://www.w3.org/ns/shacl#declare";
    let sh_prefix = "http://www.w3.org/ns/shacl#prefix";
    let sh_namespace = "http://www.w3.org/ns/shacl#namespace";

    let mut prefix_subjects: Vec<Term> = shape_ir
        .shape_quads
        .iter()
        .filter_map(|quad| {
            if quad.predicate.as_str() != sh_prefixes {
                return None;
            }
            let subject = Term::from(quad.subject.clone());
            if &subject == constraint_node {
                Some(quad.object.clone())
            } else {
                None
            }
        })
        .collect();

    let mut global_declare_subjects: HashSet<Term> = shape_ir
        .shape_quads
        .iter()
        .filter_map(|quad| {
            if quad.predicate.as_str() == sh_declare {
                Some(Term::from(quad.subject.clone()))
            } else {
                None
            }
        })
        .collect();
    prefix_subjects.extend(global_declare_subjects.drain());
    prefix_subjects.sort_by_key(|term| term.to_string());
    prefix_subjects.dedup();

    let mut collected: BTreeMap<String, String> = BTreeMap::new();
    for prefixes_subject in prefix_subjects {
        let declarations: Vec<Term> = shape_ir
            .shape_quads
            .iter()
            .filter_map(|quad| {
                if quad.predicate.as_str() != sh_declare {
                    return None;
                }
                let subject = Term::from(quad.subject.clone());
                if subject == prefixes_subject {
                    Some(quad.object.clone())
                } else {
                    None
                }
            })
            .collect();

        for declaration in declarations {
            let prefix_val = shape_ir.shape_quads.iter().find_map(|quad| {
                if quad.predicate.as_str() != sh_prefix {
                    return None;
                }
                let subject = Term::from(quad.subject.clone());
                if subject == declaration {
                    Some(quad.object.clone())
                } else {
                    None
                }
            });
            let namespace_val = shape_ir.shape_quads.iter().find_map(|quad| {
                if quad.predicate.as_str() != sh_namespace {
                    return None;
                }
                let subject = Term::from(quad.subject.clone());
                if subject == declaration {
                    Some(quad.object.clone())
                } else {
                    None
                }
            });

            let (Some(Term::Literal(prefix_lit)), Some(Term::Literal(namespace_lit))) =
                (prefix_val, namespace_val)
            else {
                return Err(format!(
                    "Ill-formed prefix declaration for {}: missing sh:prefix or sh:namespace literal",
                    declaration
                ));
            };

            let prefix = prefix_lit.value().to_string();
            let namespace = namespace_lit.value().to_string();
            if let Some(existing_namespace) = collected.get(&prefix) {
                if existing_namespace != &namespace {
                    return Err(format!(
                        "Duplicate prefix '{}' with different namespaces: '{}' and '{}'",
                        prefix, existing_namespace, namespace
                    ));
                }
            } else {
                collected.insert(prefix, namespace);
            }
        }
    }

    if collected.is_empty() {
        return Ok(String::new());
    }

    let mut prefixes = String::new();
    for (prefix, namespace) in collected {
        prefixes.push_str("PREFIX ");
        prefixes.push_str(prefix.as_str());
        prefixes.push_str(": <");
        prefixes.push_str(namespace.as_str());
        prefixes.push_str(">\n");
    }
    Ok(prefixes.trim_end().to_string())
}

fn contains_variable_token(query: &str, sigil: char, var: &str) -> bool {
    let pattern = format!("{sigil}{var}");
    let query_bytes = query.as_bytes();
    let pattern_len = pattern.len();
    for (idx, _) in query.match_indices(pattern.as_str()) {
        let boundary_before = if idx == 0 {
            true
        } else {
            let prev = query_bytes[idx - 1];
            !(prev.is_ascii_alphanumeric() || prev == b'_')
        };
        let boundary_after = if idx + pattern_len >= query_bytes.len() {
            true
        } else {
            let next = query_bytes[idx + pattern_len];
            !(next.is_ascii_alphanumeric() || next == b'_')
        };
        if boundary_before && boundary_after {
            return true;
        }
    }
    false
}

fn sparql_query_mentions_var(query: &str, var: &str) -> bool {
    contains_variable_token(query, '?', var) || contains_variable_token(query, '$', var)
}

fn sparql_query_requires_path(query: &str) -> bool {
    query.contains("$PATH")
}

fn sparql_query_is_select(query: &str) -> bool {
    for line in query.lines() {
        let trimmed = line.trim_start();
        if trimmed.is_empty() {
            continue;
        }
        let upper = trimmed.to_ascii_uppercase();
        if upper.starts_with("PREFIX ") || upper.starts_with("BASE ") {
            continue;
        }
        return upper.starts_with("SELECT");
    }
    false
}

fn local_name_from_iri(iri: &oxigraph::model::NamedNode) -> String {
    let iri_str = iri.as_str();
    if let Some(hash_idx) = iri_str.rfind('#') {
        iri_str[hash_idx + 1..].to_string()
    } else if let Some(slash_idx) = iri_str.rfind('/') {
        if slash_idx < iri_str.len() - 1 {
            iri_str[slash_idx + 1..].to_string()
        } else {
            let end = slash_idx;
            let mut start = slash_idx;
            if let Some(prev_slash) = iri_str[..end].rfind('/') {
                start = prev_slash + 1;
            }
            iri_str[start..end].to_string()
        }
    } else {
        iri_str.to_string()
    }
}

fn sparql_validator_signature(validator: &SPARQLValidator) -> (bool, bool, bool, String, String) {
    (
        validator.is_ask,
        validator.require_this,
        validator.require_path,
        validator.query.clone(),
        validator.prefixes.clone(),
    )
}

fn selected_custom_sparql_validator(
    definition: &CustomConstraintComponentDefinition,
) -> Result<&SPARQLValidator, String> {
    let mut validators: Vec<&SPARQLValidator> = Vec::new();
    if let Some(validator) = definition.validator.as_ref() {
        validators.push(validator);
    }
    if let Some(validator) = definition.node_validator.as_ref() {
        validators.push(validator);
    }
    if let Some(validator) = definition.property_validator.as_ref() {
        validators.push(validator);
    }

    let Some(first) = validators.first().copied() else {
        return Err(format!(
            "Custom constraint component {} does not declare a SPARQL validator",
            definition.iri
        ));
    };
    let first_signature = sparql_validator_signature(first);
    if validators
        .iter()
        .skip(1)
        .any(|validator| sparql_validator_signature(validator) != first_signature)
    {
        return Err(format!(
            "Custom constraint component {} has context-dependent validators that are not yet specialized",
            definition.iri
        ));
    }
    Ok(first)
}

fn lower_custom_component_kind(
    definition: &CustomConstraintComponentDefinition,
    parameter_values: &HashMap<oxigraph::model::NamedNode, Vec<Term>>,
) -> (SrcGenComponentKind, Option<String>) {
    let validator = match selected_custom_sparql_validator(definition) {
        Ok(validator) => validator,
        Err(reason) => {
            return (
                SrcGenComponentKind::Unsupported {
                    kind: "Custom(validator-selection)".to_string(),
                },
                Some(reason),
            );
        }
    };

    let query = validator.query.clone();
    if !query.is_ascii() {
        return (
            SrcGenComponentKind::Unsupported {
                kind: "Custom(non-ascii-query)".to_string(),
            },
            Some(format!(
                "Custom constraint component {} uses a non-ASCII SPARQL query that is not yet specialized",
                definition.iri
            )),
        );
    }

    let mut parameter_bindings: Vec<SrcGenSparqlBinding> = Vec::new();
    for parameter in &definition.parameters {
        let var_name = parameter
            .var_name
            .clone()
            .unwrap_or_else(|| local_name_from_iri(&parameter.path));
        if !sparql_query_mentions_var(&query, &var_name) {
            continue;
        }
        let Some(values) = parameter_values.get(&parameter.path) else {
            if parameter.optional {
                continue;
            }
            return (
                SrcGenComponentKind::Unsupported {
                    kind: "Custom(missing-parameter)".to_string(),
                },
                Some(format!(
                    "Custom constraint component {} is missing required parameter {}",
                    definition.iri, parameter.path
                )),
            );
        };
        if values.len() != 1 {
            return (
                SrcGenComponentKind::Unsupported {
                    kind: "Custom(multi-valued-parameter)".to_string(),
                },
                Some(format!(
                    "Custom constraint component {} currently supports one value per parameter (parameter {} had {})",
                    definition.iri,
                    parameter.path,
                    values.len()
                )),
            );
        }
        parameter_bindings.push(SrcGenSparqlBinding {
            var_name,
            value: values[0].clone(),
        });
    }
    parameter_bindings.sort_by(|left, right| {
        left.var_name
            .cmp(&right.var_name)
            .then_with(|| left.value.to_string().cmp(&right.value.to_string()))
    });

    (
        SrcGenComponentKind::CustomSparql {
            query: query.clone(),
            prefixes: validator.prefixes.clone(),
            is_ask: validator.is_ask,
            requires_path: validator.require_path || sparql_query_requires_path(&query),
            bind_value: validator.is_ask && sparql_query_mentions_var(&query, "value"),
            parameter_bindings,
        },
        None,
    )
}

fn resolve_shape_iri(shape_ir: &ShapeIR, shape_id: ID) -> Option<String> {
    shape_ir
        .node_shape_terms
        .get(&shape_id)
        .map(term_to_stable_string)
        .or_else(|| {
            shape_ir
                .node_shapes
                .iter()
                .find(|shape| shape.id == shape_id)
                .map(|_| format!("_:node-shape-{}", shape_id.0))
        })
}

fn resolve_property_shape_iri(shape_ir: &ShapeIR, shape_id: PropShapeID) -> Option<String> {
    shape_ir
        .property_shape_terms
        .get(&shape_id)
        .map(term_to_stable_string)
        .or_else(|| {
            shape_ir
                .property_shapes
                .iter()
                .find(|shape| shape.id == shape_id)
                .map(|_| format!("_:property-shape-{}", shape_id.0))
        })
}

fn component_kind(
    shape_ir: &ShapeIR,
    descriptor: &ComponentDescriptor,
) -> (SrcGenComponentKind, Option<String>) {
    match descriptor {
        ComponentDescriptor::Property { shape } => {
            if let Some(property_shape_iri) = resolve_property_shape_iri(shape_ir, *shape) {
                (
                    SrcGenComponentKind::PropertyLink { property_shape_iri },
                    None,
                )
            } else {
                (
                    SrcGenComponentKind::Unsupported {
                        kind: "Property(missing-shape-iri)".to_string(),
                    },
                    Some(format!(
                        "Property component references missing property shape {}",
                        shape.0
                    )),
                )
            }
        }
        ComponentDescriptor::QualifiedValueShape {
            shape,
            min_count,
            max_count,
            disjoint,
        } => {
            if let Some(shape_iri) = resolve_shape_iri(shape_ir, *shape) {
                (
                    SrcGenComponentKind::QualifiedValueShape {
                        shape_iri,
                        min_count: *min_count,
                        max_count: *max_count,
                        disjoint: disjoint.unwrap_or(false),
                    },
                    None,
                )
            } else {
                (
                    SrcGenComponentKind::Unsupported {
                        kind: "QualifiedValueShape(missing-shape)".to_string(),
                    },
                    Some(format!(
                        "QualifiedValueShape constraint references unknown shape id {}",
                        shape.0
                    )),
                )
            }
        }
        ComponentDescriptor::Closed {
            closed,
            ignored_properties,
        } => {
            let ignored_property_iris = ignored_properties
                .iter()
                .filter_map(|term| match term {
                    Term::NamedNode(node) => Some(node.as_str().to_string()),
                    _ => None,
                })
                .collect();
            (
                SrcGenComponentKind::Closed {
                    closed: *closed,
                    ignored_property_iris,
                },
                None,
            )
        }
        ComponentDescriptor::Node { shape } => {
            if let Some(shape_iri) = resolve_shape_iri(shape_ir, *shape) {
                (SrcGenComponentKind::Node { shape_iri }, None)
            } else {
                (
                    SrcGenComponentKind::Unsupported {
                        kind: "Node(missing-shape)".to_string(),
                    },
                    Some(format!(
                        "Node constraint references unknown shape id {}",
                        shape.0
                    )),
                )
            }
        }
        ComponentDescriptor::Not { shape } => {
            if let Some(shape_iri) = resolve_shape_iri(shape_ir, *shape) {
                (SrcGenComponentKind::Not { shape_iri }, None)
            } else {
                (
                    SrcGenComponentKind::Unsupported {
                        kind: "Not(missing-shape)".to_string(),
                    },
                    Some(format!(
                        "Not constraint references unknown shape id {}",
                        shape.0
                    )),
                )
            }
        }
        ComponentDescriptor::And { shapes } => {
            let mut shape_iris = Vec::with_capacity(shapes.len());
            for shape_id in shapes {
                let Some(shape_iri) = resolve_shape_iri(shape_ir, *shape_id) else {
                    return (
                        SrcGenComponentKind::Unsupported {
                            kind: "And(missing-shape)".to_string(),
                        },
                        Some(format!(
                            "And constraint references unknown shape id {}",
                            shape_id.0
                        )),
                    );
                };
                shape_iris.push(shape_iri);
            }
            (SrcGenComponentKind::And { shape_iris }, None)
        }
        ComponentDescriptor::Or { shapes } => {
            let mut shape_iris = Vec::with_capacity(shapes.len());
            for shape_id in shapes {
                let Some(shape_iri) = resolve_shape_iri(shape_ir, *shape_id) else {
                    return (
                        SrcGenComponentKind::Unsupported {
                            kind: "Or(missing-shape)".to_string(),
                        },
                        Some(format!(
                            "Or constraint references unknown shape id {}",
                            shape_id.0
                        )),
                    );
                };
                shape_iris.push(shape_iri);
            }
            (SrcGenComponentKind::Or { shape_iris }, None)
        }
        ComponentDescriptor::Xone { shapes } => {
            let mut shape_iris = Vec::with_capacity(shapes.len());
            for shape_id in shapes {
                let Some(shape_iri) = resolve_shape_iri(shape_ir, *shape_id) else {
                    return (
                        SrcGenComponentKind::Unsupported {
                            kind: "Xone(missing-shape)".to_string(),
                        },
                        Some(format!(
                            "Xone constraint references unknown shape id {}",
                            shape_id.0
                        )),
                    );
                };
                shape_iris.push(shape_iri);
            }
            (SrcGenComponentKind::Xone { shape_iris }, None)
        }
        ComponentDescriptor::Class {
            class: Term::NamedNode(class),
        } => (
            SrcGenComponentKind::Class {
                class_iri: class.as_str().to_string(),
            },
            None,
        ),
        ComponentDescriptor::Datatype {
            datatype: Term::NamedNode(datatype),
        } => (
            SrcGenComponentKind::Datatype {
                datatype_iri: datatype.as_str().to_string(),
            },
            None,
        ),
        ComponentDescriptor::NodeKind {
            node_kind: Term::NamedNode(node_kind),
        } => (
            SrcGenComponentKind::NodeKind {
                node_kind_iri: node_kind.as_str().to_string(),
            },
            None,
        ),
        ComponentDescriptor::MinCount { min_count } => (
            SrcGenComponentKind::MinCount {
                min_count: *min_count,
            },
            None,
        ),
        ComponentDescriptor::MaxCount { max_count } => (
            SrcGenComponentKind::MaxCount {
                max_count: *max_count,
            },
            None,
        ),
        ComponentDescriptor::MinLength { length } => (
            SrcGenComponentKind::MinLength {
                min_length: *length,
            },
            None,
        ),
        ComponentDescriptor::MaxLength { length } => (
            SrcGenComponentKind::MaxLength {
                max_length: *length,
            },
            None,
        ),
        ComponentDescriptor::MinExclusive { value } => (
            SrcGenComponentKind::MinExclusive {
                value_sparql: term_to_sparql(value),
            },
            None,
        ),
        ComponentDescriptor::MinInclusive { value } => (
            SrcGenComponentKind::MinInclusive {
                value_sparql: term_to_sparql(value),
            },
            None,
        ),
        ComponentDescriptor::MaxExclusive { value } => (
            SrcGenComponentKind::MaxExclusive {
                value_sparql: term_to_sparql(value),
            },
            None,
        ),
        ComponentDescriptor::MaxInclusive { value } => (
            SrcGenComponentKind::MaxInclusive {
                value_sparql: term_to_sparql(value),
            },
            None,
        ),
        ComponentDescriptor::Pattern { pattern, flags } => (
            SrcGenComponentKind::Pattern {
                pattern: pattern.clone(),
                flags: flags.clone(),
            },
            None,
        ),
        ComponentDescriptor::HasValue { value } => (
            SrcGenComponentKind::HasValue {
                value_sparql: term_to_sparql(value),
            },
            None,
        ),
        ComponentDescriptor::In { values } => (
            SrcGenComponentKind::In {
                values_sparql: values.iter().map(term_to_sparql).collect(),
            },
            None,
        ),
        ComponentDescriptor::Sparql { constraint_node } => {
            let query = match sparql_query_for(shape_ir, constraint_node) {
                Ok(query) => query,
                Err(err) => {
                    return (
                        SrcGenComponentKind::Unsupported {
                            kind: "Sparql(missing-select)".to_string(),
                        },
                        Some(err),
                    );
                }
            };
            if !sparql_query_is_select(&query) {
                return (
                    SrcGenComponentKind::Unsupported {
                        kind: "Sparql(non-select)".to_string(),
                    },
                    Some(
                        "SPARQL constraint specialization currently supports SELECT queries only"
                            .to_string(),
                    ),
                );
            }
            if !sparql_query_mentions_var(&query, "this") {
                return (
                    SrcGenComponentKind::Unsupported {
                        kind: "Sparql(missing-this)".to_string(),
                    },
                    Some(
                        "SPARQL constraint does not reference pre-bound variable ?this".to_string(),
                    ),
                );
            }
            let prefixes = match sparql_prefixes_for(shape_ir, constraint_node) {
                Ok(prefixes) => prefixes,
                Err(err) => {
                    return (
                        SrcGenComponentKind::Unsupported {
                            kind: "Sparql(prefixes)".to_string(),
                        },
                        Some(format!(
                            "SPARQL constraint prefix resolution failed for {}: {}",
                            constraint_node, err
                        )),
                    );
                }
            };
            let requires_path = sparql_query_requires_path(&query);
            let lowered_query = srcgen_lowered_query_kind(&query, &prefixes);
            (
                SrcGenComponentKind::Sparql {
                    query,
                    prefixes,
                    requires_path,
                    constraint_term: constraint_node.to_string(),
                    lowered_query,
                },
                None,
            )
        }
        ComponentDescriptor::Custom {
            definition,
            parameter_values,
        } => lower_custom_component_kind(definition, parameter_values),
        ComponentDescriptor::LanguageIn { languages } => (
            SrcGenComponentKind::LanguageIn {
                languages: languages.clone(),
            },
            None,
        ),
        ComponentDescriptor::UniqueLang { enabled } => {
            (SrcGenComponentKind::UniqueLang { enabled: *enabled }, None)
        }
        ComponentDescriptor::Equals {
            property: Term::NamedNode(property),
        } => (
            SrcGenComponentKind::Equals {
                property_iri: property.as_str().to_string(),
            },
            None,
        ),
        ComponentDescriptor::Disjoint {
            property: Term::NamedNode(property),
        } => (
            SrcGenComponentKind::Disjoint {
                property_iri: property.as_str().to_string(),
            },
            None,
        ),
        ComponentDescriptor::LessThan {
            property: Term::NamedNode(property),
        } => (
            SrcGenComponentKind::LessThan {
                property_iri: property.as_str().to_string(),
            },
            None,
        ),
        ComponentDescriptor::LessThanOrEquals {
            property: Term::NamedNode(property),
        } => (
            SrcGenComponentKind::LessThanOrEquals {
                property_iri: property.as_str().to_string(),
            },
            None,
        ),
        ComponentDescriptor::Equals { .. } => (
            SrcGenComponentKind::Unsupported {
                kind: "Equals(non-iri)".to_string(),
            },
            Some("Equals constraint uses a non-IRI property term".to_string()),
        ),
        ComponentDescriptor::Disjoint { .. } => (
            SrcGenComponentKind::Unsupported {
                kind: "Disjoint(non-iri)".to_string(),
            },
            Some("Disjoint constraint uses a non-IRI property term".to_string()),
        ),
        ComponentDescriptor::LessThan { .. } => (
            SrcGenComponentKind::Unsupported {
                kind: "LessThan(non-iri)".to_string(),
            },
            Some("LessThan constraint uses a non-IRI property term".to_string()),
        ),
        ComponentDescriptor::LessThanOrEquals { .. } => (
            SrcGenComponentKind::Unsupported {
                kind: "LessThanOrEquals(non-iri)".to_string(),
            },
            Some("LessThanOrEquals constraint uses a non-IRI property term".to_string()),
        ),
        ComponentDescriptor::Class { .. } => (
            SrcGenComponentKind::Unsupported {
                kind: "Class(non-iri)".to_string(),
            },
            Some("Class constraint uses a non-IRI class term".to_string()),
        ),
        ComponentDescriptor::Datatype { .. } => (
            SrcGenComponentKind::Unsupported {
                kind: "Datatype(non-iri)".to_string(),
            },
            Some("Datatype constraint uses a non-IRI datatype term".to_string()),
        ),
        ComponentDescriptor::NodeKind { .. } => (
            SrcGenComponentKind::Unsupported {
                kind: "NodeKind(non-iri)".to_string(),
            },
            Some("NodeKind constraint uses a non-IRI node kind term".to_string()),
        ),
    }
}

fn component_constraint_component_iri(component: &ComponentDescriptor) -> &str {
    match component {
        ComponentDescriptor::Node { .. } => "http://www.w3.org/ns/shacl#NodeConstraintComponent",
        ComponentDescriptor::Property { .. } => "http://www.w3.org/ns/shacl#PropertyShapeComponent",
        ComponentDescriptor::QualifiedValueShape { min_count, .. } => {
            if min_count.is_some() {
                "http://www.w3.org/ns/shacl#QualifiedMinCountConstraintComponent"
            } else {
                "http://www.w3.org/ns/shacl#QualifiedMaxCountConstraintComponent"
            }
        }
        ComponentDescriptor::Class { .. } => "http://www.w3.org/ns/shacl#ClassConstraintComponent",
        ComponentDescriptor::Datatype { .. } => {
            "http://www.w3.org/ns/shacl#DatatypeConstraintComponent"
        }
        ComponentDescriptor::NodeKind { .. } => {
            "http://www.w3.org/ns/shacl#NodeKindConstraintComponent"
        }
        ComponentDescriptor::MinCount { .. } => {
            "http://www.w3.org/ns/shacl#MinCountConstraintComponent"
        }
        ComponentDescriptor::MaxCount { .. } => {
            "http://www.w3.org/ns/shacl#MaxCountConstraintComponent"
        }
        ComponentDescriptor::MinExclusive { .. } => {
            "http://www.w3.org/ns/shacl#MinExclusiveConstraintComponent"
        }
        ComponentDescriptor::MinInclusive { .. } => {
            "http://www.w3.org/ns/shacl#MinInclusiveConstraintComponent"
        }
        ComponentDescriptor::MaxExclusive { .. } => {
            "http://www.w3.org/ns/shacl#MaxExclusiveConstraintComponent"
        }
        ComponentDescriptor::MaxInclusive { .. } => {
            "http://www.w3.org/ns/shacl#MaxInclusiveConstraintComponent"
        }
        ComponentDescriptor::MinLength { .. } => {
            "http://www.w3.org/ns/shacl#MinLengthConstraintComponent"
        }
        ComponentDescriptor::MaxLength { .. } => {
            "http://www.w3.org/ns/shacl#MaxLengthConstraintComponent"
        }
        ComponentDescriptor::Pattern { .. } => {
            "http://www.w3.org/ns/shacl#PatternConstraintComponent"
        }
        ComponentDescriptor::LanguageIn { .. } => {
            "http://www.w3.org/ns/shacl#LanguageInConstraintComponent"
        }
        ComponentDescriptor::UniqueLang { .. } => {
            "http://www.w3.org/ns/shacl#UniqueLangConstraintComponent"
        }
        ComponentDescriptor::Equals { .. } => {
            "http://www.w3.org/ns/shacl#EqualsConstraintComponent"
        }
        ComponentDescriptor::Disjoint { .. } => {
            "http://www.w3.org/ns/shacl#DisjointConstraintComponent"
        }
        ComponentDescriptor::LessThan { .. } => {
            "http://www.w3.org/ns/shacl#LessThanConstraintComponent"
        }
        ComponentDescriptor::LessThanOrEquals { .. } => {
            "http://www.w3.org/ns/shacl#LessThanOrEqualsConstraintComponent"
        }
        ComponentDescriptor::Not { .. } => "http://www.w3.org/ns/shacl#NotConstraintComponent",
        ComponentDescriptor::And { .. } => "http://www.w3.org/ns/shacl#AndConstraintComponent",
        ComponentDescriptor::Or { .. } => "http://www.w3.org/ns/shacl#OrConstraintComponent",
        ComponentDescriptor::Xone { .. } => "http://www.w3.org/ns/shacl#XoneConstraintComponent",
        ComponentDescriptor::Closed { .. } => {
            "http://www.w3.org/ns/shacl#ClosedConstraintComponent"
        }
        ComponentDescriptor::HasValue { .. } => {
            "http://www.w3.org/ns/shacl#HasValueConstraintComponent"
        }
        ComponentDescriptor::In { .. } => "http://www.w3.org/ns/shacl#InConstraintComponent",
        ComponentDescriptor::Sparql { .. } => {
            "http://www.w3.org/ns/shacl#SPARQLConstraintComponent"
        }
        ComponentDescriptor::Custom { definition, .. } => definition.iri.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use oxigraph::model::Literal;
    use shifty::shacl_ir::{
        ComponentDescriptor, ComponentID, FeatureToggles, ID, NodeShapeIR, PropShapeID,
        PropertyShapeIR, Rule, RuleCondition, RuleID, Severity, ShapeIR, SparqlRule, Target,
        TriplePatternTerm, TripleRule,
    };
    use std::collections::HashMap;

    #[test]
    fn lower_is_deterministic_with_unsorted_maps() {
        let mut components = HashMap::new();
        components.insert(
            shifty::shacl_ir::ComponentID(9),
            ComponentDescriptor::MaxCount { max_count: 2 },
        );
        components.insert(
            shifty::shacl_ir::ComponentID(3),
            ComponentDescriptor::MinCount { min_count: 1 },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(8),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:z-node",
            )),
        );
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:a-node",
            )),
        );

        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:b-prop",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: Vec::new(),
                constraints: Vec::new(),
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(1),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:p",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:p")),
                constraints: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let first = lower_shape_ir(&shape_ir).unwrap();
        let second = lower_shape_ir(&shape_ir).unwrap();

        assert_eq!(
            first.to_json_pretty().unwrap(),
            second.to_json_pretty().unwrap()
        );
        assert_eq!(first.node_shapes[0].iri, "urn:shape:a-node");
        assert_eq!(first.components[0].id, 3);
        assert!(first.meta.specialization_ready);
    }

    #[test]
    fn lowers_path_value_equals_constant_query_for_srcgen() {
        let lowered = srcgen_lowered_query_kind(
            r#"SELECT $this
               WHERE {
                   $this ex:required ?value .
                   FILTER(?value = "bad")
               }"#,
            "PREFIX ex: <http://example.com/ns#>",
        );

        let Some(SrcGenLoweredSparqlQueryKind::PathValueEqualsConstant {
            value_path: SrcGenLoweredPropertyPath::NamedNode { predicate_iri },
            expected_value,
        }) = lowered
        else {
            panic!("expected PathValueEqualsConstant lowering");
        };
        assert_eq!(predicate_iri, "http://example.com/ns#required");
        assert_eq!(expected_value, Term::Literal(Literal::from("bad")));
    }

    #[test]
    fn specialization_ready_for_phase1_supported_subset() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::Property {
                shape: PropShapeID(7),
            },
        );
        components.insert(
            ComponentID(3),
            ComponentDescriptor::Datatype {
                datatype: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "http://www.w3.org/2001/XMLSchema#integer",
                )),
            },
        );
        components.insert(
            ComponentID(4),
            ComponentDescriptor::MinCount { min_count: 1 },
        );
        components.insert(
            ComponentID(5),
            ComponentDescriptor::MaxCount { max_count: 1 },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person-age",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1), ComponentID(2)],
                property_shapes: vec![PropShapeID(7)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:age",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:age")),
                constraints: vec![ComponentID(3), ComponentID(4), ComponentID(5)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(
            lowered
                .components
                .iter()
                .all(|component| !component.fallback_only)
        );
        assert_eq!(lowered.node_shapes.len(), 1);
        assert_eq!(lowered.property_shapes.len(), 1);
        assert!(lowered.node_shapes[0].supported);
        assert!(lowered.property_shapes[0].supported);
    }

    #[test]
    fn property_shape_with_sequence_path_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::MinCount { min_count: 1 },
        );

        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:seq-path",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: Vec::new(),
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Sequence(vec![
                    Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                        "urn:p1",
                    ))),
                    Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                        "urn:p2",
                    ))),
                ]),
                path_term: Term::BlankNode(oxigraph::model::BlankNode::new_unchecked("path-seq")),
                constraints: vec![ComponentID(1)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms: HashMap::new(),
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.property_shapes.len(), 1);
        let property_shape = &lowered.property_shapes[0];
        assert!(property_shape.supported);
        assert!(property_shape.path_predicate.is_none());
        assert_eq!(property_shape.path_sparql, "(<urn:p1> / <urn:p2>)");
        assert!(matches!(
            property_shape.path,
            SrcGenPath::Sequence { ref items } if items.len() == 2
        ));
    }

    #[test]
    fn closed_constraint_with_complex_property_path_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Closed {
                closed: true,
                ignored_properties: Vec::new(),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::Property {
                shape: PropShapeID(7),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:closed",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:complex-path",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Asset"),
                ))],
                constraints: vec![ComponentID(1), ComponentID(2)],
                property_shapes: vec![PropShapeID(7)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Sequence(vec![
                    Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                        "urn:rel:hasPart",
                    ))),
                    Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                        "urn:rel:name",
                    ))),
                ]),
                path_term: Term::BlankNode(oxigraph::model::BlankNode::new_unchecked(
                    "path-closed-seq",
                )),
                constraints: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.node_shapes.len(), 1);
        let node_shape = &lowered.node_shapes[0];
        assert!(node_shape.supported);
        assert!(node_shape.supported_constraints.contains(&1));
        assert!(!node_shape.fallback_constraints.contains(&1));
    }

    #[test]
    fn node_shape_with_target_node_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::NodeKind {
                node_kind: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "http://www.w3.org/ns/shacl#IRI",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:target-node",
            )),
        );

        let target_node = Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:focus"));
        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Node(target_node.clone())],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.node_shapes[0].supported);
        assert_eq!(lowered.node_shapes[0].target_nodes, vec![target_node]);
        assert!(lowered.node_shapes[0].target_classes.is_empty());
    }

    #[test]
    fn node_shape_with_subjects_and_objects_targets_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::NodeKind {
                node_kind: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "http://www.w3.org/ns/shacl#BlankNodeOrIRI",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:targets-of",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![
                    Target::SubjectsOf(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                        "urn:pred:s",
                    ))),
                    Target::ObjectsOf(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                        "urn:pred:o",
                    ))),
                ],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.node_shapes[0].supported);
        assert_eq!(
            lowered.node_shapes[0].target_subjects_of,
            vec!["urn:pred:s".to_string()]
        );
        assert_eq!(
            lowered.node_shapes[0].target_objects_of,
            vec!["urn:pred:o".to_string()]
        );
    }

    #[test]
    fn node_shape_with_advanced_target_select_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::NodeKind {
                node_kind: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "http://www.w3.org/ns/shacl#IRI",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:advanced-target",
            )),
        );

        let selector = Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:selector"));
        let query = "SELECT ?this WHERE { ?this a <urn:Thing> . }";
        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Advanced(selector.clone())],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedOrBlankNode::from(oxigraph::model::NamedNode::new_unchecked(
                    "urn:selector",
                )),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_simple_literal(query),
                oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.node_shapes[0].supported);
        assert_eq!(
            lowered.node_shapes[0].target_advanced_select_queries,
            vec![query.to_string()]
        );
    }

    #[test]
    fn node_shape_with_expression_this_generates_specialized_component() {
        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:expr-this",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Node(Term::Literal(oxigraph::model::Literal::from(
                    true,
                )))],
                constraints: Vec::new(),
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components: HashMap::new(),
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedOrBlankNode::from(oxigraph::model::NamedNode::new_unchecked(
                    "urn:shape:expr-this",
                )),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#expression"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#this"),
                oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        let expression_component = lowered
            .components
            .iter()
            .find(|component| matches!(component.kind, SrcGenComponentKind::ExpressionThis))
            .expect("expected synthetic expression-this component");
        assert_eq!(
            expression_component.iri,
            "http://www.w3.org/ns/shacl#ExpressionConstraintComponent"
        );
        assert_eq!(
            lowered.node_shapes[0].constraints,
            vec![expression_component.id]
        );
        assert_eq!(
            lowered.node_shapes[0].supported_constraints,
            vec![expression_component.id]
        );
        assert!(lowered.node_shapes[0].fallback_constraints.is_empty());
        assert!(lowered.node_shapes[0].supported);
    }

    #[test]
    fn unsupported_component_disables_specialization_and_sets_fallback_annotation() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:sparql:constraint",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(!lowered.meta.specialization_ready);
        assert_eq!(lowered.fallback_annotations.len(), 1);
        assert_eq!(lowered.fallback_annotations[0].component_id, 1);
        assert!(lowered.components[0].fallback_only);
        assert!(!lowered.node_shapes[0].supported);
    }

    #[test]
    fn lowering_tracks_supported_and_fallback_constraints_per_shape() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::Property {
                shape: PropShapeID(7),
            },
        );
        components.insert(
            ComponentID(3),
            ComponentDescriptor::MinCount { min_count: 1 },
        );
        components.insert(
            ComponentID(4),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:sparql:constraint",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person-age",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1), ComponentID(2), ComponentID(4)],
                property_shapes: vec![PropShapeID(7)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:age",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:age")),
                constraints: vec![ComponentID(3), ComponentID(4)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(!lowered.meta.specialization_ready);

        assert_eq!(lowered.fallback_annotations.len(), 1);
        assert_eq!(lowered.fallback_annotations[0].component_id, 4);

        let node_shape = lowered
            .node_shapes
            .iter()
            .find(|shape| shape.iri == "urn:shape:person")
            .expect("node shape must be present");
        assert!(node_shape.supported);
        assert_eq!(node_shape.supported_constraints, vec![1, 2]);
        assert_eq!(node_shape.fallback_constraints, vec![4]);

        let property_shape = lowered
            .property_shapes
            .iter()
            .find(|shape| shape.iri == "urn:shape:person-age")
            .expect("property shape must be present");
        assert!(property_shape.supported);
        assert_eq!(property_shape.supported_constraints, vec![3]);
        assert_eq!(property_shape.fallback_constraints, vec![4]);
    }

    #[test]
    fn sparql_component_is_specialized_when_select_query_is_available() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:sparql:constraint",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );

        let shape_graph = oxigraph::model::NamedNode::new_unchecked("urn:shape-graph");
        let shape_ir = ShapeIR {
            shape_graph: shape_graph.clone(),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:sparql:constraint"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_typed_literal(
                    "SELECT $this WHERE { FILTER(false) }",
                    oxigraph::model::vocab::xsd::STRING,
                ),
                oxigraph::model::GraphName::NamedNode(shape_graph),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(matches!(
            lowered.components[0].kind,
            SrcGenComponentKind::Sparql { .. }
        ));
        assert!(matches!(
            lowered.components[0].kind,
            SrcGenComponentKind::Sparql { ref constraint_term, .. }
                if constraint_term.contains("urn:sparql:constraint")
        ));
        assert!(lowered.node_shapes[0].supported);
        assert_eq!(lowered.node_shapes[0].supported_constraints, vec![1]);
        assert!(lowered.node_shapes[0].fallback_constraints.is_empty());
    }

    #[test]
    fn sparql_adjacent_whitelist_query_is_lowered_generically() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:sparql:adjacent-whitelist",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:adjacent-whitelist",
            )),
        );

        let shape_graph = oxigraph::model::NamedNode::new_unchecked("urn:shape-graph");
        let shape_ir = ShapeIR {
            shape_graph: shape_graph.clone(),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Thing"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:sparql:adjacent-whitelist"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_typed_literal(
                    r#"SELECT $this WHERE {
                        $this (<urn:hasPart>? | <urn:connectedThrough>?) ?anchor .
                        FILTER NOT EXISTS {
                            { ?anchor ?p ?o . } UNION { ?o ?p ?anchor . }
                            FILTER (?p NOT IN (<urn:allowed>, <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>))
                        }
                    }"#,
                    oxigraph::model::vocab::xsd::STRING,
                ),
                oxigraph::model::GraphName::NamedNode(shape_graph),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        match &lowered.components[0].kind {
            SrcGenComponentKind::Sparql {
                lowered_query:
                    Some(SrcGenLoweredSparqlQueryKind::AdjacentPredicateWhitelist {
                        anchor_path,
                        allowed_predicate_iris,
                    }),
                ..
            } => {
                assert_eq!(
                    allowed_predicate_iris,
                    &vec![
                        "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
                        "urn:allowed".to_string()
                    ]
                );
                assert!(matches!(
                    anchor_path,
                    SrcGenLoweredPropertyPath::Alternative { items } if items.len() == 2
                ));
            }
            other => panic!("expected lowered adjacent-whitelist query, got {other:?}"),
        }
    }

    #[test]
    fn sparql_required_path_support_query_is_lowered_generically() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:sparql:required-path-support",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:required-path-support",
            )),
        );

        let shape_graph = oxigraph::model::NamedNode::new_unchecked("urn:shape-graph");
        let shape_ir = ShapeIR {
            shape_graph: shape_graph.clone(),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Thing"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:sparql:required-path-support"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_typed_literal(
                    r#"SELECT $this ?other
WHERE {
  $this <urn:connected> ?other .
  FILTER NOT EXISTS { $this <urn:cnx>+ ?other }
}"#,
                    oxigraph::model::vocab::xsd::STRING,
                ),
                oxigraph::model::GraphName::NamedNode(shape_graph),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        match &lowered.components[0].kind {
            SrcGenComponentKind::Sparql {
                lowered_query:
                    Some(SrcGenLoweredSparqlQueryKind::RequiredPathSupport {
                        source_predicate_iri,
                        required_path,
                    }),
                ..
            } => {
                assert_eq!(source_predicate_iri, "urn:connected");
                assert!(matches!(
                    required_path,
                    SrcGenLoweredPropertyPath::OneOrMore { inner }
                        if matches!(
                            inner.as_ref(),
                            SrcGenLoweredPropertyPath::NamedNode { predicate_iri }
                                if predicate_iri == "urn:cnx"
                        )
                ));
            }
            other => panic!("expected lowered required-path-support query, got {other:?}"),
        }
    }

    #[test]
    fn sparql_missing_related_node_query_is_lowered_generically() {
        let shape_graph = oxigraph::model::NamedNode::new_unchecked("urn:shape-graph");
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:sparql:missing-related",
                )),
            },
        );
        let node_shape_terms = HashMap::from([(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:missing-related",
            )),
        )]);
        let shape_ir = ShapeIR {
            shape_graph: shape_graph.clone(),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Property"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:sparql:missing-related"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_typed_literal(
                    r#"SELECT $this ?something
WHERE {
  ?something <urn:composedOf> $this .
  FILTER NOT EXISTS {
    $this <urn:ofConstituent> ?someSubstance .
  }
}"#,
                    oxigraph::model::vocab::xsd::STRING,
                ),
                oxigraph::model::GraphName::NamedNode(shape_graph),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        match &lowered.components[0].kind {
            SrcGenComponentKind::Sparql {
                lowered_query:
                    Some(SrcGenLoweredSparqlQueryKind::MissingRelatedNode {
                        related_path,
                        related_variable,
                        related_class,
                        required_path,
                    }),
                ..
            } => {
                assert!(matches!(
                    related_path,
                    SrcGenLoweredPropertyPath::ReverseNamedNode { predicate_iri }
                        if predicate_iri == "urn:composedOf"
                ));
                assert_eq!(related_variable, "something");
                assert_eq!(related_class, &None);
                assert!(matches!(
                    required_path,
                    SrcGenLoweredPropertyPath::NamedNode { predicate_iri }
                        if predicate_iri == "urn:ofConstituent"
                ));
            }
            other => panic!("expected lowered missing-related-node query, got {other:?}"),
        }
    }

    #[test]
    fn sparql_local_set_compatibility_query_is_lowered_generically() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:sparql:local-set-compatibility",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:local-set-compatibility",
            )),
        );

        let shape_graph = oxigraph::model::NamedNode::new_unchecked("urn:shape-graph");
        let shape_ir = ShapeIR {
            shape_graph: shape_graph.clone(),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Junction"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:sparql:local-set-compatibility"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_typed_literal(
                    r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT $this ?m2 ?cp ?m1
WHERE {
  $this <urn:cnx> ?cp .
  ?cp a/rdfs:subClassOf* <urn:ConnectionPoint> .
  ?cp <urn:hasMedium> ?m1 .
  $this <urn:hasMedium> ?m2 .
  ?m2 <urn:composedOf>/<urn:ofConstituent> ?s2 .
  FILTER NOT EXISTS { ?m1 <urn:composedOf> ?c1 . }
  FILTER NOT EXISTS {
    ?m2 <urn:composedOf>/<urn:ofConstituent> ?s12 .
    { ?s12 rdfs:subClassOf* ?m1 } UNION { ?m1 rdfs:subClassOf* ?s12 } .
  }
}"#,
                    oxigraph::model::vocab::xsd::STRING,
                ),
                oxigraph::model::GraphName::NamedNode(shape_graph),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        match &lowered.components[0].kind {
            SrcGenComponentKind::Sparql {
                lowered_query:
                    Some(SrcGenLoweredSparqlQueryKind::LocalSetCompatibility {
                        left_anchor_path,
                        right_anchor_path,
                        left_class,
                        right_class,
                        left_value_path,
                        right_value_path,
                        distinct_anchors,
                        composed_of_predicate_iri,
                        constituent_path,
                        mode,
                        ..
                    }),
                ..
            } => {
                assert!(matches!(
                    left_anchor_path,
                    SrcGenLoweredPropertyPath::SelfNode
                ));
                assert!(matches!(
                    right_anchor_path,
                    SrcGenLoweredPropertyPath::NamedNode { predicate_iri }
                        if predicate_iri == "urn:cnx"
                ));
                assert_eq!(left_class, &None,);
                assert_eq!(
                    right_class,
                    &Some(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                        "urn:ConnectionPoint"
                    )))
                );
                assert!(matches!(
                    left_value_path,
                    SrcGenLoweredPropertyPath::NamedNode { predicate_iri }
                        if predicate_iri == "urn:hasMedium"
                ));
                assert!(matches!(
                    right_value_path,
                    SrcGenLoweredPropertyPath::NamedNode { predicate_iri }
                        if predicate_iri == "urn:hasMedium"
                ));
                assert!(!distinct_anchors);
                assert_eq!(composed_of_predicate_iri, "urn:composedOf");
                assert!(matches!(
                    constituent_path,
                    Some(SrcGenLoweredPropertyPath::Sequence { items })
                        if items.len() == 2
                ));
                assert!(matches!(
                    mode,
                    SrcGenLocalSetCompatibilityMode::CompositeVsPure {
                        composite_side: SrcGenCompatibilitySide::Left
                    }
                ));
            }
            other => panic!("expected lowered local-set-compatibility query, got {other:?}"),
        }
    }

    #[test]
    fn sparql_relation_projection_pattern_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:closed-world:sparql",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:closed-world",
            )),
        );

        let shape_graph = oxigraph::model::NamedNode::new_unchecked("urn:shape-graph");
        let shape_ir = ShapeIR {
            shape_graph: shape_graph.clone(),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Equipment"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:closed-world:sparql"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_typed_literal(
                    "SELECT $this WHERE { $this ?p ?o . ?p a/rdfs:subClassOf* <urn:Relation> . }",
                    oxigraph::model::vocab::xsd::STRING,
                ),
                oxigraph::model::GraphName::NamedNode(shape_graph),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(matches!(
            lowered.components[0].kind,
            SrcGenComponentKind::Sparql { .. }
        ));
        assert_eq!(lowered.node_shapes[0].supported_constraints, vec![1]);
        assert!(lowered.node_shapes[0].fallback_constraints.is_empty());
        assert!(lowered.fallback_annotations.is_empty());
    }

    #[test]
    fn sparql_closed_world_projection_pattern_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Sparql {
                constraint_node: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:closed-world:projection",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:closed-world-projection",
            )),
        );

        let shape_graph = oxigraph::model::NamedNode::new_unchecked("urn:shape-graph");
        let shape_ir = ShapeIR {
            shape_graph: shape_graph.clone(),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Concept"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:closed-world:projection"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                oxigraph::model::Literal::new_typed_literal(
                    "SELECT $this ?p ?o WHERE { $this ?p ?o . FILTER NOT EXISTS { $this a/rdfs:subClassOf* ?class . ?class sh:property/sh:path ?p . } }",
                    oxigraph::model::vocab::xsd::STRING,
                ),
                oxigraph::model::GraphName::NamedNode(shape_graph),
            )],
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(matches!(
            lowered.components[0].kind,
            SrcGenComponentKind::Sparql { .. }
        ));
        assert_eq!(lowered.node_shapes[0].supported_constraints, vec![1]);
        assert!(lowered.node_shapes[0].fallback_constraints.is_empty());
        assert!(lowered.fallback_annotations.is_empty());
    }

    #[test]
    fn specialization_ready_for_phase2_language_and_property_pair_constraints() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::Property {
                shape: PropShapeID(7),
            },
        );
        components.insert(
            ComponentID(3),
            ComponentDescriptor::LanguageIn {
                languages: vec!["en".to_string(), "fr".to_string()],
            },
        );
        components.insert(
            ComponentID(4),
            ComponentDescriptor::UniqueLang { enabled: true },
        );
        components.insert(
            ComponentID(5),
            ComponentDescriptor::Equals {
                property: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:altName")),
            },
        );
        components.insert(
            ComponentID(6),
            ComponentDescriptor::Disjoint {
                property: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:banned")),
            },
        );
        components.insert(
            ComponentID(7),
            ComponentDescriptor::LessThan {
                property: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:maxValue",
                )),
            },
        );
        components.insert(
            ComponentID(8),
            ComponentDescriptor::LessThanOrEquals {
                property: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:maxInclusive",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person-name",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1), ComponentID(2)],
                property_shapes: vec![PropShapeID(7)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:name",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:name")),
                constraints: vec![
                    ComponentID(3),
                    ComponentID(4),
                    ComponentID(5),
                    ComponentID(6),
                    ComponentID(7),
                    ComponentID(8),
                ],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(
            lowered
                .components
                .iter()
                .all(|component| !component.fallback_only)
        );
        assert!(lowered.node_shapes[0].supported);
        assert!(lowered.property_shapes[0].supported);
    }

    #[test]
    fn specialization_ready_for_phase2_value_range_and_membership_constraints() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::Property {
                shape: PropShapeID(7),
            },
        );
        components.insert(
            ComponentID(3),
            ComponentDescriptor::MinInclusive {
                value: Term::Literal(oxigraph::model::Literal::from(10)),
            },
        );
        components.insert(
            ComponentID(4),
            ComponentDescriptor::MaxExclusive {
                value: Term::Literal(oxigraph::model::Literal::from(20)),
            },
        );
        components.insert(
            ComponentID(5),
            ComponentDescriptor::HasValue {
                value: Term::Literal(oxigraph::model::Literal::from(15)),
            },
        );
        components.insert(
            ComponentID(6),
            ComponentDescriptor::In {
                values: vec![
                    Term::Literal(oxigraph::model::Literal::from(14)),
                    Term::Literal(oxigraph::model::Literal::from(15)),
                    Term::Literal(oxigraph::model::Literal::from(16)),
                ],
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person-age",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1), ComponentID(2)],
                property_shapes: vec![PropShapeID(7)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:age",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:age")),
                constraints: vec![
                    ComponentID(3),
                    ComponentID(4),
                    ComponentID(5),
                    ComponentID(6),
                ],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(
            lowered
                .components
                .iter()
                .all(|component| !component.fallback_only)
        );
        assert!(lowered.node_shapes[0].supported);
        assert!(lowered.property_shapes[0].supported);
    }

    #[test]
    fn specialization_ready_for_phase2_closed_and_qualified_value_shape_constraints() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::Property {
                shape: PropShapeID(7),
            },
        );
        components.insert(
            ComponentID(3),
            ComponentDescriptor::Closed {
                closed: true,
                ignored_properties: vec![
                    Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:ignored")),
                    Term::BlankNode(oxigraph::model::BlankNode::new_unchecked("ignored-bnode")),
                ],
            },
        );
        components.insert(
            ComponentID(4),
            ComponentDescriptor::QualifiedValueShape {
                shape: ID(2),
                min_count: Some(1),
                max_count: Some(2),
                disjoint: Some(true),
            },
        );
        components.insert(
            ComponentID(5),
            ComponentDescriptor::QualifiedValueShape {
                shape: ID(3),
                min_count: None,
                max_count: Some(1),
                disjoint: Some(true),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );
        node_shape_terms.insert(
            ID(2),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:shape:child")),
        );
        node_shape_terms.insert(
            ID(3),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:alt-child",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person-child",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![
                NodeShapeIR {
                    id: ID(1),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                    ))],
                    constraints: vec![ComponentID(1), ComponentID(2), ComponentID(3)],
                    property_shapes: vec![PropShapeID(7)],
                    severity: Severity::Violation,
                    deactivated: false,
                },
                NodeShapeIR {
                    id: ID(2),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:Child"),
                    ))],
                    constraints: Vec::new(),
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
                NodeShapeIR {
                    id: ID(3),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:AltChild"),
                    ))],
                    constraints: Vec::new(),
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
            ],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:child",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:child")),
                constraints: vec![ComponentID(4), ComponentID(5)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(
            lowered
                .components
                .iter()
                .all(|component| !component.fallback_only)
        );
        assert!(lowered.node_shapes.iter().all(|shape| shape.supported));
        assert!(lowered.property_shapes[0].supported);
    }

    #[test]
    fn specialization_ready_for_phase2_logical_and_node_constraints() {
        let mut components = HashMap::new();
        components.insert(ComponentID(1), ComponentDescriptor::Node { shape: ID(2) });
        components.insert(ComponentID(2), ComponentDescriptor::Not { shape: ID(2) });
        components.insert(
            ComponentID(3),
            ComponentDescriptor::And {
                shapes: vec![ID(2), ID(3)],
            },
        );
        components.insert(
            ComponentID(4),
            ComponentDescriptor::Or {
                shapes: vec![ID(2), ID(3)],
            },
        );
        components.insert(
            ComponentID(5),
            ComponentDescriptor::Xone {
                shapes: vec![ID(2), ID(3)],
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );
        node_shape_terms.insert(
            ID(2),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:shape:child")),
        );
        node_shape_terms.insert(
            ID(3),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:alt-child",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person-child",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![
                NodeShapeIR {
                    id: ID(1),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                    ))],
                    constraints: Vec::new(),
                    property_shapes: vec![PropShapeID(7)],
                    severity: Severity::Violation,
                    deactivated: false,
                },
                NodeShapeIR {
                    id: ID(2),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:Child"),
                    ))],
                    constraints: Vec::new(),
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
                NodeShapeIR {
                    id: ID(3),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:AltChild"),
                    ))],
                    constraints: Vec::new(),
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
            ],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:child",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:child")),
                constraints: vec![
                    ComponentID(1),
                    ComponentID(2),
                    ComponentID(3),
                    ComponentID(4),
                    ComponentID(5),
                ],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(
            lowered
                .components
                .iter()
                .all(|component| !component.fallback_only)
        );
        assert!(lowered.property_shapes[0].supported);
    }

    #[test]
    fn specialization_ready_for_phase2_nodekind_and_text_constraints() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::Property {
                shape: PropShapeID(7),
            },
        );
        components.insert(
            ComponentID(3),
            ComponentDescriptor::NodeKind {
                node_kind: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "http://www.w3.org/ns/shacl#Literal",
                )),
            },
        );
        components.insert(ComponentID(4), ComponentDescriptor::MinLength { length: 3 });
        components.insert(
            ComponentID(5),
            ComponentDescriptor::MaxLength { length: 16 },
        );
        components.insert(
            ComponentID(6),
            ComponentDescriptor::Pattern {
                pattern: "^[A-Za-z]+$".to_string(),
                flags: Some("i".to_string()),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );
        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person-name",
            )),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1), ComponentID(2)],
                property_shapes: vec![PropShapeID(7)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:name",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:name")),
                constraints: vec![
                    ComponentID(3),
                    ComponentID(4),
                    ComponentID(5),
                    ComponentID(6),
                ],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms,
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(
            lowered
                .components
                .iter()
                .all(|component| !component.fallback_only)
        );
        assert!(lowered.node_shapes[0].supported);
        assert!(lowered.property_shapes[0].supported);
    }

    #[test]
    fn specialization_ready_for_phase2_node_datatype_and_membership_constraints() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Datatype {
                datatype: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "http://www.w3.org/2001/XMLSchema#string",
                )),
            },
        );
        components.insert(
            ComponentID(2),
            ComponentDescriptor::HasValue {
                value: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:required")),
            },
        );
        components.insert(
            ComponentID(3),
            ComponentDescriptor::In {
                values: vec![
                    Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:required")),
                    Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:alternate")),
                ],
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:shape:item")),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Item"),
                ))],
                constraints: vec![ComponentID(1), ComponentID(2), ComponentID(3)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules: HashMap::new(),
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert!(lowered.fallback_annotations.is_empty());
        assert!(
            lowered
                .components
                .iter()
                .all(|component| !component.fallback_only)
        );
        assert!(lowered.node_shapes[0].supported);
    }

    #[test]
    fn specialization_ready_is_not_disabled_by_rules_presence() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(77),
            Rule::Triple(TripleRule {
                id: RuleID(77),
                subject: TriplePatternTerm::This,
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:inferred"),
                object: TriplePatternTerm::Constant(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:value"),
                )),
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:triple",
                )),
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(77)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(lowered.meta.specialization_ready);
        assert_eq!(lowered.meta.rule_count, 1);
    }

    #[test]
    fn deactivated_rules_do_not_increment_generated_rule_count() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Person")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:person",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(88),
            Rule::Triple(TripleRule {
                id: RuleID(88),
                subject: TriplePatternTerm::This,
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:inferred"),
                object: TriplePatternTerm::Constant(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:value"),
                )),
                condition_shapes: Vec::new(),
                deactivated: true,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:triple",
                )),
            }),
        );

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Person"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules: HashMap::new(),
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 0);
        assert!(lowered.meta.specialization_ready);
    }

    #[test]
    fn triple_rule_with_class_targets_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Equipment")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:equipment",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(11),
            Rule::Triple(TripleRule {
                id: RuleID(11),
                subject: TriplePatternTerm::This,
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:inferred:type"),
                object: TriplePatternTerm::Constant(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:InferredType"),
                )),
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:triple",
                )),
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(11)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Equipment"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(matches!(
            lowered.rules[0].kind,
            SrcGenRuleKind::Triple { .. }
        ));
        assert!(!lowered.rules[0].fallback_only);
        assert_eq!(lowered.rules[0].target_classes, vec!["urn:Equipment"]);
        assert!(lowered.rule_fallback_annotations.is_empty());
    }

    #[test]
    fn triple_rule_with_target_node_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::NodeKind {
                node_kind: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "http://www.w3.org/ns/shacl#IRI",
                )),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:target-node-rule",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(111),
            Rule::Triple(TripleRule {
                id: RuleID(111),
                subject: TriplePatternTerm::This,
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:inferred:type"),
                object: TriplePatternTerm::Constant(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:InferredType"),
                )),
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:target-node",
                )),
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(111)]);

        let target_node = Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:focus"));
        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Node(target_node.clone())],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(!lowered.rules[0].fallback_only);
        assert_eq!(lowered.rules[0].target_nodes, vec![target_node]);
        assert!(lowered.rules[0].target_classes.is_empty());
        assert!(matches!(
            lowered.rules[0].kind,
            SrcGenRuleKind::Triple { .. }
        ));
    }

    #[test]
    fn triple_rule_with_this_object_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Equipment")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:equipment",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(31),
            Rule::Triple(TripleRule {
                id: RuleID(31),
                subject: TriplePatternTerm::This,
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:relatedToSelf"),
                object: TriplePatternTerm::This,
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:self",
                )),
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(31)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Equipment"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(!lowered.rules[0].fallback_only);
        assert!(matches!(
            lowered.rules[0].kind,
            SrcGenRuleKind::Triple {
                object: SrcGenRuleObject::This,
                ..
            }
        ));
    }

    #[test]
    fn triple_rule_with_constant_subject_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Equipment")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:equipment",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(32),
            Rule::Triple(TripleRule {
                id: RuleID(32),
                subject: TriplePatternTerm::Constant(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:constant:subject"),
                )),
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:hasInferred"),
                object: TriplePatternTerm::Constant(Term::Literal(
                    oxigraph::model::Literal::new_simple_literal("x"),
                )),
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:constant-subject",
                )),
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(32)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Equipment"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(!lowered.rules[0].fallback_only);
        assert!(matches!(
            lowered.rules[0].kind,
            SrcGenRuleKind::Triple {
                subject: SrcGenRuleSubject::Constant(_),
                ..
            }
        ));
    }

    #[test]
    fn triple_rule_with_condition_shape_is_specialized_when_shape_resolves() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Equipment")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:equipment",
            )),
        );
        node_shape_terms.insert(
            ID(2),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:condition",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(33),
            Rule::Triple(TripleRule {
                id: RuleID(33),
                subject: TriplePatternTerm::This,
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:inferred:type"),
                object: TriplePatternTerm::Constant(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:InferredType"),
                )),
                condition_shapes: vec![RuleCondition::NodeShape(ID(2))],
                deactivated: false,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:conditional",
                )),
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(33)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![
                NodeShapeIR {
                    id: ID(1),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:Equipment"),
                    ))],
                    constraints: vec![ComponentID(1)],
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
                NodeShapeIR {
                    id: ID(2),
                    targets: Vec::new(),
                    constraints: Vec::new(),
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
            ],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(!lowered.rules[0].fallback_only);
        assert!(matches!(
            lowered.rules[0].kind,
            SrcGenRuleKind::Triple {
                ref condition_shape_iris,
                ..
            } if condition_shape_iris == &vec!["urn:shape:condition".to_string()]
        ));
    }

    #[test]
    fn sparql_rule_with_class_targets_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Equipment")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:equipment",
            )),
        );
        node_shape_terms.insert(
            ID(2),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:condition",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(41),
            Rule::Sparql(SparqlRule {
                id: RuleID(41),
                query: "CONSTRUCT { $this <urn:inferred:edge> <urn:target> . } WHERE { }"
                    .to_string(),
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:sparql",
                )),
                condition_shapes: vec![RuleCondition::NodeShape(ID(2))],
                deactivated: false,
                order: None,
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(41)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![
                NodeShapeIR {
                    id: ID(1),
                    targets: vec![Target::Class(Term::NamedNode(
                        oxigraph::model::NamedNode::new_unchecked("urn:Equipment"),
                    ))],
                    constraints: vec![ComponentID(1)],
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
                NodeShapeIR {
                    id: ID(2),
                    targets: Vec::new(),
                    constraints: Vec::new(),
                    property_shapes: Vec::new(),
                    severity: Severity::Violation,
                    deactivated: false,
                },
            ],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(!lowered.rules[0].fallback_only);
        assert_eq!(lowered.rules[0].target_classes, vec!["urn:Equipment"]);
        assert!(matches!(
            lowered.rules[0].kind,
            SrcGenRuleKind::Sparql {
                ref query,
                ref condition_shape_iris,
                compiled_query: None,
                ref index_requirements,
            } if query.contains("CONSTRUCT")
                && condition_shape_iris == &vec!["urn:shape:condition".to_string()]
                && index_requirements.is_empty()
        ));
    }

    #[test]
    fn property_shape_rule_is_lowered_to_runtime_fallback() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::MinCount { min_count: 1 },
        );

        let mut property_shape_terms = HashMap::new();
        property_shape_terms.insert(
            PropShapeID(7),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:property",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(12),
            Rule::Triple(TripleRule {
                id: RuleID(12),
                subject: TriplePatternTerm::This,
                predicate: oxigraph::model::NamedNode::new_unchecked("urn:inferred:flag"),
                object: TriplePatternTerm::Constant(Term::Literal(
                    oxigraph::model::Literal::new_simple_literal("x"),
                )),
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:property",
                )),
            }),
        );
        let mut prop_shape_rules = HashMap::new();
        prop_shape_rules.insert(PropShapeID(7), vec![RuleID(12)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: Vec::new(),
            property_shapes: vec![PropertyShapeIR {
                id: PropShapeID(7),
                targets: Vec::new(),
                path: Path::Simple(Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:p",
                ))),
                path_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:p")),
                constraints: vec![ComponentID(1)],
                severity: Severity::Violation,
                deactivated: false,
            }],
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms: HashMap::new(),
            property_shape_terms,
            shape_quads: Vec::new(),
            rules,
            node_shape_rules: HashMap::new(),
            prop_shape_rules,
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(lowered.rules[0].fallback_only);
        assert_eq!(lowered.rule_fallback_annotations.len(), 1);
        assert_eq!(lowered.rule_fallback_annotations[0].rule_id, 12);
    }

    #[test]
    fn sparql_rule_lowering_captures_compiled_index_requirements() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Equipment")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:equipment",
            )),
        );

        let mut rules = HashMap::new();
        rules.insert(
            RuleID(51),
            Rule::Sparql(SparqlRule {
                id: RuleID(51),
                query: "PREFIX ex: <urn:> CONSTRUCT { $this ex:flag ?value . } WHERE { $this ^ex:source/ex:value ?value . }".to_string(),
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:sparql:indexed",
                )),
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(51)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Class(Term::NamedNode(
                    oxigraph::model::NamedNode::new_unchecked("urn:Equipment"),
                ))],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: Vec::new(),
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert!(matches!(
            lowered.rules[0].kind,
            SrcGenRuleKind::Sparql {
                compiled_query: Some(_),
                ref index_requirements,
                ..
            } if index_requirements.len() == 2
        ));
    }

    #[test]
    fn sparql_rule_with_advanced_target_is_specialized() {
        let mut components = HashMap::new();
        components.insert(
            ComponentID(1),
            ComponentDescriptor::Class {
                class: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked("urn:Device")),
            },
        );

        let mut node_shape_terms = HashMap::new();
        node_shape_terms.insert(
            ID(1),
            Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                "urn:shape:advanced-target",
            )),
        );

        let selector = Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
            "urn:selector:advanced",
        ));
        let mut rules = HashMap::new();
        rules.insert(
            RuleID(200),
            Rule::Sparql(SparqlRule {
                id: RuleID(200),
                query: "CONSTRUCT { $this <urn:inferred:edge> <urn:target> . } WHERE { }"
                    .to_string(),
                source_term: Term::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:rule:advanced-target",
                )),
                condition_shapes: Vec::new(),
                deactivated: false,
                order: None,
            }),
        );
        let mut node_shape_rules = HashMap::new();
        node_shape_rules.insert(ID(1), vec![RuleID(200)]);

        let shape_ir = ShapeIR {
            shape_graph: oxigraph::model::NamedNode::new_unchecked("urn:shape-graph"),
            data_graph: Some(oxigraph::model::NamedNode::new_unchecked("urn:data-graph")),
            node_shapes: vec![NodeShapeIR {
                id: ID(1),
                targets: vec![Target::Advanced(selector.clone())],
                constraints: vec![ComponentID(1)],
                property_shapes: Vec::new(),
                severity: Severity::Violation,
                deactivated: false,
            }],
            property_shapes: Vec::new(),
            components,
            component_templates: HashMap::new(),
            shape_templates: HashMap::new(),
            shape_template_cache: HashMap::new(),
            node_shape_terms,
            property_shape_terms: HashMap::new(),
            shape_quads: vec![oxigraph::model::Quad::new(
                oxigraph::model::NamedNode::new_unchecked("urn:selector:advanced"),
                oxigraph::model::NamedNode::new_unchecked("http://www.w3.org/ns/shacl#select"),
                Term::Literal(oxigraph::model::Literal::new_simple_literal(
                    "SELECT ?this WHERE { ?this a <urn:Device> . }",
                )),
                oxigraph::model::GraphName::NamedNode(oxigraph::model::NamedNode::new_unchecked(
                    "urn:shape-graph",
                )),
            )],
            rules,
            node_shape_rules,
            prop_shape_rules: HashMap::new(),
            features: FeatureToggles::default(),
        };

        let lowered = lower_shape_ir(&shape_ir).unwrap();
        assert_eq!(lowered.meta.rule_count, 1);
        assert_eq!(lowered.rules.len(), 1);
        assert!(!lowered.rules[0].fallback_only);
        assert_eq!(
            lowered.rules[0].target_advanced_select_queries,
            vec!["SELECT ?this WHERE { ?this a <urn:Device> . }".to_string()]
        );
        assert!(lowered.rule_fallback_annotations.is_empty());
    }
}
