"""form_reference producer 의 **소급 변조 금지** 소스 잠금 (A4).

## 왜 소스 잠금인가

A4 의 결함은 "한 번 잘못 쓴 코드"가 아니라 **패턴**이었다 — 그룹당 한 row 를
찾아 `existing.file_path = rel_path` 로 갱신하는 upsert. 그 한 줄이 씨드의
입력 간선을 소급해서 다른 이미지로 바꿨다. 규칙으로만 두면 다시 생긴다
(A5 에서 같은 종류의 우회가 Router 경로에서만 네 번 생겼다).

잠금은 **AST 로** 건다. 글자로 훑으면 docstring·주석·다중행에서 새거나
잘못 잡는다(A5 실측).

## 범위

이 잠금은 `outdoor_structure_form_reference_step.py` **하나**에만 건다.
`outdoor_structure_seed_step.py` 의 `structure_seed` 자산도 같은 upsert
패턴을 갖고 있으나(실측: 그 파일의 `existing.file_path = rel_path`) A4 범위
밖이라 아직 고치지 않았다 — 여기 넣으면 통과할 수 없는 게이트가 된다.
그 자산의 이행은 별건으로 남는다.
"""
from __future__ import annotations

import ast
from pathlib import Path

import pytest

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

# A4 이전의 실제 코드 — 이 잠금이 무엇을 잡는지 보여주는 표본이자,
# 잠금 자체의 실효성 확인용이다.
_VIOLATION_SAMPLE = """
def upsert(db, group_id, rel_path):
    existing = db.query(X).filter_by(entity_id=group_id).first()
    if existing:
        existing.file_path = rel_path
        return existing.id
"""

# 잠금이 **과하게** 잡지 않는지 확인하는 표본 — 신규 row 생성은 정상이다.
_CLEAN_SAMPLE = """
def insert(db, asset_id, abs_path):
    row = X(id=asset_id, file_path=abs_path)
    db.add(row)
    return asset_id
"""


def _path_assign_lines(source: str) -> list[int]:
    """이미 존재하는 객체의 ``.file_path`` 를 갈아치우는 구문의 줄 번호."""
    tree = ast.parse(source)
    hits: list[int] = []
    for node in ast.walk(tree):
        targets: list[ast.expr] = []
        if isinstance(node, ast.Assign):
            targets = list(node.targets)
        elif isinstance(node, (ast.AugAssign, ast.AnnAssign)):
            targets = [node.target]
        for t in targets:
            if isinstance(t, ast.Attribute) and t.attr == "file_path":
                hits.append(t.lineno)
    return sorted(hits)


def test_the_lock_catches_the_pattern_it_is_meant_to_catch():
    """★잠금의 실효성 — A4 이전 코드를 넣으면 잡혀야 한다."""
    assert _path_assign_lines(_VIOLATION_SAMPLE), "잠금이 옛 upsert 를 못 잡는다"


def test_the_lock_does_not_flag_creating_a_new_row():
    """신규 row 생성(생성자 인자)은 소급 변조가 아니다 — 잡으면 안 된다."""
    assert _path_assign_lines(_CLEAN_SAMPLE) == []


def test_producer_never_rewrites_an_existing_assets_path():
    """★기존 자산의 `file_path` 를 갈아치우는 구문이 있으면 안 된다.

    라운드마다 새 UUID 로 **insert-or-verify** 하고, 어긋나면 고치지 않고
    fail-closed 한다. 갱신하는 순간 과거 씨드의 입력 간선이 거짓이 된다.
    """
    hits = _path_assign_lines(TARGET.read_text(encoding="utf-8"))
    assert hits == [], (
        f"{TARGET.name}:{hits} 에서 기존 자산의 file_path 를 갱신한다 — "
        "계보 소급 변조 경로다")


def test_producer_does_not_mint_its_own_asset_uuid():
    """자산 UUID 는 **라운드 OPENED 에서 미리 채번**한 것만 쓴다.

    스텝이 제 손으로 `uuid4()` 를 부르면, crash resume 이 같은 라운드에 또
    다른 UUID 를 INSERT 해 라운드-자산 짝이 깨진다.
    """
    tree = ast.parse(TARGET.read_text(encoding="utf-8"))
    calls = [
        n.lineno for n in ast.walk(tree)
        if isinstance(n, ast.Call) and (
            (isinstance(n.func, ast.Attribute) and n.func.attr.startswith("uuid"))
            or (isinstance(n.func, ast.Name) and n.func.id.startswith("uuid")))
    ]
    assert calls == [], f"{TARGET.name}:{calls} 에서 UUID 를 직접 만든다"


if __name__ == "__main__":
    raise SystemExit(pytest.main([__file__, "-q"]))
