"""dispatch() entry point tests."""

import sys
import pathlib
import json
from types import SimpleNamespace

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 unittest.mock import MagicMock, patch  # noqa: E402

from recoil.pipeline.core.dispatch import dispatch, _reset_bootstrap_for_tests  # noqa: E402
from recoil.pipeline.core.dispatch_context import DispatchContext  # noqa: E402
from recoil.pipeline.core.exceptions import ModelConstraintError  # noqa: E402
from recoil.pipeline.core.receipts import GenerationReceipt  # noqa: E402
from recoil.pipeline.core.registry import _reset_for_tests  # noqa: E402


def _step_result(**overrides):
    base = dict(
        shot_id="X", success=True, final_state="keyframe_generated",
        output_path="/tmp/x.png", cost_usd=0.04, error=None,
        take_index=0, gate_verdict=None, model="nbp", pipeline="still",
    )
    base.update(overrides)
    return SimpleNamespace(**base)


class _SR:
    def __init__(self):
        self.calls = []
        self._dispatch_path = "unknown"
    def execute_keyframe(self, **kw):
        self.calls.append(("keyframe", dict(kw)))
        return _step_result()
    def execute_video(self, **kw):
        self.calls.append(("video", dict(kw)))
        return _step_result(output_path="/tmp/v.mp4", final_state="video_complete",
                            cost_usd=0.20, model="seeddance-2.0", pipeline="i2v")


@pytest.fixture(autouse=True)
def reset():
    _reset_for_tests()
    _reset_bootstrap_for_tests()
    yield
    _reset_for_tests()
    _reset_bootstrap_for_tests()


