"""Ontology-focused SHACL and RDF quads validation tests."""

from __future__ import annotations

from importlib import resources
import json
import os
from copy import deepcopy
from pathlib import Path

import pytest
from rdflib import Dataset
from rdflib import Graph
from pyshacl import validate as shacl_validate

from cf_ontology import OntologyManager, ingest_rdf_documents
from cf_ontology.datatypes_mapping import (
    normalize_parameter_type,
    parameter_type_to_literal_datatype,
    normalize_port_literal_datatype,
    supported_port_literal_datatypes,
)
from cf_ontology.quad_store import QuadStore
from cf_ontology.rdf_document import load_rdf_document
from cf_ontology.semantics_paths import resolve_semantics_db_path, resolve_semantics_dir


CF_BASE = "https://cogniflow.odea-project.org/cf#"
CFRUN_BASE = f"{CF_BASE}runner/"
_STRUCTURED_FORMAT = "json" + "-ld"
_CONTEXT_FILENAME = "cf_context." + "json" + "ld"


def _repo_root() -> Path:
    return Path(__file__).resolve().parents[3]


def _resolve_steps_document(module_name: str, repo_fallback: Path) -> Path:
    """
    Prefer the checked-out repository manifest for deterministic repo tests.
    Fall back to installed package resources when the repo path is unavailable.
    """
    if repo_fallback.is_file():
        return repo_fallback
    try:
        candidate = resources.files(module_name) / "steps.nq"
        if candidate.is_file():
            with resources.as_file(candidate) as resolved:
                return Path(resolved).resolve()
    except (ModuleNotFoundError, FileNotFoundError, ImportError):
        pass
    return repo_fallback


def _find_step_by_suffix(steps: list[dict], suffix: str) -> dict:
    for step in steps:
        if str(step.get("@id", "")).endswith(suffix):
            return step
    raise AssertionError(f"Step ending with '{suffix}' not found in DTO payload.")


def _find_named(items: list[dict], key: str, expected: str) -> dict:
    for item in items:
        if str(item.get(key, "")) == expected:
            return item
    raise AssertionError(f"Entry with {key}='{expected}' not found.")


def _extend_string_values(target: list[str], value: object) -> None:
    if isinstance(value, list):
        target.extend(str(item) for item in value)
    elif isinstance(value, str):
        target.append(value)


def _has_type(node: dict, expected: str) -> bool:
    node_type = node.get("@type")
    if isinstance(node_type, str):
        return node_type == expected
    if isinstance(node_type, list):
        return expected in node_type
    return False


def _contains_supported_pipeline_example(document: dict) -> bool:
    graph = document.get("@graph")
    nodes = graph if isinstance(graph, list) else [document]
    for node in nodes:
        if not isinstance(node, dict):
            continue
        if _has_type(node, "cf:ProcessingPipeline") or _has_type(
            node, f"{CF_BASE}ProcessingPipeline"
        ):
            return True
    return False


def _contains_legacy_cogniflow_url(value: object) -> bool:
    if isinstance(value, str):
        legacy_host = "cogniflow" + ".org"
        return legacy_host in value.lower()
    if isinstance(value, list):
        return any(_contains_legacy_cogniflow_url(item) for item in value)
    if isinstance(value, dict):
        return any(_contains_legacy_cogniflow_url(item) for item in value.values())
    return False


def _id_has_suffix(value: object, suffix: str) -> bool:
    return str(value).endswith(suffix)


def _canonical_context() -> dict[str, str]:
    return {
        "adf": "http://allotrope.org/datamodel#",
        "cf": CF_BASE,
        "cfrun": CFRUN_BASE,
        "owl": "http://www.w3.org/2002/07/owl#",
        "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
        "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
        "schema": "https://schema.org/",
        "sh": "http://www.w3.org/ns/shacl#",
        "skos": "http://www.w3.org/2004/02/skos/core#",
        "xsd": "http://www.w3.org/2001/XMLSchema#",
    }


def _document_from_dataset(dataset: Dataset) -> dict:
    serialized = dataset.serialize(
        format=_STRUCTURED_FORMAT,
        context=_canonical_context(),
        auto_compact=True,
    )
    if isinstance(serialized, bytes):
        serialized = serialized.decode("utf-8")
    payload = json.loads(serialized)
    if isinstance(payload, list):
        return {"@context": _canonical_context(), "@graph": payload}
    if isinstance(payload, dict):
        if "@graph" in payload:
            if "@context" not in payload:
                payload["@context"] = _canonical_context()
            return payload
        return {"@context": _canonical_context(), "@graph": [payload]}
    raise ValueError("Unsupported RDF document payload generated from N-Quads dataset.")


