"""CP-8 Phase 5 — dispatch integration for audio_t2a + lipsync_post.

Validates that dispatch("audio_t2a", ...) and dispatch("lipsync_post", ...)
return real GenerationReceipts with output_path on disk and emit JSONL audit
log entries. ALL HTTP transport is mocked via the `_transport=` payload key
the runners forward to the adapters.
"""

import json
import sys
import pathlib

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


from recoil.pipeline.core.dispatch import (  # noqa: E402
    dispatch,
    register_default_runners,
)
from recoil.pipeline.core.dispatch_context import DispatchContext  # noqa: E402
from recoil.pipeline.core.receipts import GenerationReceipt  # noqa: E402
from recoil.pipeline.core.registry import (  # noqa: E402
    get_runner,
)
from recoil.pipeline.core.runners.audio_runner import AudioRunner  # noqa: E402
from recoil.pipeline.core.runners.lipsync_post import LipSyncPostProcessor  # noqa: E402
from recoil.execution.providers import elevenlabs as _eleven  # noqa: E402


# ── Stub StepRunner (CP-4 / CP-5 idiom) ──────────────────────────────────


class _StubStepRunner:
    """Minimal StepRunner stub mirroring _SR / _StubStepRunner in test_dispatch.py
    and test_workflow_dispatch_integration.py.

    Audio + lipsync runners do NOT consult the StepRunner — they call their
    adapters directly — but dispatch() requires `step_runner` on the
    DispatchContext for bootstrap and `_dispatch_path` stamping.
    """

    def __init__(self):
        self.calls: list[tuple[str, dict]] = []
        self._dispatch_path = "unknown"

    def execute_keyframe(self, **kw):  # pragma: no cover — not called by audio/lipsync
        self.calls.append(("keyframe", dict(kw)))
        return None

    def execute_video(self, **kw):  # pragma: no cover — not called by audio/lipsync
        self.calls.append(("video", dict(kw)))
        return None


# ── Fake HTTP transports ────────────────────────────────────────────────


class _FakeResponse:
    """Mimics urllib HTTPResponse just enough for both adapters."""

    def __init__(self, body: bytes, status: int = 200, headers=None):
        self._body = body
        self.status = status
        self.headers = headers or {"request-id": "req_test_123"}

    def read(self) -> bytes:
        return self._body

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False


def _make_audio_transport(audio_bytes: bytes = b"FAKE_MP3_BYTES"):
    """Transport that returns 200 OK with the bytes for ElevenLabs TTS."""

    def _transport(url, *, headers, body, timeout):
        return _FakeResponse(audio_bytes)

    return _transport


def _make_audio_raising_transport(exc):
    def _transport(url, *, headers, body, timeout):
        raise exc

    return _transport


def _make_lipsync_5step_transport(
    *,
    job_id: str = "job_xyz",
    output_bytes: bytes = b"FAKE_MP4_BYTES",
    duration_s: float = 4.2,
):
    """5-step sync.so protocol: upload×2, submit, poll PROCESSING, poll COMPLETED, download."""
    state = {"poll_count": 0}

    def _transport(url, *, headers, body, method="GET", timeout=60.0):
        if "/v2/upload" in url:
            return _FakeResponse(
                json.dumps({"url": "https://cdn.sync.so/file_xyz"}).encode("utf-8")
            )
        if url.endswith("/v2/generate") and method == "POST":
            return _FakeResponse(json.dumps({"id": job_id}).encode("utf-8"))
        if "/v2/generate/" in url and method == "GET":
            state["poll_count"] += 1
            if state["poll_count"] <= 1:
                return _FakeResponse(
                    json.dumps({"status": "PROCESSING"}).encode("utf-8")
                )
            return _FakeResponse(
                json.dumps({
                    "status": "COMPLETED",
                    "outputUrl": "https://cdn.sync.so/output_v.mp4",
                    "duration_s": duration_s,
                }).encode("utf-8")
            )
        if "output_v.mp4" in url:
            return _FakeResponse(output_bytes)
        raise AssertionError(f"unexpected URL in fake transport: {url}")

    return _transport


