"""W6c — start_step single-step resume preflight 정합화 regression.

scene_detail 처럼 step-local ``_config_hash()`` 가 ``project_config`` 와 다른
hash 기준을 가질 때, start_step 의 manual fast-preflight 가
``compute_config_hash(project_config)`` 와 ``cp.config_hash`` 를 직접 비교하면
실제로는 stale 이 아닌 cp 가 ``step.resume_invalid`` (HTTP 409) 로 거부된다.

W6c patch: ``existing.status == 'completed'`` 일 때 단일 step resume preflight 를
``StepRunner._evaluate_resume_decision('resume')`` 로 위임한다. ``StepRunner``
가 이미 step-local ``_config_hash()`` 와 ``_check_cp_mismatch`` 를 통합한
contract 를 가지고 있다.

본 regression 은 다음 3개 결정 매핑이 endpoint 단에서 유지되는지 검증한다:
  - ``ResumeAction.SKIP`` → endpoint 가 ``status='skipped'`` 반환.
  - ``ResumeAction.BLOCK`` → endpoint 가 ``AppError(code='step.resume_invalid',
    status_code=409)`` raise.
  - ``ResumeAction.RERUN_SELF`` → endpoint 가 ``submit_background_job`` 호출 후
    ``status='started'`` 반환 (false skip 방지).

추가:
  - step-local hash mismatch 시나리오 (W4c 재현) — runner._check_cp_mismatch
    가 None 을 반환하면 cp_hash != compute_config_hash(project_config) 라도
    ``_evaluate_resume_decision`` 이 SKIP 을 반환하고 endpoint 가 409 아닌
    skipped 응답을 보낸다.
"""
from __future__ import annotations

from typing import Any, Dict, Optional

import pytest

from app.core.errors import AppError
from app.core.step_runner import ResumeAction, ResumeDecision, StepRunner


# ---- runner stub helpers (test_resume_decision.py 패턴 재사용) ----


def _make_runner_stub(
    *,
    step_id: str = "scene_detail",
    existing: Optional[dict] = None,
    decision: Optional[ResumeDecision] = None,
    project_config: Optional[dict] = None,
):
    """check_gate / _get_step_run / _evaluate_resume_decision 만 stub."""
    runner = StepRunner.__new__(StepRunner)
    runner.step_id = step_id
    runner.project_id = "p1"
    runner.episode_id = "e1"
    runner.project_config = project_config or {}
    runner.check_gate = lambda: None
    runner._get_step_run = lambda sid: existing
    runner._evaluate_resume_decision = lambda mode="resume": decision  # noqa: E731
    return runner


def _completed_existing() -> dict:
    return {
        "status": "completed",
        "run_id": "r-prev",
        "started_at": "2026-05-23T07:00:00",
        "completed_count": 61,
        "applicable_count": 61,
        "failed_count": 0,
    }


@pytest.fixture
def patch_start_step_deps(monkeypatch):
    """start_step 의 비-resume 외부 deps 를 모두 no-op / stub 처리.

    - _load_project_config: 고정 dict.
    - _ensure_not_running_as_category: no-op.
    - get_step_runner: pre-built stub 반환.
    - get_manifest_dict: 기본 step_meta.
    - submit_background_job: spy (호출 여부 + args record).

    returns the SimpleNamespace-like dict for assertions.
    """
    from types import SimpleNamespace

    state = SimpleNamespace(runner=None, submit_calls=[], submit_return=True)

    def _set_runner(r):
        state.runner = r

    def _patch(monkeypatch):
        import app.services.step_execution_service as svc

        monkeypatch.setattr(svc, "_load_project_config", lambda db, pid: {"k": "v"})
        monkeypatch.setattr(svc, "_ensure_not_running_as_category", lambda *a, **kw: None)
        monkeypatch.setattr(svc, "_step_contains", lambda sid: True)
        monkeypatch.setattr(svc, "get_manifest_dict", lambda sid: {"schema_version": 13})

        from app.services import analysis_dispatch_service as ads
        monkeypatch.setattr(
            ads, "get_step_runner",
            lambda *a, **kw: state.runner,
        )

        from app.core import job_manager
        def _submit(**kwargs):
            state.submit_calls.append(kwargs)
            return state.submit_return
        monkeypatch.setattr(job_manager, "submit_background_job", _submit)

    _patch(monkeypatch)
    state.set_runner = _set_runner
    return state


# ---- W6c-1 regression tests ----


def test_completed_skip_returns_skipped(patch_start_step_deps):
    """SKIP decision → endpoint 가 status=skipped 반환, background submit 호출 X.

    이게 wave W4c 의 핵심 재현 케이스: scene_detail step-local _config_hash
    (예: 187e36a52f3faa6a) 와 project_config hash (99914b932bd37a50) 가
    다르지만 cp_hash == step-local 이므로 StepRunner 가 SKIP 판정.
    """
    runner = _make_runner_stub(
        existing=_completed_existing(),
        decision=ResumeDecision(
            action=ResumeAction.SKIP,
            reason="cp clean, verify passed",
        ),
    )
    patch_start_step_deps.set_runner(runner)

    from app.services.step_execution_service import start_step
    result = start_step(
        db=None, project_id="p1", episode_id="e1", step_id="scene_detail",
        mode="resume", actor_id="u1", opik_context={},
    )
    assert result["status"] == "skipped"
    assert result["ok"] is True
    assert patch_start_step_deps.submit_calls == []


