"""CP-9 Phase 4 — EvalAudioRunner unit tests.

Mirror of test_eval_image_runner.py for the audio modality. Mocks the
registered EvalNode so no live API calls fire.
"""

import sys
import pathlib

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()

import pytest  # noqa: E402

from recoil.pipeline.core.eval import (  # noqa: E402
    EvalContext,
    EvalResult,
    register_eval_node,
)
from recoil.pipeline.core.registry import (  # noqa: E402
    MODALITY_EVAL_AUDIO_V1,
    ModalityRunner,
    RunResult,
)
from recoil.pipeline.core.runners.eval_audio_runner import EvalAudioRunner  # noqa: E402
from recoil.pipeline.core.runners._shared import _failure_metadata_eval as _failure_metadata  # noqa: E402


class _FakeJudge:
    def __init__(
        self,
        *,
        judge_id: str = "eval_audio_v1",
        score: float = 0.92,
        reasoning: str = "Voice match good.",
        model_used: str = "gemini-3.1-pro-preview",
        cost_usd: float = 0.0017,
        metadata=None,
        raise_exc: BaseException = None,
    ):
        self.judge_id = judge_id
        self.model_used = model_used
        self._score = score
        self._reasoning = reasoning
        self._cost_usd = cost_usd
        self._metadata = metadata or {}
        self._raise = raise_exc
        self.calls: list[EvalContext] = []

    def evaluate(self, context: EvalContext) -> EvalResult:
        self.calls.append(context)
        if self._raise is not None:
            raise self._raise
        return EvalResult(
            score=self._score,
            reasoning=self._reasoning,
            judge_id=self.judge_id,
            model_used=self.model_used,
            cost_usd=self._cost_usd,
            metadata=self._metadata,
        )


def _payload(**overrides):
    base = {
        "shot_id": "EP001_VO01",
        "artifact_path": "/tmp/fake_audio.mp3",
        "rubric": "Score this audio's intelligibility 0-1.",
    }
    base.update(overrides)
    return base


def test_run_modality_attribute():
    assert EvalAudioRunner.modality == MODALITY_EVAL_AUDIO_V1 == "eval_audio_v1"


def test_run_zero_arg_construction():
    r = EvalAudioRunner()
    assert isinstance(r, EvalAudioRunner)
    assert isinstance(r, ModalityRunner)
    assert r.modality == "eval_audio_v1"


def test_run_happy_path():
    judge = _FakeJudge(score=0.92, reasoning="Clear intelligibility.")
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert isinstance(out, RunResult)
    assert out.success is True
    assert out.error is None
    assert out.modality == "eval_audio_v1"
    assert out.metadata["final_state"] == "succeeded"
    assert out.metadata["eval_score"] == 0.92
    assert out.metadata["eval_reasoning"] == "Clear intelligibility."
    assert out.metadata["judge_id"] == "eval_audio_v1"
    assert out.metadata["model_used"] == "gemini-3.1-pro-preview"


def test_run_missing_shot_id_returns_failure_RunResult():
    out = EvalAudioRunner().run(_payload() | {"shot_id": None})
    assert out.success is False
    assert "shot_id" in out.error
    assert out.metadata == _failure_metadata()
    assert out.id.startswith("unknown_eval_audio_v1_")


def test_run_missing_artifact_path_returns_failure():
    p = _payload()
    p.pop("artifact_path")
    out = EvalAudioRunner().run(p)
    assert out.success is False
    assert "artifact_path" in out.error


def test_run_missing_rubric_returns_failure():
    p = _payload()
    p.pop("rubric")
    out = EvalAudioRunner().run(p)
    assert out.success is False
    assert "rubric" in out.error


def test_run_unknown_judge_id_returns_failure_with_KeyError_class():
    out = EvalAudioRunner().run(_payload() | {"judge_id": "nope"})
    assert out.success is False
    assert "nope" in out.error
    assert out.metadata["error_class"] == "KeyError"