def _load_graph_resource(path: Path) -> Graph:
    if path.suffix.lower() == ".nq":
        dataset = Dataset()
        dataset.parse(str(path), format="nquads")
        graph = Graph()
        for named_graph in dataset.graphs():
            for subj, pred, obj in named_graph.triples((None, None, None)):
                graph.add((subj, pred, obj))
        return graph
    return Graph().parse(str(path), format=_STRUCTURED_FORMAT)


def _load_ontology_resource(relative_parts: tuple[str, ...]) -> dict:
    path = _repo_root().joinpath("sandcastle", "cf_ontology", "src", "cf_ontology", "ontology", *relative_parts)
    if path.suffix.lower() == ".nq":
        dataset = Dataset()
        dataset.parse(str(path), format="nquads")
        return _document_from_dataset(dataset)
    return json.loads(path.read_text(encoding="utf-8"))


def _load_json_document(path: Path) -> dict:
    return load_rdf_document(path, label="test RDF document")


def _cf_ontology_source_path(*parts: str) -> Path:
    return _repo_root().joinpath("sandcastle", "cf_ontology", "src", "cf_ontology", *parts)


def _repo_step_manifest_paths() -> list[Path]:
    root = _repo_root() / "sandcastle" / "cf_basic_steps"
    return [
        root / "cf_basic_dev" / "src" / "cf_basic_dev" / "steps.nq",
        root / "cf_basic_io" / "src" / "cf_basic_io" / "steps.nq",
        root / "cf_basic_signal" / "src" / "cf_basic_signal" / "steps.nq",
        root / "cf_basic_sinks" / "src" / "cf_basic_sinks" / "steps.nq",
    ]


def _is_url(text: str) -> bool:
    lowered = text.lower()
    return lowered.startswith("http://") or lowered.startswith("https://") or lowered.startswith(
        "file://"
    )


def _resolve_relative_context_paths(document: dict, base_dir: Path) -> dict:
    """
    Rewrite relative RDF document @context entries to absolute filesystem paths.
    """
    payload = deepcopy(document)

    def _map_context_value(value):
        if isinstance(value, str):
            if value.replace("\\", "/").endswith(_CONTEXT_FILENAME):
                return _canonical_context()
            if _is_url(value):
                return value
            candidate = Path(value)
            if not candidate.is_absolute():
                candidate = (base_dir / candidate).resolve()
            return str(candidate) if candidate.exists() else value
        if isinstance(value, list):
            return [_map_context_value(item) for item in value]
        if isinstance(value, dict):
            return {key: _map_context_value(inner) for key, inner in value.items()}
        return value

    def _walk(node):
        if isinstance(node, dict):
            out = {}
            for key, value in node.items():
                out[key] = _map_context_value(value) if key == "@context" else _walk(value)
            return out
        if isinstance(node, list):
            return [_walk(item) for item in node]
        return node

    return _walk(payload)


def test_packaged_ontology_conforms_shacl():
    """
    Validate packaged ontology + shapes using OntologyManager SHACL validation.
    """
    om = OntologyManager()
    result = om.validate_ontology(inference="rdfs")
    assert bool(result.get("conforms", False)) is True


def test_ontology_manager_exports_nquads_without_packaged_context_file():
    context_path = _cf_ontology_source_path("ontology", "cf_context.nq")
    assert not context_path.exists()

    om = OntologyManager()
    exported = om.get_graph_nquads()
    assert CF_BASE in exported

    dataset = Dataset()
    dataset.parse(data=exported, format="nquads")
    total_triples = sum(len(graph) for graph in dataset.graphs())
    assert total_triples > 0


def test_pipeline_example_document_conforms_shacl(monkeypatch: pytest.MonkeyPatch):
    """
    Validate pipeline example RDF documents with SHACL.
    This validates semantics only; it does not execute pipelines.
    """
    monkeypatch.setenv("CF_ENABLE_STEP_PACKAGES", "0")
    ingest_rdf_documents(
        _repo_step_manifest_paths(),
        graph_type="step",
        package="repo-step-manifests",
    )
    om = OntologyManager()
    examples_dir = (
        _repo_root() / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine" / "examples"
    )
    files = sorted(examples_dir.glob("*.nq"))
    assert files, "No pipeline examples found to validate."
    validated_count = 0

    for path in files:
        doc = load_rdf_document(path, label="test pipeline example")
        doc = _resolve_relative_context_paths(doc, path.parent)
        if not _contains_supported_pipeline_example(doc):
            continue
        if _contains_legacy_cogniflow_url(doc):
            continue
        result = om.validate_pipeline_document(doc)
        validated_count += 1
        assert bool(result.get("conforms", False)) is True, (
            f"{path.name} failed SHACL validation:\n{result.get('report_text', '')}"
        )
    if validated_count == 0:
        pytest.skip("No canonical pipeline examples without legacy-host URLs found.")


