"""era 참조 역할을 `background_classify` 에서 읽는 논리 (감사 2-C).

★역할은 **코드가 정한다.** 모델에게 종류를 고르게 하면 enum 검사는
 문자열 범위만 볼 뿐 뜻은 자가신고라, 그 값을 캐시 신원으로 믿으면
 캐시 미스보다 나쁜 **다른 장소 참조 재사용**이 난다.

★행이 없거나 값이 어긋나면 **추측하지 않는다** — `None` 을 주면 era
 모듈이 옛 자유 키로 떨어지고 사유를 기록에 남긴다.

이 시험은 `still_recipe_service` 안의 조립을 **AST 로 떼어** 태운다.
그 함수는 db·CP·records 를 다 요구해서 통째로는 못 부르는데, 조립을
안 재면 「충돌이면 안 합친다」가 한 번도 안 걸린다.
"""
from __future__ import annotations

import ast
import inspect
import textwrap
from typing import Any, Dict, List, Optional

from app.modules.pipeline.era_research import (
    SCOPE_ROLE_EXTERIOR,
    SCOPE_ROLE_INTERIOR,
)


def _role_table(members: List[Dict[str, Any]]) -> Dict[str, Optional[str]]:
    """프로덕션과 **같은 조립** — 아래 AST 시험이 그것을 잠근다."""
    role_of: Dict[str, Optional[str]] = {}
    for m in members:
        lid = str(m.get("loc_id") or "").strip()
        if not lid:
            continue
        ind = m.get("is_indoor")
        r = (SCOPE_ROLE_INTERIOR if ind is True
             else SCOPE_ROLE_EXTERIOR if ind is False else None)
        if lid in role_of and role_of[lid] != r:
            role_of[lid] = None
        else:
            role_of.setdefault(lid, r)
    return role_of


def test_one_row_decides_the_role():
    assert _role_table([{"loc_id": "L01", "is_indoor": True}])["L01"] \
        == SCOPE_ROLE_INTERIOR
    assert _role_table([{"loc_id": "L01", "is_indoor": False}])["L01"] \
        == SCOPE_ROLE_EXTERIOR


def test_a_conflict_decides_nothing():
    """★같은 loc 에 안/밖이 엇갈리면 **고르지 않는다.**"""
    assert _role_table([
        {"loc_id": "L01", "is_indoor": True},
        {"loc_id": "L01", "is_indoor": False},
    ])["L01"] is None


def test_a_missing_flag_decides_nothing():
    assert _role_table([{"loc_id": "L01"}])["L01"] is None
    # 결손이 먼저 와도 뒤 행이 그것을 덮지 않는다 — 어긋난 것은 어긋난 것이다.
    assert _role_table([
        {"loc_id": "L01"},
        {"loc_id": "L01", "is_indoor": True},
    ])["L01"] is None


def test_the_same_value_twice_is_not_a_conflict():
    assert _role_table([
        {"loc_id": "L01", "is_indoor": True},
        {"loc_id": "L01", "is_indoor": True},
    ])["L01"] == SCOPE_ROLE_INTERIOR


def test_production_reads_is_indoor_and_never_guesses():
    """★**프로덕션이 같은 것을 하는지** 본다 — 위 표는 사본일 뿐이다.

    글자가 아니라 AST 로 센다: 조립부에 `is_indoor` 읽기가 있고,
    충돌 갈래에서 `None` 을 넣는 대입이 있어야 한다.
    """
    from app.services import still_recipe_service as srs

    src = textwrap.dedent(inspect.getsource(srs))
    tree = ast.parse(src)

    reads_is_indoor = any(
        isinstance(n, ast.Constant) and n.value == "is_indoor"
        for n in ast.walk(tree))
    assert reads_is_indoor, "프로덕션이 is_indoor 를 안 읽는다"

    # 충돌이면 None — `_loc_role[...] = None` 대입이 있어야 한다.
    assigns_none = any(
        isinstance(n, ast.Assign)
        and isinstance(n.value, ast.Constant) and n.value.value is None
        and any(isinstance(t, ast.Subscript)
                and isinstance(t.value, ast.Name)
                and t.value.id == "_loc_role" for t in n.targets)
        for n in ast.walk(tree))
    assert assigns_none, "충돌일 때 역할을 비우는 자리가 없다"

    # 그리고 그 역할을 **넘기는** 자리가 세 곳이어야 한다.
    passed = sum(
        1 for n in ast.walk(tree)
        if isinstance(n, ast.keyword) and n.arg == "canonical_scope_role")
    assert passed == 3, f"역할을 넘기는 자리가 {passed}곳이다 (기대 3)"


def test_all_three_era_call_sites_pass_the_full_identity():
    """★셋을 다 넘기는지 — 하나만 빠져도 그 자리는 영영 fallback 이다."""
    from app.services import still_recipe_service as srs

    tree = ast.parse(textwrap.dedent(inspect.getsource(srs)))
    for kw in ("canonical_scope_id", "canonical_scope_role",
               "canonical_scope_sha"):
        n = sum(1 for x in ast.walk(tree)
                if isinstance(x, ast.keyword) and x.arg == kw)
        assert n == 3, f"{kw} 를 넘기는 자리가 {n}곳이다 (기대 3)"


def test_groupbg_identity_follows_the_origin_shot_not_the_follower():
    """★공유 배경은 **origin 샷**의 장소로 만든다 — 신원도 그래야 한다.

    `_run_groupbg` 는 `canonical["place_text"]` 로 배경을 만든다. 신원만
    follower 샷의 씬을 읽으면, 공유 그룹이 장소를 넘나들 때 **배경은
    origin 것인데 참조는 다른 장소 것**이 된다 — 이 판이 막겠다고 한
    바로 그 오병합이다 (2026-08-27 Codex).
    """
    from app.services import still_recipe_service as srs

    tree = ast.parse(textwrap.dedent(inspect.getsource(srs)))
    # `_era_scope(_si_by_tag.get(<X>))` 의 <X> 를 모아 본다.
    args = []
    for n in ast.walk(tree):
        if (isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
                and n.func.id == "_era_scope" and n.args):
            a = n.args[0]
            if (isinstance(a, ast.Call) and isinstance(a.func, ast.Attribute)
                    and a.func.attr == "get"
                    and isinstance(a.func.value, ast.Name)
                    and a.func.value.id == "_si_by_tag" and a.args):
                inner = a.args[0]
                args.append(inner.id if isinstance(inner, ast.Name) else "?")
    assert "origin_tag" in args, (
        f"groupbg 신원이 origin 샷을 안 본다 — 넘긴 것: {args}")
