"""CP-7 Phase 3 — Take.execute + status compression tests.

These tests monkey-patch `pipeline.core.workflow._run_step_via_dispatch`
so they exercise Take.execute's status compression in isolation from
real dispatch. Phase 6 covers the real-dispatch chain.
"""

import sys
import pathlib
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 recoil.pipeline.core import workflow as wf_mod  # noqa: E402
from recoil.pipeline.core.take import Take, _compress_step_status  # noqa: E402
from recoil.pipeline.core.workflow import Workflow, WorkflowStep  # noqa: E402


def _step(step_id="s1", modality="image_t2i", depends_on=None) -> WorkflowStep:
    return WorkflowStep(
        step_id=step_id, modality=modality,
        payload={"shot_id": "X", "prompt": "p", "model": "nbp"},
        depends_on=depends_on or [],
    )


def _fake_receipt(success=True, error=None, modality="image_t2i", shot_id="X"):
    rr = SimpleNamespace(
        success=success, error=error, modality=modality,
        output_path="/tmp/x" if success else None,
    )
    return SimpleNamespace(
        run_result=rr, modality=modality,
        receipt_id=f"rcpt_fake_{shot_id}_{modality}",
        provenance={},
    )


@pytest.fixture
def patch_dispatch(monkeypatch):
    def _make_patch(fn):
        monkeypatch.setattr(wf_mod, "_run_step_via_dispatch", fn)
    return _make_patch


def _wf(steps) -> Workflow:
    return Workflow(workflow_id="wf1", steps=steps)


# ── Status compression unit tests ──────────────────────────────────────

@pytest.mark.parametrize("statuses,expected", [
    (["succeeded", "succeeded"], "succeeded"),
    (["failed", "failed"], "failed"),
    (["skipped", "skipped"], "failed"),
    (["succeeded", "failed"], "partial"),
    (["succeeded", "skipped"], "partial"),
    (["failed", "skipped"], "failed"),
])
def test_compress_status_combinations(statuses, expected):
    wf = _wf([_step(step_id=f"s{i}") for i in range(len(statuses))])
    for s, st in zip(wf.steps, statuses):
        s.status = st
    assert _compress_step_status(wf) == expected


# ── Take.execute integration with monkey-patched dispatch ──────────────

def test_take_execute_returns_self(patch_dispatch):
    patch_dispatch(lambda step, wf, ctx: _fake_receipt(modality=step.modality))
    take = Take(take_id="t0", take_index=0, workflow=_wf([_step(step_id="kf")]))
    assert take.execute(context=object()) is take


def test_take_execute_succeeded(patch_dispatch):
    patch_dispatch(lambda step, wf, ctx: _fake_receipt(modality=step.modality))
    take = Take(take_id="t0", take_index=0, workflow=_wf([
        _step(step_id="kf"),
        _step(step_id="vid", modality="video_i2v", depends_on=["kf"]),
    ]))
    take.execute(context=object())
    assert take.status == "succeeded"


def test_take_execute_failed(patch_dispatch):
    patch_dispatch(lambda step, wf, ctx: _fake_receipt(success=False, error="boom",
                                                       modality=step.modality))
    take = Take(take_id="t0", take_index=0, workflow=_wf([_step(step_id="kf")]))
    take.execute(context=object())
    assert take.status == "failed"


def test_take_execute_partial(patch_dispatch):
    def _mixed(step, wf, ctx):
        ok = step.step_id == "kf"
        return _fake_receipt(success=ok, error=None if ok else "boom",
                             modality=step.modality)
    patch_dispatch(_mixed)
    take = Take(take_id="t0", take_index=0, workflow=_wf([
        _step(step_id="kf"),
        _step(step_id="vid", modality="video_i2v", depends_on=["kf"]),
        _step(step_id="post", modality="image_t2i", depends_on=["vid"]),
    ]))
    take.execute(context=object())
    assert take.status == "partial"
    assert [s.status for s in take.workflow.steps] == ["succeeded", "failed", "skipped"]


def test_take_execute_status_running_during_hook(patch_dispatch):
    patch_dispatch(lambda step, wf, ctx: _fake_receipt(modality=step.modality))
    take = Take(take_id="t0", take_index=0, workflow=_wf([_step(step_id="kf")]))
    observed = []
    take.execute(context=object(), pre_step=lambda s, w: observed.append(take.status))
    assert observed == ["running"] and take.status == "succeeded"


def test_take_execute_passes_hooks(patch_dispatch):
    patch_dispatch(lambda step, wf, ctx: _fake_receipt(modality=step.modality))
    take = Take(take_id="t0", take_index=0, workflow=_wf([
        _step(step_id="kf"),
        _step(step_id="vid", modality="video_i2v", depends_on=["kf"]),
    ]))
    log = []
    take.execute(context=object(),
                 pre_step=lambda s, w: log.append(("pre", s.step_id)),
                 post_step=lambda s, w: log.append(("post", s.step_id, s.status)))
    assert log == [("pre", "kf"), ("post", "kf", "succeeded"),
                   ("pre", "vid"), ("post", "vid", "succeeded")]


def test_take_execute_re_run_resets(patch_dispatch):
    counter = {"n": 0}
    def _ok(step, wf, ctx):
        counter["n"] += 1
        return _fake_receipt(modality=step.modality)
    patch_dispatch(_ok)
    take = Take(take_id="t0", take_index=0, workflow=_wf([_step(step_id="kf")]))
    take.execute(context=object())
    take.execute(context=object())
    assert counter["n"] == 2 and take.status == "succeeded"


def test_take_execute_hook_raises_sets_failed_status(patch_dispatch):
    """LOCKED-1 from spec-review: when Workflow.run raises (hook exception
    per CP-6 behavior), Take.status MUST land in a terminal value via the
    finally-block compression. Without try/finally, Take is stuck in
    'running' permanently — the same bug class CP-6 post-build LOCKED-1
    fixed for Workflow.steps.

    Scenario: 2-step workflow [kf, vid]. kf succeeds (dispatched via the
    patched _run_step_via_dispatch returning a fake success receipt). vid's
    pre_step hook raises RuntimeError. Workflow.run propagates the exception
    (CP-6 STRICT contract). Take.execute MUST:
      - re-raise the exception (so caller knows something failed)
      - leave Take.status as 'partial' (kf succeeded, vid did not)
      - NOT leave Take.status as 'running'
    """
    patch_dispatch(lambda step, wf, ctx: _fake_receipt(modality=step.modality))
    take = Take(
        take_id="t0",
        take_index=0,
        workflow=_wf([
            _step(step_id="kf"),
            _step(step_id="vid", modality="video_i2v", depends_on=["kf"]),
        ]),
    )

    def _boom_on_vid(step, workflow):
        if step.step_id == "vid":
            raise RuntimeError("hook boom")

    with pytest.raises(RuntimeError, match="hook boom"):
        take.execute(context=object(), pre_step=_boom_on_vid)

    # Must NOT be 'running' — finally must have run compression.
    assert take.status != "running", (
        f"Take stuck in 'running' after hook exception — LOCKED-1 fix not applied. "
        f"Got status={take.status!r}"
    )
    # kf succeeded, vid hook raised (vid status='failed' per CP-6 hook fix),
    # so compression yields 'partial'.
    assert take.status == "partial", (
        f"Expected 'partial' (kf succeeded, vid failed via hook), got {take.status!r}"
    )