def test_combine_documents_merges_stub_nodes_with_full_definitions():
    merged = OntologyManager._combine_documents(
        [
            {
                "@context": {"cf": "https://cogniflow.odea-project.org/cf#"},
                "@graph": [
                    {
                        "@id": "cf:StepA",
                        "cf:hasParameter": {"@id": "cf:StepA_param_value"},
                    },
                    {"@id": "cf:StepA_param_value"},
                ],
            },
            {
                "@context": {"cf": "https://cogniflow.odea-project.org/cf#"},
                "@graph": [
                    {
                        "@id": "cf:StepA_param_value",
                        "cf:parameterName": "value",
                        "cf:defaultValue": 1,
                        "cf:valueContract": {"@id": "cf:StepA_param_value_contract"},
                    }
                ],
            },
        ]
    )

    graph = merged.get("@graph")
    assert isinstance(graph, list)
    by_id = {
        node.get("@id"): node
        for node in graph
        if isinstance(node, dict) and isinstance(node.get("@id"), str)
    }
    assert by_id["cf:StepA_param_value"]["cf:parameterName"] == "value"
    assert by_id["cf:StepA_param_value"]["cf:defaultValue"] == 1
    assert by_id["cf:StepA_param_value"]["cf:valueContract"] == {
        "@id": "cf:StepA_param_value_contract"
    }


def test_step_document_ingest_export_roundtrip_is_valid_document():
    """
    Ingest a step RDF document into quads, export it from DuckDB as N-Quads, and parse
    the export with RDFLib to verify syntactic/semantic validity.
    """
    basic_signal_steps = _resolve_steps_document(
        module_name="cf_basic_signal",
        repo_fallback=(
            _repo_root()
            / "sandcastle"
            / "cf_basic_steps"
            / "cf_basic_signal"
            / "src"
            / "cf_basic_signal"
            / "steps.nq"
        ),
    )
    basic_io_steps = _resolve_steps_document(
        module_name="cf_basic_io",
        repo_fallback=(
            _repo_root()
            / "sandcastle"
            / "cf_basic_steps"
            / "cf_basic_io"
            / "src"
            / "cf_basic_io"
            / "steps.nq"
        ),
    )
    ingest_rdf_documents([basic_signal_steps], graph_type="step", package="cf.basic.signal")
    ingest_rdf_documents([basic_io_steps], graph_type="step", package="cf.basic.io")
    basic_signal_doc = _load_json_document(basic_signal_steps)
    expected_average = _find_step_by_suffix(basic_signal_doc.get("@graph", []), "AverageStep")

    semantics_dir = resolve_semantics_dir()
    steps_db = resolve_semantics_db_path(semantics_dir, kind="steps")
    store = QuadStore(db_path=steps_db, semantics_dir=semantics_dir, read_only=True)
    exported = store.export_nquads()

    dataset = Dataset()
    dataset.parse(data=exported, format="nquads")
    total_triples = sum(len(graph) for graph in dataset.graphs())
    assert total_triples > 0


