"""CP-4 Phase 2 — registry unit tests."""

import sys
import pathlib

# Bootstrap (in case test runs outside the installed venv)
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.registry import (  # noqa: E402
    MODALITY_IMAGE_T2I,
    MODALITY_VIDEO_I2V,
    MODALITY_AUDIO_T2A,
    MODALITY_LIPSYNC_POST,
    ModalityRunner,
    RunResult,
    register_runner,
    register_runner_factory,
    get_runner,
    list_modalities,
    is_registered,
)


class _DummyRunner:
    """Minimal ModalityRunner satisfier for tests."""
    modality = "test_dummy"
    def run(self, payload):
        return RunResult(id="test", modality=self.modality, success=True)


def test_canonical_modality_constants():
    assert MODALITY_IMAGE_T2I    == "image_t2i"
    assert MODALITY_VIDEO_I2V    == "video_i2v"
    assert MODALITY_AUDIO_T2A    == "audio_t2a"
    assert MODALITY_LIPSYNC_POST == "lipsync_post"


def test_runresult_minimal_construction():
    r = RunResult(id="x", modality="image_t2i")
    assert r.id == "x"
    assert r.modality == "image_t2i"
    assert r.output_path is None
    assert r.output_url is None
    assert r.metadata == {}
    assert r.success is False
    assert r.error is None


def test_runresult_full_construction():
    r = RunResult(
        id="shot_001",
        modality="video_i2v",
        output_path="/tmp/foo.mp4",
        output_url="https://fal.cdn/foo.mp4",
        metadata={"cost": 0.13, "duration": 5},
        success=True,
        error=None,
    )
    assert r.success is True
    assert r.metadata["cost"] == 0.13


def test_register_and_get():
    runner = _DummyRunner()
    register_runner("test_mod", runner)
    assert get_runner("test_mod") is runner


def test_register_runtime_checkable_protocol():
    runner = _DummyRunner()
    assert isinstance(runner, ModalityRunner)


def test_register_rejects_empty_modality_id():
    with pytest.raises(ValueError):
        register_runner("", _DummyRunner())


def test_register_rejects_object_without_run():
    class NoRun:
        pass
    with pytest.raises(TypeError):
        register_runner("bad", NoRun())


def test_duplicate_register_same_instance_idempotent():
    runner = _DummyRunner()
    register_runner("test_mod", runner)
    register_runner("test_mod", runner)  # should not raise
    assert get_runner("test_mod") is runner


def test_duplicate_register_different_instance_raises():
    register_runner("test_mod", _DummyRunner())
    with pytest.raises(KeyError):
        register_runner("test_mod", _DummyRunner())


def test_get_runner_missing_raises_keyerror_with_helpful_message():
    register_runner("alpha", _DummyRunner())
    with pytest.raises(KeyError) as exc:
        get_runner("nonexistent")
    msg = str(exc.value)
    assert "nonexistent" in msg
    assert "alpha" in msg  # should list registered modalities


def test_list_modalities_sorted_and_complete():
    register_runner("zeta", _DummyRunner())
    register_runner("alpha", _DummyRunner())
    register_runner("mu", _DummyRunner())
    assert list_modalities() == ["alpha", "mu", "zeta"]


def test_is_registered():
    assert not is_registered("not_yet")
    register_runner("not_yet", _DummyRunner())
    assert is_registered("not_yet")


def test_factory_registration_lazy():
    constructed = []
    def factory():
        constructed.append(1)
        return _DummyRunner()
    register_runner_factory("lazy_mod", factory)
    assert constructed == []
    assert is_registered("lazy_mod")
    runner = get_runner("lazy_mod")
    assert constructed == [1]
    # second get_runner does not re-invoke factory
    runner2 = get_runner("lazy_mod")
    assert constructed == [1]
    assert runner is runner2


def test_factory_rejected_on_existing_modality():
    register_runner("taken", _DummyRunner())
    with pytest.raises(KeyError):
        register_runner_factory("taken", lambda: _DummyRunner())


def test_factory_returning_invalid_object_raises():
    register_runner_factory("bad_factory", lambda: object())
    with pytest.raises(TypeError):
        get_runner("bad_factory")