def test_dispatch_image_happy_path(tmp_path):
    sr = _SR()
    ctx = DispatchContext(
        caller_id="test",
        step_runner=sr,
        project="tartarus", episode=1,
        receipts_log_path=str(tmp_path / "r.jsonl"),
    )
    receipt = dispatch("image_t2i", {"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    assert isinstance(receipt, GenerationReceipt)
    assert receipt.modality == "image_t2i"
    assert receipt.run_result.success is True
    assert receipt.run_result.output_path == "/tmp/x.png"
    assert receipt.caller_id == "test"
    assert receipt.project == "tartarus"
    assert receipt.episode == 1
    assert receipt.shot_id == "X"
    assert receipt.provenance["dispatch_path"] == "test"
    assert receipt.eval_scores == {}


def test_dispatch_video_happy_path(tmp_path):
    sr = _SR()
    ctx = DispatchContext(caller_id="test", step_runner=sr,
                          receipts_log_path=str(tmp_path / "r.jsonl"))
    receipt = dispatch("video_i2v", {"shot_id": "X", "prompt": "p", "model": "seeddance-2.0", "aspect_ratio": "9_16"}, context=ctx)
    assert receipt.run_result.success is True
    assert receipt.modality == "video_i2v"


def test_dispatch_audio_real_runner_validation_failure_receipt(tmp_path):
    """Post-CP-8: dispatch into the real AudioRunner with a payload missing
    required keys returns a failure-RunResult (NOT a NotImplementedError).
    Receipt still emits; final_state is 'failed' (canonical failure metadata)."""
    sr = _SR()
    ctx = DispatchContext(caller_id="test", step_runner=sr,
                          receipts_log_path=str(tmp_path / "r.jsonl"))
    receipt = dispatch("audio_t2a", {"prompt": "x"}, context=ctx)
    assert receipt.run_result.success is False
    err = receipt.run_result.error or ""
    assert "missing required keys" in err
    for key in ("shot_id", "text", "voice_id", "model"):
        assert key in err
    assert receipt.run_result.metadata.get("final_state") == "failed"


def test_dispatch_unknown_modality_raises_keyerror(tmp_path):
    sr = _SR()
    ctx = DispatchContext(caller_id="test", step_runner=sr,
                          receipts_log_path=str(tmp_path / "r.jsonl"))
    with pytest.raises(KeyError) as exc:
        dispatch("bogus_modality", {}, context=ctx)
    msg = str(exc.value)
    assert "bogus_modality" in msg


def test_dispatch_writes_jsonl(tmp_path):
    sr = _SR()
    log = tmp_path / "r.jsonl"
    ctx = DispatchContext(caller_id="test", step_runner=sr, receipts_log_path=str(log))
    dispatch("image_t2i", {"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    dispatch("image_t2i", {"shot_id": "Y", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    lines = [json.loads(line) for line in log.read_text().splitlines() if line.strip()]
    assert len(lines) == 2
    assert lines[0]["shot_id"] == "X"
    assert lines[1]["shot_id"] == "Y"
    # round-trip
    g = GenerationReceipt.from_dict(lines[0])
    assert g.modality == "image_t2i"


def test_dispatch_disable_jsonl(tmp_path):
    sr = _SR()
    log = tmp_path / "r.jsonl"
    ctx = DispatchContext(caller_id="test", step_runner=sr, receipts_log_path="DISABLED")
    dispatch("image_t2i", {"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    assert not log.exists()


def test_dispatch_stamps_dispatch_path_on_step_runner(tmp_path):
    sr = _SR()
    assert sr._dispatch_path == "unknown"
    ctx = DispatchContext(caller_id="production_loop", step_runner=sr,
                          receipts_log_path=str(tmp_path / "r.jsonl"))
    dispatch("image_t2i", {"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    assert sr._dispatch_path == "production_loop"


def test_dispatch_bootstrap_idempotent(tmp_path):
    sr = _SR()
    ctx = DispatchContext(caller_id="t", step_runner=sr,
                          receipts_log_path=str(tmp_path / "r.jsonl"))
    dispatch("image_t2i", {"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    dispatch("image_t2i", {"shot_id": "Y", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    # No exception → bootstrap memoized correctly


def test_dispatch_payload_must_be_dict():
    sr = _SR()
    ctx = DispatchContext(caller_id="t", step_runner=sr, receipts_log_path="DISABLED")
    with pytest.raises(TypeError):
        dispatch("image_t2i", ["not", "a", "dict"], context=ctx)


def test_dispatch_context_must_be_dispatch_context():
    with pytest.raises(TypeError):
        dispatch("image_t2i", {}, context={"caller_id": "t"})  # raw dict not allowed


def test_dispatch_provenance_includes_payload_keys(tmp_path):
    sr = _SR()
    ctx = DispatchContext(caller_id="t", step_runner=sr,
                          receipts_log_path=str(tmp_path / "r.jsonl"))
    receipt = dispatch("image_t2i",
                       {"shot_id": "X", "prompt": "p", "model": "nbp",
                        "extra_orch": "blah"},
                       context=ctx)
    assert "shot_id" in receipt.provenance["payload_keys"]
    assert "model" in receipt.provenance["payload_keys"]
    assert "extra_orch" in receipt.provenance["payload_keys"]
    assert receipt.provenance["model"] == "nbp"


def test_dispatch_provenance_overrides_merged(tmp_path):
    sr = _SR()
    ctx = DispatchContext(caller_id="t", step_runner=sr,
                          receipts_log_path=str(tmp_path / "r.jsonl"),
                          provenance_overrides={"prompt_hash": "abc123"})
    receipt = dispatch("image_t2i", {"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}, context=ctx)
    assert receipt.provenance["prompt_hash"] == "abc123"
    assert receipt.provenance["dispatch_path"] == "t"  # not clobbered


# ── Phase 2 — gpt-image-2 constraint validator tests ──────────────────────


def _ctx() -> DispatchContext:
    """Construct a minimal DispatchContext that won't bootstrap real runners."""
    sr = MagicMock()
    sr._dispatch_path = "test"
    return DispatchContext(
        caller_id="test_constraints",
        step_runner=sr,
        project=None,  # disable aspect_ratio injection
        episode=None,
    )


@patch("recoil.pipeline.core.dispatch.get_runner")
@patch("recoil.core.model_profiles.get_profile", return_value={
    "supports_transparent_bg": False,
    "multi_ref_supported": False,
})
def test_gpt_image_2_transparent_bg_raises(mock_profile, mock_runner):
    """gpt-image-2 + background='transparent' → ModelConstraintError before runner.run()."""
    payload = {
        "model": "gpt-image-2",
        "prompt": "x",
        "aspect_ratio": "1:1",
        "background": "transparent",
    }
    with pytest.raises(ModelConstraintError, match="transparent background"):
        dispatch("image_t2i", payload, context=_ctx())
    mock_runner.return_value.run.assert_not_called()


@patch("recoil.pipeline.core.dispatch.get_runner")
@patch("recoil.core.model_profiles.get_profile", return_value={
    "supports_transparent_bg": False,
    "multi_ref_supported": False,
})
def test_gpt_image_2_multi_ref_raises(mock_profile, mock_runner):
    """gpt-image-2 + len(image_urls)>1 → ModelConstraintError. Also covers identity_refs key."""
    payload_image_urls = {
        "model": "gpt-image-2",
        "prompt": "x",
        "aspect_ratio": "1:1",
        "image_urls": ["http://a.png", "http://b.png"],
    }
    with pytest.raises(ModelConstraintError, match="multi-ref"):
        dispatch("image_t2i", payload_image_urls, context=_ctx())

    payload_identity_refs = {
        "model": "gpt-image-2",
        "prompt": "x",
        "aspect_ratio": "1:1",
        "identity_refs": ["http://a.png", "http://b.png"],
    }
    with pytest.raises(ModelConstraintError, match="multi-ref"):
        dispatch("image_t2i", payload_identity_refs, context=_ctx())

    mock_runner.return_value.run.assert_not_called()


@patch("recoil.pipeline.core.dispatch.get_runner")
@patch("recoil.core.model_profiles.get_profile", return_value={
    "supports_transparent_bg": False,
    "multi_ref_supported": False,
})
def test_gpt_image_2_valid_payload_passes(mock_profile, mock_runner):
    """Valid gpt-image-2 payload (single ref, no transparent bg) → no exception, runner called."""
    payload = {
        "model": "gpt-image-2",
        "prompt": "x",
        "aspect_ratio": "1:1",
        "image_urls": ["http://a.png"],
    }
    from recoil.pipeline.core.registry import RunResult
    mock_runner.return_value.run.return_value = RunResult(
        id="test", modality="image_t2i", output_path=None, output_url=None,
        metadata={}, success=True, error=None,
    )
    receipt = dispatch("image_t2i", payload, context=_ctx())
    assert receipt.run_result.success
    mock_runner.return_value.run.assert_called_once()


@patch("recoil.pipeline.core.dispatch.get_runner")
@patch("recoil.core.model_profiles.get_profile", return_value={
    # seedream-v4.5 declares neither flag → defaults (True) → no constraint fires.
})
def test_seedream_v4_5_transparent_bg_passes_no_constraint(mock_profile, mock_runner):
    """A model without the constraint flags passes through — no regression for non-gpt-image-2."""
    payload = {
        "model": "seedream-v4.5",
        "prompt": "x",
        "aspect_ratio": "1:1",
        "background": "transparent",
    }
    from recoil.pipeline.core.registry import RunResult
    mock_runner.return_value.run.return_value = RunResult(
        id="test", modality="image_t2i", output_path=None, output_url=None,
        metadata={}, success=True, error=None,
    )
    receipt = dispatch("image_t2i", payload, context=_ctx())
    assert receipt.run_result.success
    mock_runner.return_value.run.assert_called_once()