def test_pipeline_run_requires_session_link_in_shacl() -> None:
    shapes_path = (
        _repo_root()
        / "sandcastle"
        / "cf_ontology"
        / "src"
        / "cf_ontology"
        / "ontology"
        / "shapes"
        / "pipeline.shapes.nq"
    )
    shapes_graph = _load_graph_resource(shapes_path)

    missing_session_doc = {
        "@context": {
            "cf": CF_BASE,
            "ex": "http://example.org/",
            "xsd": "http://www.w3.org/2001/XMLSchema#",
        },
        "@graph": [
            {
                "@id": "ex:run_without_session",
                "@type": "cf:PipelineRun",
                "cf:runIndex": {"@type": "xsd:integer", "@value": 0},
            }
        ],
    }
    failing_graph = Graph().parse(data=json.dumps(missing_session_doc), format=_STRUCTURED_FORMAT)
    failing_conforms, _, _ = shacl_validate(
        failing_graph,
        shacl_graph=shapes_graph,
        inference="rdfs",
        abort_on_first=False,
        advanced=True,
    )
    assert bool(failing_conforms) is False

    valid_doc = {
        "@context": {
            "cf": CF_BASE,
            "ex": "http://example.org/",
            "xsd": "http://www.w3.org/2001/XMLSchema#",
        },
        "@graph": [
            {
                "@id": "ex:session_1",
                "@type": "cf:PipelineSession",
                "cf:sessionId": "session-1",
            },
            {
                "@id": "ex:run_with_session",
                "@type": "cf:PipelineRun",
                "cf:inSession": {"@id": "ex:session_1"},
                "cf:runIndex": {"@type": "xsd:integer", "@value": 1},
            },
        ],
    }
    passing_graph = Graph().parse(data=json.dumps(valid_doc), format=_STRUCTURED_FORMAT)
    passing_conforms, _, _ = shacl_validate(
        passing_graph,
        shacl_graph=shapes_graph,
        inference="rdfs",
        abort_on_first=False,
        advanced=True,
    )
    assert bool(passing_conforms) is True


def test_ontology_backed_datatype_normalization_uses_canonical_registry() -> None:
    assert normalize_parameter_type("cf:number") == "cf:double"
    assert normalize_parameter_type(f"{CF_BASE}float") == (
        f"{CF_BASE}double"
    )
    assert normalize_parameter_type("xsd:boolean") == "cf:boolean"
    assert parameter_type_to_literal_datatype("cf:number") == "xsd:double"
    assert parameter_type_to_literal_datatype(f"{CF_BASE}number") == (
        "http://www.w3.org/2001/XMLSchema#double"
    )
    assert normalize_port_literal_datatype("cf:number") == "xsd:double"
    assert normalize_port_literal_datatype(f"{CF_BASE}number") == (
        "http://www.w3.org/2001/XMLSchema#double"
    )
    assert normalize_port_literal_datatype("xsd:bolean") == "xsd:boolean"


def test_port_datatype_shape_accepts_registry_v0_canonical_set_only() -> None:
    shapes_path = (
        _repo_root()
        / "sandcastle"
        / "cf_ontology"
        / "src"
        / "cf_ontology"
        / "ontology"
        / "shapes"
        / "ports.shapes.nq"
    )
    shapes_graph = _load_graph_resource(shapes_path)

    context = {
        "cf": CF_BASE,
        "ex": "http://example.org/",
        "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
        "xsd": "http://www.w3.org/2001/XMLSchema#",
    }

    for index, datatype in enumerate(supported_port_literal_datatypes()):
        valid_doc = {
            "@context": context,
            "@graph": [
                {
                    "@id": f"ex:port_{index}",
                    "@type": "cf:Port",
                    "cf:valueContract": {"@id": f"ex:port_{index}_contract"},
                },
                {
                    "@id": f"ex:port_{index}_contract",
                    "@type": "cf:ValueContract",
                    "cf:elementType": {"@id": datatype},
                    "cf:shapeKind": {"@id": "cf:shape_single"},
                }
            ],
        }
        valid_graph = Graph().parse(data=json.dumps(valid_doc), format=_STRUCTURED_FORMAT)
        conforms, _, _ = shacl_validate(
            valid_graph,
            shacl_graph=shapes_graph,
            inference="rdfs",
            abort_on_first=False,
            advanced=True,
        )
        assert bool(conforms) is True, f"Expected registry datatype {datatype} to pass SHACL."

    for datatype in ("xsd:dateTime", "cf:boolean"):
        invalid_doc = {
            "@context": context,
            "@graph": [
                {
                    "@id": "ex:invalid_port",
                    "@type": "cf:Port",
                    "cf:valueContract": {"@id": "ex:invalid_port_contract"},
                },
                {
                    "@id": "ex:invalid_port_contract",
                    "@type": "cf:ValueContract",
                    "cf:elementType": {"@id": datatype},
                    "cf:shapeKind": {"@id": "cf:shape_single"},
                }
            ],
        }
        invalid_graph = Graph().parse(data=json.dumps(invalid_doc), format=_STRUCTURED_FORMAT)
        conforms, _, _ = shacl_validate(
            invalid_graph,
            shacl_graph=shapes_graph,
            inference="rdfs",
            abort_on_first=False,
            advanced=True,
        )
        assert bool(conforms) is False, f"Expected non-canonical datatype {datatype} to fail SHACL."