def test_run_default_judge_id_used_when_omitted():
    judge = _FakeJudge(judge_id="eval_audio_v1")
    register_eval_node("eval_audio_v1", judge)
    p = _payload()
    p.pop("judge_id", None)
    out = EvalAudioRunner().run(p)
    assert out.success is True
    assert judge.calls[0].judge_id == "eval_audio_v1"


def test_run_custom_judge_id_used():
    custom = _FakeJudge(judge_id="custom_audio_judge", score=0.27)
    register_eval_node("custom_audio_judge", custom)
    out = EvalAudioRunner().run(_payload() | {"judge_id": "custom_audio_judge"})
    assert out.success is True
    assert out.metadata["eval_score"] == 0.27


def test_run_judge_raises_provider_error_returns_failure():
    from recoil.execution.providers.gemini_vision import EvalAuthError

    judge = _FakeJudge(raise_exc=EvalAuthError("missing key"))
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.success is False
    assert out.metadata["error_class"] == "EvalAuthError"


def test_run_judge_raises_unknown_exception_returns_failure():
    judge = _FakeJudge(raise_exc=KeyError("inner_dict_key_missing"))
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.success is False
    assert out.metadata["error_class"] == "KeyError"


def test_run_payload_artifact_path_string_coerced_to_Path():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    EvalAudioRunner().run(_payload() | {"artifact_path": "/tmp/a.mp3"})
    assert isinstance(judge.calls[0].target_artifact_path, pathlib.Path)


def test_run_payload_ref_paths_coerced_to_Paths():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    EvalAudioRunner().run(_payload() | {"ref_paths": ["/r1.wav", "/r2.mp3"]})
    refs = judge.calls[0].metadata.get("ref_paths")
    assert refs is not None
    assert all(isinstance(p, pathlib.Path) for p in refs)
    assert [str(p) for p in refs] == ["/r1.wav", "/r2.mp3"]


def test_run_metadata_contains_eval_score_and_reasoning():
    judge = _FakeJudge(score=0.5, reasoning="Average.")
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.metadata["eval_score"] == 0.5
    assert out.metadata["eval_reasoning"] == "Average."


def test_run_metadata_contains_judge_metadata_dict():
    judge = _FakeJudge(metadata={"warnings": []})
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.metadata["judge_metadata"] == {"warnings": []}


def test_run_id_format():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.id.startswith("EP001_VO01_eval_audio_v1_")
    suffix = out.id.rsplit("_", 1)[-1]
    assert suffix.isdigit()
    assert int(suffix) > 10**15


def test_run_id_unique_across_calls_audit_12c_R1():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    runner = EvalAudioRunner()
    a = runner.run(_payload())
    b = runner.run(_payload())
    assert a.id != b.id


def test_run_output_path_is_None():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.output_path is None


def test_run_output_url_is_None():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.output_url is None


def test_run_eval_cost_usd_propagated_from_judge():
    judge = _FakeJudge(cost_usd=0.0001)
    register_eval_node("eval_audio_v1", judge)
    out = EvalAudioRunner().run(_payload())
    assert out.metadata["eval_cost_usd"] == pytest.approx(0.0001)


def test_run_failure_metadata_shape_six_keys():
    fm = _failure_metadata()
    assert set(fm.keys()) == {
        "final_state",
        "eval_score",
        "eval_reasoning",
        "judge_id",
        "model_used",
        "eval_cost_usd",
    }


def test_run_extra_passes_transport_when_provided():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    sentinel = object()
    EvalAudioRunner().run(_payload() | {"_transport": sentinel})
    assert judge.calls[0].metadata.get("_transport") is sentinel


def test_run_extra_omits_transport_when_not_provided():
    judge = _FakeJudge()
    register_eval_node("eval_audio_v1", judge)
    EvalAudioRunner().run(_payload())
    assert "_transport" not in judge.calls[0].metadata
