"""CP-9 Phase 3 — EvalRegistry.

Validates register_eval_node / get_eval_node / list_eval_nodes /
is_eval_registered + the EvalRegistry class facade. Mirrors the CP-4
modality registry test patterns (test_registry.py).
"""

import sys
import pathlib

import pytest

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent.parent.parent))
from recoil.core.paths import ensure_pipeline_importable  # noqa: E402

ensure_pipeline_importable()

from recoil.pipeline.core.eval import (  # noqa: E402
    EvalContext,
    EvalResult,
    EvalRegistry,
    register_eval_node,
    get_eval_node,
    list_eval_nodes,
    is_eval_registered,
    _reset_eval_registry_for_tests,
)


class _StubJudge:
    def __init__(self, judge_id: str, model_used: str = "stub-model") -> None:
        self.judge_id = judge_id
        self.model_used = model_used

    def evaluate(self, context: EvalContext) -> EvalResult:
        return EvalResult(
            score=0.5, reasoning="stub", judge_id=self.judge_id,
            model_used=self.model_used,
        )


def test_register_eval_node_happy_path() -> None:
    j = _StubJudge("j1")
    register_eval_node("j1", j)
    assert get_eval_node("j1") is j


def test_register_eval_node_idempotent_same_instance() -> None:
    j = _StubJudge("j1")
    register_eval_node("j1", j)
    register_eval_node("j1", j)  # idempotent — no error
    assert get_eval_node("j1") is j


def test_register_eval_node_collision_different_instance_raises() -> None:
    a = _StubJudge("j1")
    b = _StubJudge("j1")
    register_eval_node("j1", a)
    with pytest.raises(ValueError, match="already registered"):
        register_eval_node("j1", b)


def test_register_eval_node_force_override() -> None:
    a = _StubJudge("j1")
    b = _StubJudge("j1")
    register_eval_node("j1", a)
    register_eval_node("j1", b, force=True)
    assert get_eval_node("j1") is b


def test_register_eval_node_invalid_node_raises_TypeError() -> None:
    class Empty:
        pass
    with pytest.raises(TypeError, match="EvalNode protocol"):
        register_eval_node("bad", Empty())  # type: ignore[arg-type]


def test_register_eval_node_empty_id_raises_ValueError() -> None:
    with pytest.raises(ValueError, match="non-empty"):
        register_eval_node("", _StubJudge("x"))


def test_get_eval_node_unregistered_raises_KeyError() -> None:
    with pytest.raises(KeyError, match="no EvalNode registered"):
        get_eval_node("nonexistent")


def test_list_eval_nodes_sorted() -> None:
    register_eval_node("zeta", _StubJudge("zeta"))
    register_eval_node("alpha", _StubJudge("alpha"))
    register_eval_node("mike", _StubJudge("mike"))
    assert list_eval_nodes() == ["alpha", "mike", "zeta"]


def test_list_eval_nodes_empty_when_none_registered() -> None:
    assert list_eval_nodes() == []


def test_is_eval_registered_bool_no_exception() -> None:
    assert is_eval_registered("nope") is False
    register_eval_node("yep", _StubJudge("yep"))
    assert is_eval_registered("yep") is True


def test_reset_eval_registry_for_tests() -> None:
    register_eval_node("a", _StubJudge("a"))
    register_eval_node("b", _StubJudge("b"))
    assert len(list_eval_nodes()) == 2
    _reset_eval_registry_for_tests()
    assert list_eval_nodes() == []


def test_eval_registry_class_facade_register_get_list_is_registered() -> None:
    j = _StubJudge("facade_j")
    EvalRegistry.register("facade_j", j)
    assert EvalRegistry.get("facade_j") is j
    assert EvalRegistry.list() == ["facade_j"]
    assert EvalRegistry.is_registered("facade_j") is True
    assert EvalRegistry.is_registered("missing") is False


def test_eval_registry_class_facade_shares_state_with_functions() -> None:
    """The class is a thin facade — both views see the same dict."""
    j = _StubJudge("shared_j")
    register_eval_node("shared_j", j)
    assert EvalRegistry.get("shared_j") is j
    assert EvalRegistry.list() == ["shared_j"]


def test_eval_registry_distinct_from_modality_registry() -> None:
    """Audit § 12 LOCKED — eval registry must NOT collide with the CP-4
    modality registry's namespace. Same id can exist in both without
    error or shared state."""
    from recoil.pipeline.core.registry import (
        register_runner, list_modalities, _reset_for_tests,
    )

    class _StubRunner:
        modality = "shared_id"

        def run(self, payload):  # type: ignore[no-untyped-def]
            from recoil.pipeline.core.registry import RunResult
            return RunResult(id="x", modality="shared_id", success=True)

    _reset_for_tests()
    register_runner("shared_id", _StubRunner())
    register_eval_node("shared_id", _StubJudge("shared_id"))

    assert "shared_id" in list_modalities()
    assert "shared_id" in list_eval_nodes()
    # Different storage — clearing one doesn't touch the other.
    _reset_for_tests()
    assert is_eval_registered("shared_id") is True
    assert "shared_id" not in list_modalities()