def test_completed_block_raises_resume_invalid_409(patch_start_step_deps):
    """BLOCK decision → AppError(code='step.resume_invalid', status_code=409)."""
    runner = _make_runner_stub(
        existing=_completed_existing(),
        decision=ResumeDecision(
            action=ResumeAction.BLOCK,
            reason="schema_version mismatch: 체크포인트=11, 현재=13",
            origin="contract_drift",
        ),
    )
    patch_start_step_deps.set_runner(runner)

    from app.services.step_execution_service import start_step
    with pytest.raises(AppError) as ei:
        start_step(
            db=None, project_id="p1", episode_id="e1", step_id="scene_detail",
            mode="resume", actor_id="u1", opik_context={},
        )
    err = ei.value
    assert err.code == "step.resume_invalid"
    assert err.status_code == 409
    assert "schema_version mismatch" in (err.message or "")
    assert patch_start_step_deps.submit_calls == []


def test_completed_rerun_self_falls_through_to_background_submit(patch_start_step_deps):
    """RERUN_SELF (artifact_missing) decision → submit_background_job 호출.

    false skip 금지: cp 없거나 verify fail 케이스에서 endpoint 가
    status='skipped' 로 돌려보내면 안 된다.
    """
    runner = _make_runner_stub(
        existing=_completed_existing(),
        decision=ResumeDecision(
            action=ResumeAction.RERUN_SELF,
            reason="checkpoint missing but step_run.status=completed",
            origin="artifact_missing",
        ),
    )
    patch_start_step_deps.set_runner(runner)

    from app.services.step_execution_service import start_step
    result = start_step(
        db=None, project_id="p1", episode_id="e1", step_id="scene_detail",
        mode="resume", actor_id="u1", opik_context={},
    )
    assert result["status"] == "started"
    assert result["ok"] is True
    assert len(patch_start_step_deps.submit_calls) == 1


def test_non_completed_status_falls_through(patch_start_step_deps):
    """status != completed (예: partial) → 기존처럼 background submit 으로 빠진다.

    W6c patch 가 completed 분기만 변경하므로 다른 상태는 영향 없어야 한다.
    """
    existing = dict(_completed_existing())
    existing["status"] = "partial"
    runner = _make_runner_stub(
        existing=existing,
        decision=ResumeDecision(
            action=ResumeAction.RERUN_SELF,
            reason="status=partial",
            origin="prior_state",
        ),
    )
    patch_start_step_deps.set_runner(runner)

    from app.services.step_execution_service import start_step
    result = start_step(
        db=None, project_id="p1", episode_id="e1", step_id="scene_detail",
        mode="resume", actor_id="u1", opik_context={},
    )
    assert result["status"] == "started"
    assert len(patch_start_step_deps.submit_calls) == 1


def test_no_existing_step_run_falls_through(patch_start_step_deps):
    """existing == None (first-run) → completed 분기 자체 미진입, submit."""
    runner = _make_runner_stub(existing=None, decision=None)
    patch_start_step_deps.set_runner(runner)

    from app.services.step_execution_service import start_step
    result = start_step(
        db=None, project_id="p1", episode_id="e1", step_id="scene_detail",
        mode="resume", actor_id="u1", opik_context={},
    )
    assert result["status"] == "started"


def test_step_local_hash_differs_from_project_config_hash_still_skips(patch_start_step_deps):
    """W4c 재현 시나리오 정확히 — step-local config_hash != project_config_hash
    이지만 _check_cp_mismatch 가 None 을 반환하는 경우, _evaluate_resume_decision
    이 SKIP 을 반환하고 endpoint 가 409 가 아니라 skipped 응답.

    StepRunner.check_cp_mismatch contract 는 step-local _config_hash 를
    우선 사용 (step_runner.py:1496-1541). 본 테스트는 endpoint 가 그
    contract 를 그대로 따른다는 invariant.
    """
    # cp_hash 는 step-local hash 와 일치 (mismatch 없음).
    # project_config_hash 는 다르지만 StepRunner contract 가 그걸 무시함.
    runner = _make_runner_stub(
        existing=_completed_existing(),
        decision=ResumeDecision(
            action=ResumeAction.SKIP,
            reason="cp clean, verify passed",
        ),
        project_config={"k": "v"},  # arbitrary, hash 가 cp_hash 와 달라도 무관
    )
    patch_start_step_deps.set_runner(runner)

    from app.services.step_execution_service import start_step
    result = start_step(
        db=None, project_id="p1", episode_id="e1", step_id="scene_detail",
        mode="resume", actor_id="u1", opik_context={},
    )
    assert result["status"] == "skipped", (
        "step-local _config_hash 와 일치하는 cp 는 project_config_hash 와 "
        "달라도 false 409 가 되어선 안 된다 (W4c 재현 케이스)."
    )
