"""W20F10 R1 — CompositeImageGenStep 이 service 호출 전에 sync 를 발화하는지 정적 검증.

배경:
    `CompositeImageGenStep._execute` 는 과거 `_sync_analysis_to_db()` 를 호출하지
    않은 채 곧장 ``ReferenceImageService.generate_composites()`` 로 들어갔다.
    composite step 단독 force/resume 또는 ref step 을 거치지 않는 흐름에서는
    ``ReferencePipelineOrchestrator.run`` 의 ``episode.status != 'analyzed'``
    guard 가 stale projection 으로 막혔다. R1 fix 는 RefImageGenStep 과 동일
    패턴으로 진입부에 ``self._sync_analysis_to_db()`` 를 추가한다.

본 test 는 runtime 호출 그래프 대신 AST 정적 분석으로 가벼운 회귀 가드만 둔다:
fixture 무거움을 피하고, sync 호출 누락 / service 호출 앞서 발화하는 회귀를
1 PASS 로 감지한다 (codex 가 무거운 fixture 회피를 명시).
"""
from __future__ import annotations

import ast
from pathlib import Path


_IMAGE_STEPS_PATH = (
    Path(__file__).resolve().parents[2]
    / "app" / "core" / "steps" / "image_steps.py"
)


def _find_class_function(tree: ast.AST, class_name: str, func_name: str) -> ast.FunctionDef:
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef) and node.name == class_name:
            for sub in node.body:
                if isinstance(sub, ast.FunctionDef) and sub.name == func_name:
                    return sub
    raise AssertionError(f"{class_name}.{func_name} not found")


def _is_self_method_call(node: ast.AST, method_name: str) -> bool:
    return (
        isinstance(node, ast.Call)
        and isinstance(node.func, ast.Attribute)
        and isinstance(node.func.value, ast.Name)
        and node.func.value.id == "self"
        and node.func.attr == method_name
    )


def _is_attr_method_call(node: ast.AST, method_name: str) -> bool:
    """`<anything>.<method_name>(...)` 호출 매칭 (eg. ``svc.generate_composites(...)``)."""
    return (
        isinstance(node, ast.Call)
        and isinstance(node.func, ast.Attribute)
        and node.func.attr == method_name
    )


def _first_call_line(func: ast.FunctionDef, predicate) -> int | None:
    for sub in ast.walk(func):
        if predicate(sub):
            return getattr(sub, "lineno", None)
    return None


def test_composite_image_gen_step_syncs_before_service_call():
    tree = ast.parse(_IMAGE_STEPS_PATH.read_text(encoding="utf-8"))
    execute = _find_class_function(tree, "CompositeImageGenStep", "_execute")

    sync_line = _first_call_line(
        execute, lambda n: _is_self_method_call(n, "_sync_analysis_to_db"),
    )
    assert sync_line is not None, (
        "CompositeImageGenStep._execute must call self._sync_analysis_to_db() "
        "(W20F10 R1 stale projection guard)."
    )

    svc_line = _first_call_line(
        execute, lambda n: _is_attr_method_call(n, "generate_composites"),
    )
    assert svc_line is not None, (
        "CompositeImageGenStep._execute must call generate_composites() "
        "via the reference image service."
    )

    assert sync_line < svc_line, (
        f"_sync_analysis_to_db (line {sync_line}) must precede "
        f"generate_composites (line {svc_line}) — stale projection guard."
    )


def test_ref_image_gen_step_still_syncs_before_service_call():
    """W20F10 R1 회귀 가드 — RefImageGenStep 의 동일 패턴이 의도치 않게 사라지지 않는지."""
    tree = ast.parse(_IMAGE_STEPS_PATH.read_text(encoding="utf-8"))
    execute = _find_class_function(tree, "RefImageGenStep", "_execute")

    sync_line = _first_call_line(
        execute, lambda n: _is_self_method_call(n, "_sync_analysis_to_db"),
    )
    svc_line = _first_call_line(
        execute, lambda n: _is_attr_method_call(n, "generate_base_references"),
    )
    assert sync_line is not None
    assert svc_line is not None
    assert sync_line < svc_line
