"""REC-164 Phase 4 — D3 consumer staleness guard in generate.run_generation.

Exercises the LIVE refuse path: a tmp project on disk, a real plan + locked
coverage_passes, and a derivation manifest whose recorded plan_structural_sha
either matches (fresh → dispatch) or mismatches/absent (stale → hard-refuse,
no dispatch). The live EpisodeRunner stack is mocked so the only thing under
test is the guard + its CLI/direct-call wiring.
"""
from __future__ import annotations

import json
import sys

import pytest

from recoil.execution.step_types import ProjectPaths
from recoil.pipeline._lib import derivation_manifest
from recoil.pipeline._lib.derivation_sha import plan_structural_sha
from recoil.pipeline.cli import generate


PROJECT = "fixture"
EPISODE = 1
PASS_ID = "PASS_011"
STALE_SHA = "sha256:" + "0" * 64


def _plan_dict() -> dict:
    return {
        "episode_id": "ep_001",
        "project": PROJECT,
        "shots": [
            {
                "shot_id": "EP001_SH01",
                "scene_index": 1,
                "routing_data": {"is_env_only": False, "target_editorial_duration_s": 4},
                "prompt_data": {"shot_type": "MS"},
                "asset_data": {"location_id": "L1", "characters": [{"char_id": "JADE"}]},
                "spatial_data": {"camera_side": "A"},
            }
        ],
    }


def _coverage_pass_dict() -> dict:
    return {
        "pass_id": PASS_ID,
        "episode_id": "ep_001",
        "shot_range": ["EP001_SH01", "EP001_SH01"],
        "camera_side": "A",
        "label": "fixture",
        "focus_character": "",
        "pass_type": "env",
        "location_id": "L1",
        "generation_config": {"mode": "t2v"},
        "segments": [
            {
                "segment_index": 0,
                "source_shot_id": "EP001_SH01",
                "shot_type": "MS",
                "duration_s": 2,
                "prompt": "shot EP001_SH01",
            }
        ],
    }


@pytest.fixture
def project_root(tmp_path, monkeypatch):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    (root / PROJECT).mkdir()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    return root


@pytest.fixture
def fake_dispatch(monkeypatch):
    """Stub the live EpisodeRunner dispatch stack so a guard-passing run does no
    real generation. Records whether the runner was constructed / dispatched."""
    calls = {"runner_constructed": False, "dispatched": False}

    class FakeRunner:
        def __init__(self, **kwargs):
            calls["runner_constructed"] = True

        async def run_episode_batches(self, *args, **kwargs):
            calls["dispatched"] = True
            return []

    monkeypatch.setattr(generate, "validate_all_passes", lambda passes: [])
    monkeypatch.setattr(generate, "ExecutionStore", lambda *a, **k: object())
    monkeypatch.setattr(generate, "StepRunner", lambda *a, **k: object())
    monkeypatch.setattr(generate, "StrategyEngine", lambda *a, **k: object())
    monkeypatch.setattr(generate, "LearningEngine", lambda *a, **k: object())
    monkeypatch.setattr(generate, "EpisodeRunner", FakeRunner)
    return calls


def _write_plan() -> dict:
    paths = ProjectPaths.for_episode(PROJECT, EPISODE)
    plan = _plan_dict()
    plan_path = paths.plans_dir / f"ep_{EPISODE:03d}_plan.json"
    plan_path.parent.mkdir(parents=True, exist_ok=True)
    plan_path.write_text(json.dumps(plan), encoding="utf-8")
    return plan


def _write_passes() -> None:
    paths = ProjectPaths.for_episode(PROJECT, EPISODE)
    passes_path = paths.coverage_passes_dir / f"ep_{EPISODE:03d}_passes.json"
    passes_path.parent.mkdir(parents=True, exist_ok=True)
    passes_path.write_text(json.dumps([_coverage_pass_dict()]), encoding="utf-8")


def _stamp_coverage_passes(recorded_sha: str) -> None:
    """Stamp the coverage_passes manifest entry exactly as the Phase-3 producer
    does, with the supplied recorded plan_structural_sha."""
    derivation_manifest.stamp_stage(
        PROJECT,
        EPISODE,
        "coverage_passes",
        kind="derived",
        structural_sha=None,
        content_sha="sha256:passes",
        source={"plan_structural_sha": recorded_sha},
        builder="build_coverage_passes --lock",
        built_at="2026-06-14T00:00:00+00:00",
    )


def _run_main(monkeypatch, capsys, *flags: str) -> tuple[int, dict]:
    monkeypatch.setattr(
        sys,
        "argv",
        ["generate.py", "--project", PROJECT, "--episode", str(EPISODE), *flags],
    )
    code = generate.main()
    return code, json.loads(capsys.readouterr().out)