def test_runtime_binding_vocab_matches_registry_supported_port_types() -> None:
    supported_types = set(supported_port_literal_datatypes())
    mapping_doc = _load_ontology_resource(("vocab", "datatypes_mapping.nq"))
    bindings_doc = _load_ontology_resource(("vocab", "runtime_storage_bindings.nq"))

    port_binding_rows = [
        node for node in mapping_doc.get("@graph", []) if _has_type(node, "cf:PortTypeBindingMapping")
    ]
    runtime_bindings = [node for node in bindings_doc.get("@graph", []) if _has_type(node, "cf:RuntimeBinding")]

    mapped_port_types = {node["cf:mappedPortType"]["@id"] for node in port_binding_rows}
    runtime_binding_types = {node["cf:forElementType"]["@id"] for node in runtime_bindings}
    runtime_binding_ids = {node["@id"] for node in runtime_bindings}
    referenced_runtime_binding_ids = {node["cf:mappedRuntimeBinding"]["@id"] for node in port_binding_rows}

    assert mapped_port_types == supported_types
    assert runtime_binding_types == supported_types
    assert referenced_runtime_binding_ids == runtime_binding_ids


def test_cf_ontology_local_registry_file_is_removed() -> None:
    assert not _cf_ontology_source_path("type_registry.v0.json").exists()
    source = _cf_ontology_source_path("datatypes_mapping.py").read_text(encoding="utf-8")
    assert "type_registry.v0.json" not in source


def test_storage_binding_vocab_keeps_storage_only_datetime_metadata() -> None:
    bindings_doc = _load_ontology_resource(("vocab", "runtime_storage_bindings.nq"))

    storage_bindings = [node for node in bindings_doc.get("@graph", []) if _has_type(node, "cf:StorageBinding")]
    storage_binding_types = {node["cf:forElementType"]["@id"] for node in storage_bindings}
    datetime_storage_binding = next(
        node for node in storage_bindings if node.get("@id") == "cf:storage_binding_xsd_dateTime_duckdb_v1"
    )

    assert "xsd:dateTime" in storage_binding_types
    assert datetime_storage_binding["cf:duckdbType"] == "TIMESTAMP"


def test_port_value_contract_shape_binding_and_table_schema_are_valid() -> None:
    shapes_path = (
        _repo_root()
        / "sandcastle"
        / "cf_ontology"
        / "src"
        / "cf_ontology"
        / "ontology"
        / "shapes"
        / "ports.shapes.nq"
    )
    shapes_graph = _load_graph_resource(shapes_path)

    context = {
        "cf": CF_BASE,
        "ex": "http://example.org/",
        "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
        "xsd": "http://www.w3.org/2001/XMLSchema#",
    }

    valid_doc = {
        "@context": context,
        "@graph": [
            {
                "@id": "ex:runtime_binding_double",
                "@type": "cf:RuntimeBinding",
                "cf:forElementType": {"@id": "xsd:double"},
                "cf:runtimeKind": "DOUBLE",
                "cf:abiStruct": "double",
                "cf:typeVersion": {"@type": "xsd:integer", "@value": 1},
            },
            {
                "@id": "ex:storage_binding_double",
                "@type": "cf:StorageBinding",
                "cf:forElementType": {"@id": "xsd:double"},
                "cf:storageProfile": "native",
                "cf:duckdbType": "DOUBLE",
                "cf:typeVersion": {"@type": "xsd:integer", "@value": 1},
            },
            {
                "@id": "ex:vector_contract",
                "@type": "cf:ValueContract",
                "cf:elementType": {"@id": "xsd:double"},
                "cf:shapeKind": {"@id": "cf:shape_vector"},
                "cf:runtimeBinding": {"@id": "ex:runtime_binding_double"},
                "cf:storageBinding": {"@id": "ex:storage_binding_double"},
            },
            {
                "@id": "ex:table_column_value",
                "@type": "cf:TableColumnSpec",
                "cf:columnName": "value",
                "cf:columnElementType": {"@id": "xsd:double"},
                "cf:columnNullable": {"@type": "xsd:boolean", "@value": False},
            },
            {
                "@id": "ex:table_schema_v1",
                "@type": "cf:TableSchema",
                "cf:hasColumn": {"@id": "ex:table_column_value"},
            },
            {
                "@id": "ex:table_contract",
                "@type": "cf:ValueContract",
                "cf:elementType": {"@id": "xsd:double"},
                "cf:shapeKind": {"@id": "cf:shape_table"},
                "cf:tableSchema": {"@id": "ex:table_schema_v1"},
            },
            {
                "@id": "ex:vector_port",
                "@type": "cf:Port",
                "cf:valueContract": {"@id": "ex:vector_contract"},
            },
            {
                "@id": "ex:table_port",
                "@type": "cf:Port",
                "cf:valueContract": {"@id": "ex:table_contract"},
            },
        ],
    }

    valid_graph = Graph().parse(data=json.dumps(valid_doc), format=_STRUCTURED_FORMAT)
    conforms, _, _ = shacl_validate(
        valid_graph,
        shacl_graph=shapes_graph,
        inference="rdfs",
        abort_on_first=False,
        advanced=True,
    )
    assert bool(conforms) is True


