"""CP-6 Phase 4 — Workflow + real dispatch() integration tests.

These tests exercise the full chain: Workflow.run → dispatch() → registry
→ runner.run() → StepResult → RunResult → GenerationReceipt → step.receipt.
StepRunner is stubbed (no real API calls); everything else is live.
"""

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 recoil.pipeline.core.dispatch import _reset_bootstrap_for_tests  # noqa: E402
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 _reset_for_tests  # noqa: E402
from recoil.pipeline.core.workflow import Workflow, WorkflowStep  # 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 _StubStepRunner:
    def __init__(self):
        self.calls: list[tuple[str, dict]] = []
        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_single_step_workflow_runs_via_real_dispatch(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="workflow_test",
        step_runner=sr,
        project="tartarus",
        episode=1,
        receipts_log_path=str(tmp_path / "receipts.jsonl"),
    )
    wf = Workflow(workflow_id="wf_single", steps=[
        WorkflowStep(step_id="kf", modality="image_t2i",
                     payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
    ])
    wf.run(context=ctx)

    assert wf.steps[0].status == "succeeded"
    assert wf.steps[0].receipt is not None
    assert isinstance(wf.steps[0].receipt, GenerationReceipt)
    assert wf.steps[0].receipt.run_result.success is True
    assert wf.steps[0].receipt.run_result.output_path == "/tmp/x.png"
    # workflow identity stamped on receipt
    prov = wf.steps[0].receipt.provenance
    assert prov["workflow_id"] == "wf_single"
    assert prov["workflow_step_id"] == "kf"
    assert prov["dispatch_path"] == "workflow_test"


def test_two_step_workflow_keyframe_then_video(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        project="tartarus", episode=1,
        receipts_log_path=str(tmp_path / "receipts.jsonl"),
    )
    wf = Workflow(workflow_id="wf_kf_vid", steps=[
        WorkflowStep(step_id="kf", modality="image_t2i",
                     payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
        WorkflowStep(step_id="vid", modality="video_i2v",
                     payload={"shot_id": "X", "prompt": "p", "model": "seeddance-2.0", "aspect_ratio": "9_16"},
                     depends_on=["kf"]),
    ])
    wf.run(context=ctx)

    assert wf.steps[0].status == "succeeded"
    assert wf.steps[1].status == "succeeded"
    assert wf.steps[1].receipt.modality == "video_i2v"
    assert wf.steps[1].receipt.provenance["workflow_step_id"] == "vid"
    # StepRunner saw both calls
    methods = [c[0] for c in sr.calls]
    assert methods == ["keyframe", "video"]


def test_workflow_global_provenance_flows_into_receipt(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        receipts_log_path=str(tmp_path / "receipts.jsonl"),
    )
    wf = Workflow(
        workflow_id="wf_prov", steps=[
            WorkflowStep(step_id="kf", modality="image_t2i",
                         payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
        ],
        global_provenance={"scene_id": "scene_3", "tags": ["beat_climax"]},
    )
    wf.run(context=ctx)
    prov = wf.steps[0].receipt.provenance
    assert prov["scene_id"] == "scene_3"
    assert prov["tags"] == ["beat_climax"]
    assert prov["workflow_id"] == "wf_prov"
    assert prov["workflow_step_id"] == "kf"


def test_workflow_step_id_overrides_caller_provenance_for_workflow_keys(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        receipts_log_path=str(tmp_path / "receipts.jsonl"),
        provenance_overrides={
            "workflow_id": "should_be_overridden",
            "workflow_step_id": "should_also_be_overridden",
            "tag": "kept",
        },
    )
    wf = Workflow(workflow_id="canonical_wf", steps=[
        WorkflowStep(step_id="kf", modality="image_t2i",
                     payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
    ])
    wf.run(context=ctx)
    prov = wf.steps[0].receipt.provenance
    assert prov["workflow_id"] == "canonical_wf"
    assert prov["workflow_step_id"] == "kf"
    assert prov["tag"] == "kept"


def test_workflow_emits_jsonl_receipts(tmp_path):
    sr = _StubStepRunner()
    log = tmp_path / "receipts.jsonl"
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        receipts_log_path=str(log),
    )
    wf = Workflow(workflow_id="wf1", steps=[
        WorkflowStep(step_id="kf", modality="image_t2i",
                     payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
        WorkflowStep(step_id="vid", modality="video_i2v",
                     payload={"shot_id": "X", "prompt": "p", "model": "seeddance-2.0", "aspect_ratio": "9_16"},
                     depends_on=["kf"]),
    ])
    wf.run(context=ctx)
    lines = [json.loads(line) for line in log.read_text().splitlines() if line.strip()]
    assert len(lines) == 2
    # Each line is a serialized GenerationReceipt
    assert lines[0]["modality"] == "image_t2i"
    assert lines[1]["modality"] == "video_i2v"
    assert lines[0]["provenance"]["workflow_step_id"] == "kf"
    assert lines[1]["provenance"]["workflow_step_id"] == "vid"


def test_workflow_disable_jsonl_via_dispatch_context(tmp_path):
    sr = _StubStepRunner()
    log = tmp_path / "receipts.jsonl"
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        receipts_log_path="DISABLED",
    )
    wf = Workflow(workflow_id="wf1", steps=[
        WorkflowStep(step_id="kf", modality="image_t2i",
                     payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
    ])
    wf.run(context=ctx)
    assert not log.exists()


def test_workflow_unknown_modality_marks_step_failed(tmp_path):
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        receipts_log_path="DISABLED",
    )
    wf = Workflow(workflow_id="wf1", steps=[
        WorkflowStep(step_id="bogus", modality="totally_bogus",
                     payload={"shot_id": "X"}),
    ])
    wf.run(context=ctx)
    # KeyError from get_runner is caught by executor → step failed, no receipt
    assert wf.steps[0].status == "failed"
    assert wf.steps[0].receipt is None
    assert "KeyError" in (wf.steps[0].error or "") or "totally_bogus" in (wf.steps[0].error or "")


def test_workflow_audio_real_runner_validation_marks_step_failed(tmp_path):
    """Post-CP-8: AudioRunner is real. A workflow with an audio_t2a step whose
    payload is missing required keys produces a failed step with a
    failure-RunResult (no NotImplementedError); the receipt is still attached."""
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        receipts_log_path="DISABLED",
    )
    wf = Workflow(workflow_id="wf1", steps=[
        WorkflowStep(step_id="aud", modality="audio_t2a",
                     payload={"prompt": "x"}),
    ])
    wf.run(context=ctx)
    assert wf.steps[0].status == "failed"
    assert wf.steps[0].receipt is not None
    err = wf.steps[0].receipt.run_result.error or ""
    assert "missing required keys" in err


def test_workflow_run_requires_dispatch_context():
    wf = Workflow(workflow_id="wf1", steps=[
        WorkflowStep(step_id="kf", modality="image_t2i",
                     payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
    ])
    # Real dispatch path enforces DispatchContext type via _run_step_via_dispatch.
    # Behavior: TypeError surfaces inside the executor → step.status = "failed".
    wf.run(context={"not": "a dispatch context"})  # type: ignore
    assert wf.steps[0].status == "failed"
    assert "TypeError" in (wf.steps[0].error or "")


def test_workflow_public_surface_importable():
    from recoil.pipeline.core import Workflow, WorkflowStep  # noqa: F401


def test_executed_workflow_round_trip(tmp_path):
    """A workflow executed end-to-end should round-trip through to_dict/from_dict
    with all receipts intact."""
    sr = _StubStepRunner()
    ctx = DispatchContext(
        caller_id="workflow_test", step_runner=sr,
        project="tartarus", episode=1,
        receipts_log_path=str(tmp_path / "receipts.jsonl"),
    )
    wf = Workflow(workflow_id="wf_rt", steps=[
        WorkflowStep(step_id="kf", modality="image_t2i",
                     payload={"shot_id": "X", "prompt": "p", "model": "nbp", "aspect_ratio": "9_16"}),
        WorkflowStep(step_id="vid", modality="video_i2v",
                     payload={"shot_id": "X", "prompt": "p", "model": "seeddance-2.0", "aspect_ratio": "9_16"},
                     depends_on=["kf"]),
    ])
    wf.run(context=ctx)
    wf2 = Workflow.from_dict(wf.to_dict())
    assert wf2.steps[0].status == "succeeded"
    assert wf2.steps[1].status == "succeeded"
    assert wf2.steps[0].receipt is not None
    assert wf2.steps[1].receipt is not None
    assert wf2.steps[1].receipt.provenance["workflow_step_id"] == "vid"