def test_guard_refuses_on_mismatch(project_root, fake_dispatch):
    plan = _write_plan()
    _write_passes()
    current = plan_structural_sha(plan)
    assert STALE_SHA != current
    _stamp_coverage_passes(STALE_SHA)

    result = generate.run_generation(PROJECT, EPISODE, pass_ids=[PASS_ID])

    assert result["success"] is False
    assert result["error"] == "coverage_passes_stale"
    assert result["recorded"] == STALE_SHA
    assert result["current"] == current
    assert f"ep_{EPISODE:03d}" in result["message"]
    # No dispatch: the runner is never constructed and run_episode_batches never runs.
    assert fake_dispatch["runner_constructed"] is False
    assert fake_dispatch["dispatched"] is False


def test_guard_passes_on_match(project_root, fake_dispatch):
    plan = _write_plan()
    _write_passes()
    _stamp_coverage_passes(plan_structural_sha(plan))  # recorded == current

    result = generate.run_generation(PROJECT, EPISODE, pass_ids=[PASS_ID])

    assert result.get("error") != "coverage_passes_stale"
    assert result["success"] is True
    assert fake_dispatch["dispatched"] is True


def test_guard_proceeds_when_provenance_missing(project_root, fake_dispatch):
    _write_plan()
    _write_passes()
    # No manifest stamped at all → coverage_passes stage absent → recorded is None.
    # Corrected contract: absent provenance WARNS and proceeds (backward-compat for
    # legacy/un-stamped flows like reroll); the guard refuses only on DRIFT.

    result = generate.run_generation(PROJECT, EPISODE, pass_ids=[PASS_ID])

    # The staleness guard did NOT block on absent provenance.
    assert result.get("error") != "coverage_passes_stale"


def test_staged_candidate_run_is_not_silent_success(project_root, monkeypatch):
    """REC-231: a coverage-refresh / topology-drift run STAGES a not_derived candidate
    and dispatches nothing, so it returns EMPTY scenes — identical on the surface to a
    clean no-op. It must NOT report success=true/shots=0 (which would mask the required
    operator re-board + `rederive --conform`). The CLI reads the runner's staged signal
    and surfaces success=False + staged_candidates + requires_conform.
    """
    plan = _write_plan()
    _write_passes()
    _stamp_coverage_passes(plan_structural_sha(plan))  # guard passes → reaches dispatch

    class StagingRunner:
        def __init__(self, **kwargs):
            self._staged_candidates_without_dispatch = {}

        async def run_episode_batches(self, *args, **kwargs):
            # mimic the non-derive staging branch: candidate appended, nothing dispatched.
            self._staged_candidates_without_dispatch = {"ep_001_BATCH_001": 2}
            return []

    monkeypatch.setattr(generate, "validate_all_passes", lambda passes: [])
    monkeypatch.setattr(generate, "ExecutionStore", lambda *a, **k: object())
    monkeypatch.setattr(generate, "StepRunner", lambda *a, **k: object())
    monkeypatch.setattr(generate, "StrategyEngine", lambda *a, **k: object())
    monkeypatch.setattr(generate, "LearningEngine", lambda *a, **k: object())
    monkeypatch.setattr(generate, "EpisodeRunner", StagingRunner)

    result = generate.run_generation(PROJECT, EPISODE, pass_ids=[PASS_ID])

    assert result["success"] is False
    assert result["status"] == "candidates_staged"
    assert result["requires_conform"] is True
    assert result["staged_candidates"] == {"ep_001_BATCH_001": 2}
    assert result["shots_succeeded"] == 0
    assert result["shots_failed"] == 0


def test_bypass_param_skips_guard(project_root, fake_dispatch, monkeypatch, capsys):
    plan = _write_plan()
    _write_passes()
    _stamp_coverage_passes(STALE_SHA)  # stale provenance for every call below

    # Enforced (default) hard-refuses ...
    enforced = generate.run_generation(PROJECT, EPISODE, pass_ids=[PASS_ID])
    assert enforced["error"] == "coverage_passes_stale"
    assert fake_dispatch["dispatched"] is False

    # ... the direct param bypass proceeds to dispatch ...
    bypassed = generate.run_generation(
        PROJECT, EPISODE, pass_ids=[PASS_ID], enforce_staleness_guard=False
    )
    assert bypassed.get("error") != "coverage_passes_stale"
    assert bypassed["success"] is True
    assert fake_dispatch["dispatched"] is True

    # ... and the CLI --no-staleness-guard flag is the SAME path (parity): the
    # flag sets enforce_staleness_guard=False, so the CLI must also proceed,
    # while the flagless CLI invocation refuses with EXIT_VALIDATION.
    code_refuse, result_refuse = _run_main(monkeypatch, capsys, "--pass", PASS_ID)
    assert code_refuse == generate.EXIT_VALIDATION
    assert result_refuse["error"] == "coverage_passes_stale"

    code_bypass, result_bypass = _run_main(
        monkeypatch, capsys, "--pass", PASS_ID, "--no-staleness-guard"
    )
    assert code_bypass == generate.EXIT_OK
    assert result_bypass.get("error") != "coverage_passes_stale"
    assert result_bypass["success"] is True
