"""CP-9 Phase 3 — EvalResult dataclass.

Validates score range guards, judge_id non-empty, type guards, and the
to_dict / from_dict round-trip used by PanelOfJudges scorecard emission.
"""

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 EvalResult  # noqa: E402


def test_eval_result_construction_happy_path() -> None:
    r = EvalResult(
        score=0.5,
        reasoning="ok",
        judge_id="j1",
        model_used="gemini-3.1-pro-preview",
    )
    assert r.score == 0.5
    assert r.reasoning == "ok"
    assert r.judge_id == "j1"
    assert r.model_used == "gemini-3.1-pro-preview"
    assert r.cost_usd == 0.0
    assert r.metadata == {}


def test_eval_result_construction_with_full_fields() -> None:
    r = EvalResult(
        score=0.8,
        reasoning="strong composition",
        judge_id="j_full",
        model_used="gemini-3.1-pro-preview",
        cost_usd=0.015,
        metadata={"request_id": "abc", "warnings": ["score_clipped"]},
    )
    assert r.cost_usd == 0.015
    assert r.metadata["request_id"] == "abc"


def test_eval_result_score_below_zero_raises() -> None:
    with pytest.raises(ValueError, match=r"\[0\.0, 1\.0\]"):
        EvalResult(score=-0.1, reasoning="x", judge_id="j", model_used="m")


def test_eval_result_score_above_one_raises() -> None:
    with pytest.raises(ValueError, match=r"\[0\.0, 1\.0\]"):
        EvalResult(score=1.5, reasoning="x", judge_id="j", model_used="m")


def test_eval_result_score_non_numeric_raises_TypeError() -> None:
    with pytest.raises(TypeError, match="must be a number"):
        EvalResult(score="0.5", reasoning="x", judge_id="j", model_used="m")  # type: ignore[arg-type]


def test_eval_result_score_bool_raises_TypeError() -> None:
    # bool is a subclass of int — explicitly reject so a buggy judge that
    # returned True/False doesn't sneak through as score=1.0/0.0.
    with pytest.raises(TypeError, match="must be a number"):
        EvalResult(score=True, reasoning="x", judge_id="j", model_used="m")  # type: ignore[arg-type]


def test_eval_result_judge_id_empty_raises() -> None:
    with pytest.raises(ValueError, match="non-empty string"):
        EvalResult(score=0.5, reasoning="x", judge_id="", model_used="m")


def test_eval_result_metadata_default_dict_is_unique_per_instance() -> None:
    a = EvalResult(score=0.1, reasoning="x", judge_id="a", model_used="m")
    b = EvalResult(score=0.2, reasoning="x", judge_id="b", model_used="m")
    a.metadata["k"] = "v"
    assert "k" not in b.metadata


def test_eval_result_to_dict_round_trip() -> None:
    original = EvalResult(
        score=0.42,
        reasoning="meh",
        judge_id="j_rt",
        model_used="gemini-3.1-pro-preview",
        cost_usd=0.003,
        metadata={"warnings": []},
    )
    d = original.to_dict()
    restored = EvalResult.from_dict(d)
    assert restored == original


def test_eval_result_from_dict_legacy_no_cost_no_metadata() -> None:
    """Forward-compat: a scorecard from a legacy emitter that omitted
    cost_usd / metadata still round-trips."""
    restored = EvalResult.from_dict({
        "score": 0.5,
        "reasoning": "x",
        "judge_id": "j",
        "model_used": "m",
    })
    assert restored.cost_usd == 0.0
    assert restored.metadata == {}


def test_eval_result_score_boundary_zero_and_one() -> None:
    EvalResult(score=0.0, reasoning="x", judge_id="j", model_used="m")
    EvalResult(score=1.0, reasoning="x", judge_id="j", model_used="m")