def test_port_value_contract_shape_binding_and_table_schema_reject_invalid() -> None:
    shapes_path = (
        _repo_root()
        / "sandcastle"
        / "cf_ontology"
        / "src"
        / "cf_ontology"
        / "ontology"
        / "shapes"
        / "ports.shapes.nq"
    )
    shapes_graph = _load_graph_resource(shapes_path)

    context = {
        "cf": CF_BASE,
        "ex": "http://example.org/",
        "xsd": "http://www.w3.org/2001/XMLSchema#",
    }

    invalid_docs = [
        {
            "@context": context,
            "@graph": [
                {
                    "@id": "ex:invalid_contract_shape",
                    "@type": "cf:ValueContract",
                    "cf:elementType": {"@id": "xsd:double"},
                    "cf:shapeKind": {"@id": "cf:shape_cube"},
                },
                {
                    "@id": "ex:invalid_port_shape",
                    "@type": "cf:Port",
                    "cf:valueContract": {"@id": "ex:invalid_contract_shape"},
                },
            ],
        },
        {
            "@context": context,
            "@graph": [
                {
                    "@id": "ex:invalid_contract_table",
                    "@type": "cf:ValueContract",
                    "cf:elementType": {"@id": "xsd:double"},
                    "cf:shapeKind": {"@id": "cf:shape_table"},
                },
                {
                    "@id": "ex:invalid_port_table",
                    "@type": "cf:Port",
                    "cf:valueContract": {"@id": "ex:invalid_contract_table"},
                },
            ],
        },
        {
            "@context": context,
            "@graph": [
                {
                    "@id": "ex:invalid_runtime_binding",
                    "@type": "cf:RuntimeBinding",
                    "cf:forElementType": {"@id": "xsd:double"},
                    "cf:runtimeKind": "DOUBLE",
                    "cf:typeVersion": {"@type": "xsd:integer", "@value": 1},
                }
            ],
        },
        {
            "@context": context,
            "@graph": [
                {
                    "@id": "ex:invalid_table_column",
                    "@type": "cf:TableColumnSpec",
                    "cf:columnName": "ts",
                    "cf:columnElementType": {"@id": "xsd:duration"},
                    "cf:columnNullable": {"@type": "xsd:boolean", "@value": False},
                },
                {
                    "@id": "ex:invalid_table_schema",
                    "@type": "cf:TableSchema",
                    "cf:hasColumn": {"@id": "ex:invalid_table_column"},
                },
                {
                    "@id": "ex:invalid_contract_table_col",
                    "@type": "cf:ValueContract",
                    "cf:elementType": {"@id": "xsd:double"},
                    "cf:shapeKind": {"@id": "cf:shape_table"},
                    "cf:tableSchema": {"@id": "ex:invalid_table_schema"},
                },
            ],
        },
    ]

    for invalid_doc in invalid_docs:
        invalid_graph = Graph().parse(data=json.dumps(invalid_doc), format=_STRUCTURED_FORMAT)
        conforms, _, _ = shacl_validate(
            invalid_graph,
            shacl_graph=shapes_graph,
            inference="rdfs",
            abort_on_first=False,
            advanced=True,
        )
        assert bool(conforms) is False


