"""주행을 세우는 코드 목록은 **한 곳**이다. ★유료 0.

같은 세 코드(`step.cancelled`·`step.owner_lost`·`step.gate_unreadable`)를
다섯 자리가 **각자 적어** 뒀다 — 그런 것은 한쪽만 고쳐진다.

★코드 **하나만** 보는 자리는 안 옮긴다 — 뜻이 다르다. 예를 들어
`step_runner` 의 `owner_lost` 단독 검사는 「남의 것이 된 행에 쓰지 않는다」는
다른 규칙이다.
"""
from __future__ import annotations

import ast
import os

import pytest

CODES = {"step.cancelled", "step.owner_lost", "step.gate_unreadable"}
MOVED = (
    "app/core/steps/detail_steps.py",
    "app/modules/pipeline/cine_transform.py",
    "app/services/scene_generation_coordinator.py",
    "app/services/scene_image_service.py",
)


def _inline_spots(path: str) -> list:
    """이 파일이 **세 코드를 한 벌로** 인라인한 자리."""
    try:
        tree = ast.parse(open(path, encoding="utf-8").read())
    except SyntaxError:
        return []
    out = []
    for n in ast.walk(tree):
        if not isinstance(n, ast.Compare):
            continue
        vals = set()
        for c in n.comparators:
            for e in getattr(c, "elts", []) or []:
                if isinstance(e, ast.Constant):
                    vals.add(e.value)
        if vals == CODES:
            out.append(n.lineno)
    return out


class TestTheListLivesInOnePlace:
    def test_no_module_inlines_the_whole_list(self):
        """★★전수로 센다 — 몇 곳만 고치면 나머지가 갈린다."""
        bad = []
        for root, _dirs, files in os.walk("app"):
            for fn in files:
                if not fn.endswith(".py"):
                    continue
                p = os.path.join(root, fn)
                if p.endswith("run_control.py"):
                    continue            # ★정의한 자리
                for ln in _inline_spots(p):
                    bad.append(f"{p}:{ln}")
        assert bad == [], f"★목록을 각자 적은 자리: {bad}"

    @pytest.mark.parametrize("path", MOVED)
    def test_the_moved_ones_call_the_predicate(self, path):
        src = open(path, encoding="utf-8").read()
        assert "is_abort(exc)" in src, f"★{path} 가 공용 술어를 안 쓴다"
        assert "run_control import" in src and "is_abort" in src

    def test_the_predicate_is_public(self):
        import app.core.run_control as rc

        assert "is_abort" in rc.__all__ and "ABORT_CODES" in rc.__all__

    def test_single_code_checks_are_left_alone(self):
        """★★코드 **하나만** 보는 자리는 뜻이 다르다 — 안 옮긴다.

        `step_runner` 의 `owner_lost` 단독 검사는 「이미 남의 것이 된 행에
        쓰지 않는다」는 다른 규칙이다. 그것까지 합치면 취소가 아닌 것을
        취소처럼 다루게 된다.
        """
        src = open("app/core/step_runner.py", encoding="utf-8").read()
        assert "step.owner_lost" in src, "★단독 검사가 사라졌다"
        assert _inline_spots("app/core/step_runner.py") == []


class TestThePredicateItself:
    def test_it_matches_the_three_and_nothing_else(self):
        from app.core.errors import AppError
        from app.core.run_control import ABORT_CODES, is_abort

        for code in ABORT_CODES:
            assert is_abort(AppError(code=code, message="x", status_code=409))
        for code in ("provider.timeout", "step.failed", ""):
            assert not is_abort(
                AppError(code=code, message="x", status_code=500))

    def test_a_plain_exception_is_not_an_abort(self):
        """★positive control — 넓히다가 일반 실패까지 세우면 안 된다."""
        from app.core.run_control import is_abort

        assert not is_abort(RuntimeError("그냥 실패"))
