"""
Tests for handle_instance_validation() using anonymized SHACL rules.

NeatSession.with_shacl is replaced by a thin wrapper that runs pyshacl directly
against an rdflib.Graph fixture, so the real SHACL shapes are exercised without
needing a live CDF connection.

Namespaces used in test data mirror those in the SHACL files:
  pump:  http://purl.org/cognite/sp_rmdm_dm/Pump/
  tag:   http://purl.org/cognite/sp_rmdm_dm/Tag/
  comp:  http://purl.org/cognite/sp_rmdm_dm/Compressor/
  hx:    http://purl.org/cognite/sp_rmdm_dm/HeatExchanger/
  ec:    http://purl.org/cognite/sp_rmdm_dm/EquipmentClass/
  et:    http://purl.org/cognite/sp_rmdm_dm/EquipmentType/
"""

from __future__ import annotations

from pathlib import Path
from typing import Any
from unittest.mock import Mock

import pyshacl
from rdflib import Graph, Literal, Namespace, URIRef
from rdflib.namespace import RDF

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------

SHACL_DIR = Path(__file__).parent / "shacl_rules"

# ---------------------------------------------------------------------------
# Namespace constants (matching what the SHACL files declare)
# ---------------------------------------------------------------------------

PUMP = Namespace("http://purl.org/cognite/sp_rmdm_dm/Pump/")
TAG = Namespace("http://purl.org/cognite/sp_rmdm_dm/Tag/")
COMP = Namespace("http://purl.org/cognite/sp_rmdm_dm/Compressor/")
HX = Namespace("http://purl.org/cognite/sp_rmdm_dm/HeatExchanger/")
EC = Namespace("http://purl.org/cognite/sp_rmdm_dm/EquipmentClass/")
ET = Namespace("http://purl.org/cognite/sp_rmdm_dm/EquipmentType/")
DOC = Namespace("http://purl.org/cognite/sp_rmdm_dm/Document/")
WO = Namespace("http://purl.org/cognite/sp_rmdm_dm/WorkOrder/")
FN = Namespace("http://purl.org/cognite/sp_rmdm_dm/FailureNotification/")
NOTIF = Namespace("http://purl.org/cognite/sp_rmdm_dm/Notification/")

# Shared test support instances (EquipmentClass + EquipmentType with matching back-reference)
_EC_URI = EC["TestClass001"]
_ET_URI = ET["TestType001"]
_ET_WRONG_CLASS = ET["TestType002"]  # references a different class → ISO mismatch
_EC2_URI = EC["TestClass002"]


def _support_triples(g: Graph, *, iso_mismatch: bool = False) -> None:
    """Add EquipmentClass and EquipmentType support nodes to graph *g*."""
    g.add((_EC_URI, RDF.type, EC["EquipmentClass"]))
    g.add((_EC2_URI, RDF.type, EC["EquipmentClass"]))
    g.add((_ET_URI, RDF.type, ET["EquipmentType"]))
    if iso_mismatch:
        # EquipmentType references a DIFFERENT class than what the tag uses
        g.add((_ET_URI, ET["equipmentClass"], _EC2_URI))
    else:
        g.add((_ET_URI, ET["equipmentClass"], _EC_URI))


# ---------------------------------------------------------------------------
# SHACL loading helpers
# ---------------------------------------------------------------------------


def _shacl_text(*filenames: str) -> str:
    """Concatenate SHACL TTL files.

    Each file has its own @prefix declarations so we load them separately into
    an rdflib Graph to avoid @prefix conflicts, then serialise back to turtle.
    """
    g = Graph()
    for fn in filenames:
        g.parse(str(SHACL_DIR / fn), format="turtle")
    return g.serialize(format="turtle")


PUMP_SHACL = _shacl_text("tag_shacl_rules.ttl", "pump_shacl_rules.ttl")
COMP_SHACL = _shacl_text("tag_shacl_rules.ttl", "compressor_shacl_rules.ttl")
HX_SHACL = _shacl_text("tag_shacl_rules.ttl", "heatexchanger_shacl_rules.ttl")
TAG_SHACL = _shacl_text("tag_shacl_rules.ttl")


# ---------------------------------------------------------------------------
# NeatSession mock factory
# ---------------------------------------------------------------------------