def test_step_package_value_contract_adoption_conforms_shacl() -> None:
    shapes_path = (
        _repo_root()
        / "sandcastle"
        / "cf_ontology"
        / "src"
        / "cf_ontology"
        / "ontology"
        / "shapes"
        / "ports.shapes.nq"
    )
    shapes_graph = _load_graph_resource(shapes_path)

    step_paths = [
        _repo_root()
        / "sandcastle"
        / "cf_basic_steps"
        / "cf_basic_dev"
        / "src"
        / "cf_basic_dev"
        / "steps.nq",
        _repo_root()
        / "sandcastle"
        / "cf_basic_steps"
        / "cf_basic_io"
        / "src"
        / "cf_basic_io"
        / "steps.nq",
        _repo_root()
        / "sandcastle"
        / "cf_basic_steps"
        / "cf_basic_signal"
        / "src"
        / "cf_basic_signal"
        / "steps.nq",
        _repo_root()
        / "sandcastle"
        / "cf_basic_steps"
        / "cf_basic_sinks"
        / "src"
        / "cf_basic_sinks"
        / "steps.nq",
    ]
    expected_vector_contracts_by_package = {
        "cf_basic_dev": {
            "basic-dev/InlineSource_output_data": ("xsd:double", "cf:shape_vector"),
        },
        "cf_basic_io": {
            "basic-io/HandleSinkStep_input_data": ("xsd:double", "cf:shape_vector"),
        },
        "cf_basic_signal": {
            "basic-signal/AverageStep_input_values": ("xsd:double", "cf:shape_vector"),
            "basic-signal/FifoWindowBufferStep_output_values": ("xsd:double", "cf:shape_vector"),
        },
    }

    for step_path in step_paths:
        doc = _load_json_document(step_path)
        graph_nodes = doc.get("@graph")
        assert isinstance(graph_nodes, list)
        by_id = {node.get("@id"): node for node in graph_nodes if isinstance(node, dict)}
        expected_vector_contracts = expected_vector_contracts_by_package.get(step_path.parent.name, {})
        observed_vector_contracts: set[str] = set()

        typed_ports = [
            node
            for node in graph_nodes
            if isinstance(node, dict)
            and (
                _has_type(node, "cf:InputPort")
                or _has_type(node, "cf:OutputPort")
                or _has_type(node, "cf:Port")
            )
            and isinstance(node.get("cf:valueContract"), dict)
            and isinstance(node["cf:valueContract"].get("@id"), str)
        ]
        assert typed_ports, f"No contracted ports found in {step_path.name}."

        for port in typed_ports:
            contract_ref = port.get("cf:valueContract")
            assert isinstance(contract_ref, dict) and isinstance(contract_ref.get("@id"), str), (
                f"Missing valueContract on port {port.get('@id')} in {step_path.name}."
            )
            contract = by_id.get(contract_ref["@id"])
            assert isinstance(contract, dict)
            assert _has_type(contract, "cf:ValueContract")
            port_id = port.get("@id")
            matched_suffix = next(
                (suffix for suffix in expected_vector_contracts if _id_has_suffix(port_id, suffix)),
                None,
            )
            if matched_suffix:
                expected_element_type, expected_shape = expected_vector_contracts[matched_suffix]
                observed_vector_contracts.add(matched_suffix)
                assert contract.get("cf:elementType", {}).get("@id") == expected_element_type
                assert contract.get("cf:shapeKind", {}).get("@id") == expected_shape
            else:
                assert isinstance(contract.get("cf:elementType", {}).get("@id"), str)
                assert contract.get("cf:shapeKind", {}).get("@id") in {
                    "cf:shape_single",
                    "cf:shape_vector",
                    "cf:shape_matrix",
                    "cf:shape_table",
                }

        assert observed_vector_contracts == set(expected_vector_contracts), (
            f"Missing selected migrated vector ports in {step_path.name}: "
            f"{set(expected_vector_contracts) - observed_vector_contracts}"
        )

        selected_graph_nodes: list[dict] = []
        selected_ids: set[str] = set()
        for port in typed_ports:
            port_id = port.get("@id")
            if isinstance(port_id, str) and port_id not in selected_ids:
                port_copy = deepcopy(port)
                # Isolate contract adoption checks from legacy uncertainty typing noise.
                port_copy.pop("cf:uncertaintyFor", None)
                selected_graph_nodes.append(port_copy)
                selected_ids.add(port_id)

            contract_ref = port.get("cf:valueContract", {})
            contract_id = contract_ref.get("@id") if isinstance(contract_ref, dict) else None
            if isinstance(contract_id, str) and contract_id not in selected_ids:
                contract = by_id.get(contract_id)
                if isinstance(contract, dict):
                    selected_graph_nodes.append(deepcopy(contract))
                    selected_ids.add(contract_id)

        reduced_doc = {"@context": doc.get("@context"), "@graph": selected_graph_nodes}
        resolved_doc = _resolve_relative_context_paths(reduced_doc, step_path.parent)
        data_graph = Graph().parse(data=json.dumps(resolved_doc), format=_STRUCTURED_FORMAT)
        conforms, _, _ = shacl_validate(
            data_graph,
            shacl_graph=shapes_graph,
            inference="rdfs",
            abort_on_first=False,
            advanced=True,
        )
        assert bool(conforms) is True, f"Adopted step manifest failed SHACL: {step_path.name}"


