from __future__ import annotations

from copy import deepcopy

import pytest

from cf_ontology.namespaces import CF_BASE
from cf_ontology.pipeline_state_runtime import _apply_invocation_overrides


class _FakeOntologyManager:
    def __init__(self, *, conforms: bool = True, report_text: str = "") -> None:
        self._conforms = conforms
        self._report_text = report_text
        self.last_doc = None

    def validate_pipeline_document(self, doc):  # noqa: ANN001
        self.last_doc = doc
        return {
            "conforms": self._conforms,
            "report_text": self._report_text,
        }


def _pipeline_doc() -> dict:
    return {
        "@context": {
            "cf": CF_BASE,
            "ex": "http://example.org/",
            "schema": "https://schema.org/",
        },
        "@graph": [
            {
                "@id": "ex:pipeline",
                "@type": "cf:ProcessingPipeline",
                "cf:hasPipelineNode": {"@id": "ex:n1"},
            },
            {
                "@id": "ex:n1",
                "@type": "cf:PipelineNode",
                "cf:nodeDefinition": {"@id": "ex:InputStep"},
                "cf:hasParameterBinding": {"@id": "ex:n1_endpoint_binding"},
            },
            {
                "@id": "ex:InputStep",
                "@type": "cf:ProcessingStep",
                "cf:stepCategory": {"@id": "cf:input"},
            },
            {
                "@id": "ex:n1_endpoint_binding",
                "@type": "cf:ParameterBinding",
                "cf:bindsParameter": {"@id": "ex:InputStep_param_endpoint"},
                "cf:value": "opc.tcp://127.0.0.1:4840/VirtualPhServer",
            },
        ],
    }


def _invocation_doc(*, key: str = "endpoint", value: str = "opc.tcp://127.0.0.1:4841/VirtualPhServer") -> dict:
    return {
        "@context": {
            "cf": CF_BASE,
            "ex": "http://example.org/",
            "schema": "https://schema.org/",
        },
        "@graph": [
            {
                "@id": "ex:invocation",
                "@type": "cf:PipelineInvocation",
                "cf:invokesPipeline": {"@id": "ex:pipeline"},
                "cf:overridesEntryPoint": {"@id": "ex:override_n1"},
            },
            {
                "@id": "ex:override_n1",
                "@type": "cf:EntryPointOverride",
                "cf:targetsNode": {"@id": "ex:n1"},
                "cf:overrideSettings": {"@id": "ex:override_settings_n1"},
            },
            {
                "@id": "ex:override_settings_n1",
                "@type": "schema:StructuredValue",
                "schema:additionalProperty": {"@id": "ex:override_settings_n1_property"},
            },
            {
                "@id": "ex:override_settings_n1_property",
                "@type": "schema:PropertyValue",
                "schema:propertyID": key,
                "schema:value": value,
            },
        ],
    }


def _binding_value(pipeline_doc: dict) -> str:
    for node in pipeline_doc.get("@graph", []):
        if node.get("@id") == "ex:n1_endpoint_binding":
            return str(node.get("cf:value"))
    raise AssertionError("ex:n1_endpoint_binding missing")


def test_apply_invocation_overrides_updates_input_binding() -> None:
    pipeline_doc = _pipeline_doc()
    pipeline_before = deepcopy(pipeline_doc)
    invocation_doc = _invocation_doc()
    fake_om = _FakeOntologyManager(conforms=True)

    patched = _apply_invocation_overrides(fake_om, pipeline_doc, invocation_doc)

    assert _binding_value(pipeline_before) == "opc.tcp://127.0.0.1:4840/VirtualPhServer"
    assert _binding_value(patched) == "opc.tcp://127.0.0.1:4841/VirtualPhServer"
    assert _binding_value(pipeline_doc) == "opc.tcp://127.0.0.1:4840/VirtualPhServer"
    assert fake_om.last_doc is not None


def test_apply_invocation_overrides_rejects_unknown_parameter_key() -> None:
    pipeline_doc = _pipeline_doc()
    invocation_doc = _invocation_doc(key="unknown_setting")
    fake_om = _FakeOntologyManager(conforms=True)

    with pytest.raises(ValueError, match="does not match any parameter binding"):
        _apply_invocation_overrides(fake_om, pipeline_doc, invocation_doc)


def test_apply_invocation_overrides_requires_conforming_invocation() -> None:
    pipeline_doc = _pipeline_doc()
    invocation_doc = _invocation_doc()
    fake_om = _FakeOntologyManager(conforms=False, report_text="validation failed")

    with pytest.raises(ValueError, match="Invocation SHACL validation failed"):
        _apply_invocation_overrides(fake_om, pipeline_doc, invocation_doc)