def _make_neat_cls(data_graph: Graph):
    """Return a NeatSession replacement that validates *data_graph* via pyshacl."""

    class _FakeValidateInstances:
        def with_shacl(self, *, shacl_rules: str, **_: Any) -> tuple:
            shapes = Graph()
            shapes.parse(data=shacl_rules, format="turtle")
            conforms, report_graph, report_text = pyshacl.validate(
                data_graph,
                shacl_graph=shapes,
                advanced=True,
                abort_on_first=False,
            )
            return (conforms, report_graph, report_text)

    class _FakeNeat:
        def __init__(self, _client: Any, verbose: bool = False) -> None:
            self.validate_instances = _FakeValidateInstances()

    return _FakeNeat


# ---------------------------------------------------------------------------
# Handler runner
# ---------------------------------------------------------------------------


def _run(monkeypatch, data: dict, data_graph: Graph) -> dict:
    """Patch NeatSession + Records API and call handle_instance_validation."""
    # NeatSession is imported lazily inside the handler function, so patch it
    # at the source module rather than on the handler module.
    monkeypatch.setattr(
        "cognite_data_quality._neat.NeatSession",
        _make_neat_cls(data_graph),
    )
    monkeypatch.setattr(
        "handlers.instance_validation.post_validation_results_to_records",
        lambda **_kw: (0, []),
    )
    from handlers.instance_validation import handle_instance_validation

    return handle_instance_validation(data, client=Mock())


def _base_data(shacl_text: str, eq_type: str = "Pump") -> dict:
    return {
        "instances": {"items": {"n": [{"space": "sp_test", "externalId": "dummy"}]}},
        "shacl_rules": shacl_text,
        "datamodel_space": "sp_rmdm_dm",
        "datamodel_external_id": eq_type,
        "datamodel_version": "v8.19",
        "auto_load_depth": 0,
        "verbose": False,
    }


# ===========================================================================
# Pump tests
# ===========================================================================


def _pump_coverage_triples(g: Graph, pump_uri: URIRef) -> None:
    """Add Document/WorkOrder/Notification nodes required by pump Warning shapes."""
    for doc_type in ("XB", "MB", "DS"):
        doc = URIRef(f"urn:test:doc:{doc_type}")
        g.add((doc, RDF.type, DOC["Document"]))
        g.add((doc, DOC["assets"], pump_uri))
        g.add((doc, DOC["documentTypeCode"], Literal(doc_type)))
    for wo_type in ("PM02", "PM03"):
        wo = URIRef(f"urn:test:wo:{wo_type}")
        g.add((wo, RDF.type, WO["WorkOrder"]))
        g.add((wo, WO["assets"], pump_uri))
        g.add((wo, WO["type"], Literal(wo_type)))
    fn_node = URIRef("urn:test:fn:M2")
    g.add((fn_node, RDF.type, FN["FailureNotification"]))
    g.add((fn_node, FN["assets"], pump_uri))
    g.add((fn_node, FN["type"], Literal("M2")))
    notif_node = URIRef("urn:test:notif:M1")
    g.add((notif_node, RDF.type, NOTIF["Notification"]))
    g.add((notif_node, NOTIF["assets"], pump_uri))
    g.add((notif_node, NOTIF["type"], Literal("M1")))


def _valid_pump_graph() -> Graph:
    g = Graph()
    p = URIRef("urn:test:pump:P001")
    g.add((p, RDF.type, PUMP["Pump"]))
    # Tag-namespace properties (checked via sh:node tag:TagShape)
    g.add((p, TAG["name"], Literal("11-PA-0001")))
    g.add((p, TAG["description"], Literal("Utility area oil transfer pump")))
    g.add((p, TAG["system"], Literal("11")))
    g.add((p, TAG["functionCode"], Literal("PA")))
    g.add((p, TAG["functionCodeDesc"], Literal("Centrifugal Pump")))
    g.add((p, TAG["dateInstalled"], Literal("2020-01-01")))
    g.add((p, TAG["tagDiscipline"], Literal("Mechanical")))
    g.add((p, TAG["ISOEquipmentClass"], _EC_URI))
    g.add((p, TAG["ISOEquipmentType"], _ET_URI))
    # Pump-namespace properties (checked by pump-specific shapes)
    g.add((p, PUMP["name"], Literal("11-PA-0001")))
    g.add((p, PUMP["system"], Literal("11")))
    g.add((p, PUMP["functionCode"], Literal("PA")))
    g.add((p, PUMP["tagDiscipline"], Literal("Mechanical")))
    g.add((p, PUMP["ISOEquipmentClass"], _EC_URI))
    g.add((p, PUMP["ISOEquipmentType"], _ET_URI))
    _support_triples(g)
    _pump_coverage_triples(g, p)
    return g