def _make_video_and_audio(tmp_path):
    v = tmp_path / "in.mp4"
    a = tmp_path / "in.mp3"
    v.write_bytes(b"FAKE VIDEO BYTES")
    a.write_bytes(b"FAKE AUDIO BYTES")
    return v, a


# ── Fixtures ────────────────────────────────────────────────────────────


def _audio_payload(tmp_path, **overrides):
    base = {
        "shot_id": "EP001_SH02",
        "text": "Hello world",
        "voice_id": "voice_xyz",
        "model": "eleven_multilingual_v2",
        "output_dir": str(tmp_path / "audio_out"),
        "_transport": _make_audio_transport(),
    }
    base.update(overrides)
    return base


def _lipsync_payload(tmp_path, **overrides):
    v, a = _make_video_and_audio(tmp_path)
    base = {
        "shot_id": "EP001_SH02",
        "video_path": str(v),
        "audio_path": str(a),
        "model": "lipsync-2.0",
        "output_dir": str(tmp_path / "lipsync_out"),
        "_transport": _make_lipsync_5step_transport(),
        "poll_interval_s": 0.0,
    }
    base.update(overrides)
    return base


# ── Tests ───────────────────────────────────────────────────────────────


def test_dispatch_audio_t2a_returns_real_receipt_with_output_on_disk(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    receipt = dispatch("audio_t2a", _audio_payload(tmp_path), context=ctx)

    assert isinstance(receipt, GenerationReceipt)
    assert receipt.run_result.success is True
    assert receipt.run_result.modality == "audio_t2a"
    assert receipt.modality == "audio_t2a"
    assert receipt.run_result.output_path is not None
    assert pathlib.Path(receipt.run_result.output_path).is_file()
    assert (
        pathlib.Path(receipt.run_result.output_path).read_bytes()
        == b"FAKE_MP3_BYTES"
    )


def test_dispatch_lipsync_post_returns_real_receipt_with_output_on_disk(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    receipt = dispatch("lipsync_post", _lipsync_payload(tmp_path), context=ctx)

    assert isinstance(receipt, GenerationReceipt)
    assert receipt.run_result.success is True
    assert receipt.run_result.modality == "lipsync_post"
    assert receipt.modality == "lipsync_post"
    assert receipt.run_result.output_path is not None
    assert pathlib.Path(receipt.run_result.output_path).is_file()
    assert (
        pathlib.Path(receipt.run_result.output_path).read_bytes()
        == b"FAKE_MP4_BYTES"
    )


def test_dispatch_jsonl_audit_log_writes_one_line_per_dispatch(tmp_path):
    sr = _StubStepRunner()
    log = tmp_path / "receipts.jsonl"
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        receipts_log_path=str(log),
    )
    dispatch("audio_t2a", _audio_payload(tmp_path), context=ctx)
    dispatch("lipsync_post", _lipsync_payload(tmp_path), context=ctx)

    lines = [json.loads(line) for line in log.read_text().splitlines() if line.strip()]
    assert len(lines) == 2
    assert lines[0]["modality"] == "audio_t2a"
    assert lines[1]["modality"] == "lipsync_post"
    # Round-trip check: each line is a serialized GenerationReceipt
    g0 = GenerationReceipt.from_dict(lines[0])
    assert g0.modality == "audio_t2a"
    assert g0.run_result.success is True


def test_dispatch_audio_failure_surfaces_in_receipt(tmp_path):
    """Mock transport raises AuthError → receipt.run_result.success=False
    with error mentioning AuthError."""
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    payload = _audio_payload(
        tmp_path,
        _transport=_make_audio_raising_transport(_eleven.AuthError("401 invalid key")),
    )
    receipt = dispatch("audio_t2a", payload, context=ctx)

    assert receipt.run_result.success is False
    assert receipt.run_result.error is not None
    assert "AuthError" in receipt.run_result.error
    assert receipt.run_result.metadata.get("error_class") == "AuthError"
    assert receipt.run_result.metadata.get("final_state") == "failed"


def test_dispatch_lipsync_failure_surfaces_in_receipt(tmp_path):
    """Lipsync with a video_path that doesn't exist on disk → adapter raises
    PayloadError → receipt.run_result.error mentions PayloadError."""
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    _, a = _make_video_and_audio(tmp_path)
    payload = {
        "shot_id": "EP001_SH02",
        "video_path": str(tmp_path / "missing.mp4"),
        "audio_path": str(a),
        "model": "lipsync-2.0",
        "output_dir": str(tmp_path / "lipsync_out"),
    }
    receipt = dispatch("lipsync_post", payload, context=ctx)

    assert receipt.run_result.success is False
    assert receipt.run_result.error is not None
    assert receipt.run_result.metadata.get("error_class") == "PayloadError"


def test_dispatch_provenance_stamps_dispatch_path_payload_keys_and_caller(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="production_loop",
        step_runner=sr,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    receipt = dispatch("audio_t2a", _audio_payload(tmp_path), context=ctx)

    prov = receipt.provenance
    assert prov["dispatch_path"] == "production_loop"
    assert receipt.caller_id == "production_loop"
    # _transport key is filtered out (starts with _); other keys present.
    payload_keys = prov["payload_keys"]
    assert "shot_id" in payload_keys
    assert "text" in payload_keys
    assert "voice_id" in payload_keys
    assert "model" in payload_keys
    assert "output_dir" in payload_keys
    assert "_transport" not in payload_keys  # filtered
    assert prov["model"] == "eleven_multilingual_v2"


def test_registry_resolves_real_audio_runner(tmp_path):
    """Post-CP-8: get_runner('audio_t2a') returns a real AudioRunner instance,
    not the deleted CP-4 stub."""
    sr = _StubStepRunner()
    register_default_runners(sr, force=True)
    runner = get_runner("audio_t2a")
    assert isinstance(runner, AudioRunner)
    assert runner.modality == "audio_t2a"


def test_registry_resolves_real_lipsync_runner(tmp_path):
    """Post-CP-8: get_runner('lipsync_post') returns a real LipSyncPostProcessor."""
    sr = _StubStepRunner()
    register_default_runners(sr, force=True)
    runner = get_runner("lipsync_post")
    assert isinstance(runner, LipSyncPostProcessor)
    assert runner.modality == "lipsync_post"


def test_dispatch_malformed_audio_payload_surfaces_in_receipt(tmp_path):
    """Missing shot_id (and other required keys) → receipt.success=False with
    error mentioning the missing keys. Runner does NOT raise."""
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    # Missing shot_id; AudioRunner returns failure-RunResult, dispatch wraps it.
    receipt = dispatch(
        "audio_t2a",
        {"text": "hi", "voice_id": "v", "model": "m"},
        context=ctx,
    )
    assert receipt.run_result.success is False
    err = receipt.run_result.error or ""
    assert "missing required keys" in err
    assert "shot_id" in err
    # Receipt is still emitted (dispatch never raises on validation failure).
    assert receipt.run_result.metadata.get("final_state") == "failed"


def test_consecutive_dispatches_share_context_unique_receipt_ids(tmp_path):
    """Two consecutive dispatches with the same DispatchContext produce two
    distinct GenerationReceipts with unique receipt_ids."""
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    r1 = dispatch("audio_t2a", _audio_payload(tmp_path), context=ctx)
    r2 = dispatch("audio_t2a", _audio_payload(tmp_path, shot_id="EP001_SH03"), context=ctx)

    assert r1.receipt_id != r2.receipt_id
    assert r1.shot_id == "EP001_SH02"
    assert r2.shot_id == "EP001_SH03"
    # Same caller_id (context is shared)
    assert r1.caller_id == r2.caller_id == "test"
    assert r1.run_result.success is True
    assert r2.run_result.success is True
