"""Ontology-focused SHACL and JSON-LD quads validation tests."""

from __future__ import annotations

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

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

from cf_ontology import OntologyManager, ingest_jsonld_files
from cf_ontology.datatypes_mapping import (
    normalize_parameter_type,
    normalize_port_literal_datatype,
    supported_port_literal_datatypes,
)
from cf_ontology.quad_store import QuadStore
from cf_ontology.semantics_paths import resolve_semantics_db_path, resolve_semantics_dir


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


def _resolve_steps_jsonld(module_name: str, repo_fallback: Path) -> Path:
    """
    Resolve a package steps.jsonld path dynamically from installed resources.
    Falls back to the repository path for local-development test runs.
    """
    try:
        candidate = resources.files(module_name) / "steps.jsonld"
        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 _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 JSON-LD @context entries to absolute filesystem paths.
    """
    payload = deepcopy(document)

    def _map_context_value(value):
        if isinstance(value, str):
            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_pipeline_example_jsonld_conforms_shacl():
    """
    Validate pipeline example JSON-LD documents with SHACL.
    This validates semantics only; it does not execute pipelines.
    """
    om = OntologyManager()
    examples_dir = (
        _repo_root() / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine" / "examples"
    )
    files = sorted(examples_dir.glob("*.jsonld"))
    assert files, "No pipeline examples found to validate."

    for path in files:
        doc = json.loads(path.read_text(encoding="utf-8"))
        doc = _resolve_relative_context_paths(doc, path.parent)
        result = om.validate_pipeline_jsonld(doc)
        assert bool(result.get("conforms", False)) is True, (
            f"{path.name} failed SHACL validation:\n{result.get('report_text', '')}"
        )


def test_step_jsonld_ingest_export_roundtrip_is_valid_jsonld():
    """
    Ingest step JSON-LD into quads, export it from DuckDB, and parse exported
    JSON-LD with RDFLib to verify syntactic/semantic validity.
    """
    basic_signal_steps = _resolve_steps_jsonld(
        module_name="cf_basic_signal",
        repo_fallback=(
            _repo_root()
            / "sandcastle"
            / "cf_basic_steps"
            / "cf_basic_signal"
            / "src"
            / "cf_basic_signal"
            / "steps.jsonld"
        ),
    )
    basic_io_steps = _resolve_steps_jsonld(
        module_name="cf_basic_io",
        repo_fallback=(
            _repo_root()
            / "sandcastle"
            / "cf_basic_steps"
            / "cf_basic_io"
            / "src"
            / "cf_basic_io"
            / "steps.jsonld"
        ),
    )
    ingest_jsonld_files([basic_signal_steps], graph_type="step", package="cf.basic.signal")
    ingest_jsonld_files([basic_io_steps], graph_type="step", package="cf.basic.io")

    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_jsonld(flatten=False)

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

    # Ensure step DTO extraction can read ingested data from quads.
    om = OntologyManager(load_resources=False)
    steps = om.get_processing_steps_info_quads_bulk()
    assert len(steps) > 0

    # Deep-check rich DTO fields from AverageStep.
    average = _find_step_by_suffix(steps, "AverageStep")
    assert average.get("rdfs:label") == "Average (Mean + Standard Error)"
    assert average.get("cf:description") == (
        "Computes mean and standard error over a windowed DataCube."
    )
    assert average.get("cf:stepCategory") == "cf:analytical"
    assert average.get("cf:belongsToStepPackage") in {"cfbs:BasicSignalPackage", "cf.basic.signal"}

    avg_inputs = average.get("cf:hasInput")
    assert isinstance(avg_inputs, list)
    assert len(avg_inputs) == 1
    avg_input_values = _find_named(avg_inputs, "cf:portName", "values")
    assert avg_input_values.get("cf:portType") == "adf:DataCube"
    assert avg_input_values.get("cf:description") == "Windowed values to aggregate."
    assert avg_input_values.get("cf:portKey") == "cf:values"

    avg_outputs = average.get("cf:hasOutput")
    assert isinstance(avg_outputs, list)
    mean_out = _find_named(avg_outputs, "cf:portName", "mean")
    assert mean_out.get("cf:portType") == "xsd:double"
    assert mean_out.get("cf:description") == "Arithmetic mean of the input window."
    se_out = _find_named(avg_outputs, "cf:portName", "standardError")
    assert se_out.get("cf:portType") == "xsd:double"
    assert se_out.get("cf:description") == "Standard error of the mean for the input window."

    avg_params = average.get("cf:hasParameter")
    assert isinstance(avg_params, list)
    avg_method = _find_named(avg_params, "cf:parameterName", "averageMethod")
    assert avg_method.get("cf:parameterType") == "cf:string"
    assert avg_method.get("cf:description") == "Averaging method applied to the window."
    assert avg_method.get("cf:defaultValue") == "arithmetic"
    assert set(avg_method.get("cf:allowedValue") or []) == {"arithmetic", "geometric", "harmonic"}
    assert avg_method.get("cf:isRequired") is False

    # Check required/min/max/allowed/default semantics from a parameter-rich step.
    fifo = _find_step_by_suffix(steps, "FifoWindowBufferStep")
    fifo_params = fifo.get("cf:hasParameter")
    assert isinstance(fifo_params, list)
    emit_mode = _find_named(fifo_params, "cf:parameterName", "emitMode")
    assert emit_mode.get("cf:defaultValue") == "all"
    assert set(emit_mode.get("cf:allowedValue") or []) == {"all", "first", "latest"}
    assert emit_mode.get("cf:isRequired") is False
    window_size = _find_named(fifo_params, "cf:parameterName", "windowSize")
    assert window_size.get("cf:isRequired") is True
    assert window_size.get("cf:defaultValue") == 1
    assert window_size.get("cf:minValue") == 1
    assert window_size.get("cf:maxValue") == 1000000

    # Check JSON default value survives ingest/export and returns parsed object.
    opcua_reader = _find_step_by_suffix(steps, "OpcuaReaderStep")
    opcua_params = opcua_reader.get("cf:hasParameter")
    assert isinstance(opcua_params, list)
    nodes_param = _find_named(opcua_params, "cf:parameterName", "nodes")
    assert nodes_param.get("cf:parameterType") == "cf:json"
    assert isinstance(nodes_param.get("cf:defaultValue"), dict)
    assert nodes_param["cf:defaultValue"].get("pH") == "ns=2;s=VirtualPhMeter01.pH"


def test_pipeline_run_requires_session_link_in_shacl() -> None:
    shapes_path = (
        _repo_root()
        / "sandcastle"
        / "cf_ontology"
        / "src"
        / "cf_ontology"
        / "ontology"
        / "shapes"
        / "pipeline.shapes.jsonld"
    )
    shapes_graph = Graph().parse(str(shapes_path), format="json-ld")

    missing_session_doc = {
        "@context": {
            "cf": "https://cogniflow.org/ns#",
            "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="json-ld")
    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": "https://cogniflow.org/ns#",
            "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="json-ld")
    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_registry_backed_datatype_normalization_handles_aliases() -> None:
    assert normalize_parameter_type("cf:number") == "cf:double"
    assert normalize_parameter_type("https://cogniflow.org/ns#float") == (
        "https://cogniflow.org/ns#double"
    )
    assert normalize_parameter_type("xsd:boolean") == "cf:boolean"
    assert normalize_port_literal_datatype("cf:number") == "xsd:double"
    assert normalize_port_literal_datatype("xsd:bolean") == "xsd:boolean"
    assert normalize_port_literal_datatype("http://allotrope.org/ontologies/datacube#DataCube") == (
        "http://allotrope.org/datamodel#DataCube"
    )


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.jsonld"
    )
    shapes_graph = Graph().parse(str(shapes_path), format="json-ld")

    context = {
        "adf": "http://allotrope.org/datamodel#",
        "cf": "https://cogniflow.org/ns#",
        "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:portType": {"@id": datatype},
                }
            ],
        }
        valid_graph = Graph().parse(data=json.dumps(valid_doc), format="json-ld")
        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", "http://allotrope.org/ontologies/datacube#DataCube"):
        invalid_doc = {
            "@context": context,
            "@graph": [
                {
                    "@id": "ex:invalid_port",
                    "@type": "cf:Port",
                    "cf:portType": {"@id": datatype},
                }
            ],
        }
        invalid_graph = Graph().parse(data=json.dumps(invalid_doc), format="json-ld")
        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."