class TestPumpValidation:
    def test_pump_valid_passes(self, monkeypatch) -> None:
        result = _run(monkeypatch, _base_data(PUMP_SHACL), _valid_pump_graph())
        assert result["conforms"] is True
        assert result.get("violation_count", 0) == 0

    def test_pump_wrong_discipline_fails(self, monkeypatch) -> None:
        g = _valid_pump_graph()
        p = URIRef("urn:test:pump:P001")
        g.remove((p, PUMP["tagDiscipline"], Literal("Mechanical")))
        g.add((p, PUMP["tagDiscipline"], Literal("Electrical")))
        result = _run(monkeypatch, _base_data(PUMP_SHACL), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_pump_invalid_function_code_fails(self, monkeypatch) -> None:
        g = _valid_pump_graph()
        p = URIRef("urn:test:pump:P001")
        # "KA" is a Compressor code — invalid for Pump
        g.remove((p, PUMP["functionCode"], Literal("PA")))
        g.add((p, PUMP["functionCode"], Literal("KA")))
        result = _run(monkeypatch, _base_data(PUMP_SHACL), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_pump_system_name_mismatch_fails(self, monkeypatch) -> None:
        g = _valid_pump_graph()
        p = URIRef("urn:test:pump:P001")
        # system "99" does not match the first 2 digits of name "11-PA-0001"
        g.remove((p, PUMP["system"], Literal("11")))
        g.add((p, PUMP["system"], Literal("99")))
        result = _run(monkeypatch, _base_data(PUMP_SHACL), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_pump_missing_description_fails(self, monkeypatch) -> None:
        g = _valid_pump_graph()
        p = URIRef("urn:test:pump:P001")
        g.remove((p, TAG["description"], None))
        result = _run(monkeypatch, _base_data(PUMP_SHACL), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_pump_deleted_instances_filtered(self, monkeypatch) -> None:
        data = _base_data(PUMP_SHACL)
        data["instances"] = {"items": {"n": [{"space": "sp_test", "externalId": "P001", "deletedTime": 1234567890}]}}
        # data_graph is irrelevant because all instances are filtered out
        result = _run(monkeypatch, data, Graph())
        assert result["conforms"] is True
        assert result.get("instance_count", 0) == 0


# ===========================================================================
# Compressor tests
# ===========================================================================


def _valid_compressor_graph() -> Graph:
    g = Graph()
    p = URIRef("urn:test:comp:K001")
    g.add((p, RDF.type, COMP["Compressor"]))
    g.add((p, TAG["name"], Literal("23-KA-0001")))
    g.add((p, TAG["description"], Literal("Gas injection centrifugal compressor")))
    g.add((p, TAG["system"], Literal("23")))
    g.add((p, TAG["functionCode"], Literal("KA")))
    g.add((p, TAG["functionCodeDesc"], Literal("Centrifugal Compressor")))
    g.add((p, TAG["dateInstalled"], Literal("2019-06-15")))
    g.add((p, TAG["tagDiscipline"], Literal("Mechanical")))
    g.add((p, TAG["ISOEquipmentClass"], _EC_URI))
    g.add((p, TAG["ISOEquipmentType"], _ET_URI))
    g.add((p, COMP["name"], Literal("23-KA-0001")))
    g.add((p, COMP["system"], Literal("23")))
    g.add((p, COMP["functionCode"], Literal("KA")))
    g.add((p, COMP["tagDiscipline"], Literal("Mechanical")))
    g.add((p, COMP["ISOEquipmentClass"], _EC_URI))
    g.add((p, COMP["ISOEquipmentType"], _ET_URI))
    _support_triples(g)
    return g


class TestCompressorValidation:
    def test_compressor_valid_passes(self, monkeypatch) -> None:
        result = _run(monkeypatch, _base_data(COMP_SHACL, "Compressor"), _valid_compressor_graph())
        assert result["conforms"] is True
        assert result.get("violation_count", 0) == 0

    def test_compressor_wrong_discipline_fails(self, monkeypatch) -> None:
        g = _valid_compressor_graph()
        p = URIRef("urn:test:comp:K001")
        # "Process" is allowed for pumps/HX but NOT for compressors
        g.remove((p, COMP["tagDiscipline"], Literal("Mechanical")))
        g.add((p, COMP["tagDiscipline"], Literal("Process")))
        result = _run(monkeypatch, _base_data(COMP_SHACL, "Compressor"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_compressor_invalid_function_code_fails(self, monkeypatch) -> None:
        g = _valid_compressor_graph()
        p = URIRef("urn:test:comp:K001")
        # "PA" is a Pump code — invalid for Compressor
        g.remove((p, COMP["functionCode"], Literal("KA")))
        g.add((p, COMP["functionCode"], Literal("PA")))
        result = _run(monkeypatch, _base_data(COMP_SHACL, "Compressor"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_compressor_system_name_mismatch_fails(self, monkeypatch) -> None:
        g = _valid_compressor_graph()
        p = URIRef("urn:test:comp:K001")
        g.remove((p, COMP["system"], Literal("23")))
        g.add((p, COMP["system"], Literal("99")))
        result = _run(monkeypatch, _base_data(COMP_SHACL, "Compressor"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1


# ===========================================================================
# HeatExchanger tests
# ===========================================================================


def _valid_hx_graph() -> Graph:
    g = Graph()
    p = URIRef("urn:test:hx:H001")
    g.add((p, RDF.type, HX["HeatExchanger"]))
    g.add((p, TAG["name"], Literal("20-HA-0001")))
    g.add((p, TAG["description"], Literal("Process gas shell and tube heat exchanger")))
    g.add((p, TAG["system"], Literal("20")))
    g.add((p, TAG["functionCode"], Literal("HA")))
    g.add((p, TAG["functionCodeDesc"], Literal("Shell and Tube Heat Exchanger")))
    g.add((p, TAG["dateInstalled"], Literal("2018-03-22")))
    g.add((p, TAG["tagDiscipline"], Literal("Mechanical")))
    g.add((p, TAG["ISOEquipmentClass"], _EC_URI))
    g.add((p, TAG["ISOEquipmentType"], _ET_URI))
    g.add((p, HX["name"], Literal("20-HA-0001")))
    g.add((p, HX["system"], Literal("20")))
    g.add((p, HX["functionCode"], Literal("HA")))
    g.add((p, HX["tagDiscipline"], Literal("Mechanical")))
    g.add((p, HX["ISOEquipmentClass"], _EC_URI))
    g.add((p, HX["ISOEquipmentType"], _ET_URI))
    _support_triples(g)
    return g


class TestHeatExchangerValidation:
    def test_heatexchanger_valid_passes(self, monkeypatch) -> None:
        result = _run(monkeypatch, _base_data(HX_SHACL, "HeatExchanger"), _valid_hx_graph())
        assert result["conforms"] is True
        assert result.get("violation_count", 0) == 0

    def test_heatexchanger_wrong_discipline_fails(self, monkeypatch) -> None:
        g = _valid_hx_graph()
        p = URIRef("urn:test:hx:H001")
        g.remove((p, HX["tagDiscipline"], Literal("Mechanical")))
        g.add((p, HX["tagDiscipline"], Literal("Electrical")))
        result = _run(monkeypatch, _base_data(HX_SHACL, "HeatExchanger"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_heatexchanger_invalid_function_code_fails(self, monkeypatch) -> None:
        g = _valid_hx_graph()
        p = URIRef("urn:test:hx:H001")
        # "PA" is a Pump code — invalid for HeatExchanger
        g.remove((p, HX["functionCode"], Literal("HA")))
        g.add((p, HX["functionCode"], Literal("PA")))
        result = _run(monkeypatch, _base_data(HX_SHACL, "HeatExchanger"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_heatexchanger_system_name_mismatch_fails(self, monkeypatch) -> None:
        g = _valid_hx_graph()
        p = URIRef("urn:test:hx:H001")
        g.remove((p, HX["system"], Literal("20")))
        g.add((p, HX["system"], Literal("99")))
        result = _run(monkeypatch, _base_data(HX_SHACL, "HeatExchanger"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1


# ===========================================================================
# Tag tests
# ===========================================================================


def _valid_tag_graph() -> Graph:
    g = Graph()
    t = URIRef("urn:test:tag:T001")
    g.add((t, RDF.type, TAG["Tag"]))
    g.add((t, TAG["name"], Literal("30-PA-0001")))
    g.add((t, TAG["description"], Literal("Utility area process pump")))
    g.add((t, TAG["system"], Literal("30")))
    g.add((t, TAG["functionCode"], Literal("PA")))
    g.add((t, TAG["functionCodeDesc"], Literal("Centrifugal Pump")))
    g.add((t, TAG["dateInstalled"], Literal("2021-07-01")))
    g.add((t, TAG["tagDiscipline"], Literal("Mechanical")))
    g.add((t, TAG["ISOEquipmentClass"], _EC_URI))
    g.add((t, TAG["ISOEquipmentType"], _ET_URI))
    _support_triples(g)
    return g


class TestTagValidation:
    def test_tag_valid_passes(self, monkeypatch) -> None:
        result = _run(monkeypatch, _base_data(TAG_SHACL, "Tag"), _valid_tag_graph())
        assert result["conforms"] is True
        assert result.get("violation_count", 0) == 0

    def test_tag_missing_name_fails(self, monkeypatch) -> None:
        g = _valid_tag_graph()
        t = URIRef("urn:test:tag:T001")
        g.remove((t, TAG["name"], None))
        result = _run(monkeypatch, _base_data(TAG_SHACL, "Tag"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_tag_invalid_function_code_fails(self, monkeypatch) -> None:
        g = _valid_tag_graph()
        t = URIRef("urn:test:tag:T001")
        g.remove((t, TAG["functionCode"], Literal("PA")))
        g.add((t, TAG["functionCode"], Literal("ZZ")))
        result = _run(monkeypatch, _base_data(TAG_SHACL, "Tag"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1

    def test_tag_iso_class_type_mismatch_fails(self, monkeypatch) -> None:
        g = _valid_tag_graph()
        # EquipmentType references EC2 but tag references EC1 → ISO mismatch
        _support_triples(g, iso_mismatch=True)
        result = _run(monkeypatch, _base_data(TAG_SHACL, "Tag"), g)
        assert result["conforms"] is False
        assert result["violation_count"] >= 1


# ===========================================================================
# Handler edge-case tests (no SHACL evaluation needed)
# ===========================================================================


class TestHandlerEdgeCases:
    def test_missing_shacl_rules_returns_error(self, monkeypatch) -> None:
        monkeypatch.setattr(
            "handlers.instance_validation.post_validation_results_to_records",
            lambda **_kw: (0, []),
        )
        from handlers.instance_validation import handle_instance_validation

        data = {
            "instances": {"items": {"n": []}},
            "datamodel_space": "sp_rmdm_dm",
            "datamodel_external_id": "Pump",
            "datamodel_version": "v8.19",
        }
        result = handle_instance_validation(data, client=Mock())
        assert result["conforms"] is False
        assert "error" in result

    def test_missing_datamodel_params_returns_error(self, monkeypatch) -> None:
        monkeypatch.setattr(
            "handlers.instance_validation.post_validation_results_to_records",
            lambda **_kw: (0, []),
        )
        from handlers.instance_validation import handle_instance_validation

        data = {
            "instances": {"items": {"n": []}},
            "shacl_rules": "@prefix sh: <http://www.w3.org/ns/shacl#> .",
            # missing datamodel_space, datamodel_external_id, datamodel_version
        }
        result = handle_instance_validation(data, client=Mock())
        assert result["conforms"] is False
        assert "error" in result

    def test_empty_instances_returns_conforms_true(self, monkeypatch) -> None:
        monkeypatch.setattr(
            "handlers.instance_validation.post_validation_results_to_records",
            lambda **_kw: (0, []),
        )
        from handlers.instance_validation import handle_instance_validation

        data = {
            "instances": {"items": {"n": []}},
            "shacl_rules": "@prefix sh: <http://www.w3.org/ns/shacl#> .",
            "datamodel_space": "sp_rmdm_dm",
            "datamodel_external_id": "Pump",
            "datamodel_version": "v8.19",
            "verbose": False,
        }
        result = handle_instance_validation(data, client=Mock())
        assert result["conforms"] is True
        assert result.get("instance_count", 0) == 0