def test_pipeline_examples_reference_step_manifests_with_value_contracts() -> None:
    example_paths = [
        _repo_root()
        / "sandcastle"
        / "cf_pipeline"
        / "cf_pipeline_engine"
        / "examples"
        / "omp_reduction_parallel.nq",
        _repo_root()
        / "sandcastle"
        / "cf_pipeline"
        / "cf_pipeline_engine"
        / "examples"
        / "opcua_fifo_avg_to_duckdb_parquet_triggered.nq",
        _repo_root()
        / "sandcastle"
        / "cf_pipeline"
        / "cf_pipeline_engine"
        / "examples"
        / "opcua_temp_fifo_avg_to_duckdb_parquet_triggered.nq",
    ]

    for example_path in example_paths:
        doc = load_rdf_document(example_path, label="pipeline example")
        resolved = _resolve_relative_context_paths(doc, example_path.parent)

        steps_paths: list[str] = []
        if isinstance(resolved, dict) and "@graph" in resolved:
            graph_nodes = resolved.get("@graph")
            if isinstance(graph_nodes, list):
                for node in graph_nodes:
                    if isinstance(node, dict) and node.get("@type") == "cf:StepsHeader":
                        _extend_string_values(steps_paths, node.get("cf:stepsPath", []))
        elif isinstance(resolved, dict):
            header = resolved.get("cf:hasStepsHeader", {})
            if isinstance(header, dict):
                _extend_string_values(steps_paths, header.get("cf:stepsPath", []))

        assert steps_paths, f"No step manifests referenced by {example_path.name}."
        for step_path_text in steps_paths:
            step_path = Path(step_path_text)
            if not step_path.is_absolute():
                step_path = (example_path.parent / step_path).resolve()
            assert step_path.is_file(), f"Missing referenced steps manifest: {step_path}"
            step_doc = _load_json_document(step_path)
            step_graph = step_doc.get("@graph", [])
            typed_ports = [
                node
                for node in step_graph
                if isinstance(node, dict)
                and (
                    _has_type(node, "cf:InputPort")
                    or _has_type(node, "cf:OutputPort")
                    or _has_type(node, "cf:Port")
                )
                and isinstance(node.get("cf:valueContract"), dict)
                and isinstance(node["cf:valueContract"].get("@id"), str)
            ]
            assert typed_ports, f"No contracted ports in referenced manifest {step_path.name}."
            assert all("cf:valueContract" in port for port in typed_ports), (
                f"Referenced manifest lacks valueContract adoption: {step_path.name}"
            )


def test_step_package_value_contract_uses_supported_shape_kind() -> None:
    step_path = (
        _repo_root()
        / "sandcastle"
        / "cf_basic_steps"
        / "cf_basic_dev"
        / "src"
        / "cf_basic_dev"
        / "steps.nq"
    )
    doc = _load_json_document(step_path)
    graph_nodes = doc.get("@graph")
    assert isinstance(graph_nodes, list)

    by_id = {node.get("@id"): node for node in graph_nodes if isinstance(node, dict)}
    typed_port = next(
        node
        for node in graph_nodes
        if isinstance(node, dict)
        and (_has_type(node, "cf:InputPort") or _has_type(node, "cf:OutputPort") or _has_type(node, "cf:Port"))
        and isinstance(node.get("cf:valueContract"), dict)
        and isinstance(node["cf:valueContract"].get("@id"), str)
    )
    contract_ref = typed_port.get("cf:valueContract", {})
    assert isinstance(contract_ref, dict) and isinstance(contract_ref.get("@id"), str)
    contract_node = by_id.get(contract_ref["@id"])
    assert isinstance(contract_node, dict)
    shape_kind = contract_node.get("cf:shapeKind")
    assert isinstance(shape_kind, dict)
    assert shape_kind.get("@id") in {
        "cf:shape_single",
        "cf:shape_vector",
        "cf:shape_matrix",
        "cf:shape_table",
    }
