"""도면 렌더 worker 가 스텝 신원을 잃던 것 (감사 A-1 ③).

## 무엇이 결함이었나

`FloorPlanRenderStep` 은 `ThreadPoolExecutor` 로 도면을 병렬 생성하면서
`bind_current_budget` 만 worker 에 넘겼다. 그런데 `call_gpt_image_bytes` 가
쓰는 `ambient_call_meta()` 는 StepRunner 가 심어 둔 **thread-local** 스텝
컨텍스트를 읽고(`image_tracer.py:364-373`), 그것은 worker thread 로
자동 전파되지 않는다.

`ambient_call_meta` 독스트링이 이 한계를 이미 적어 두었다 —
「thread-local 이라 worker thread 에는 자동 전파되지 않는다 … 나중에
ThreadPool 로 나누면 신원이 다시 빈다」. 이 스텝이 정확히 그 자리였다.

실측(DB, 2026-08-28): `step=floor_plan · model=gpt-image-2` **133건**이
`project_id`·`episode_id` **둘 다 NULL**(마지막 2026-08-25). 유료 호출인데
프로젝트 단위로 「이 도면이 살아 있나」를 물을 수 없었다.

## 이 시험이 재는 것

worker 안에서 **실제로 읽히는 값**(`ambient_call_meta()`)을 잰다 —
래퍼가 무엇을 캡처했는지가 아니라 **호출부가 무엇을 보는지**.
"""
from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor

from app.core.steps.floor_plan_render_step import _bind_step_identity
from app.modules.llm.image_tracer import ambient_call_meta
from app.modules.llm.llm_client import _get_thread_opik_meta, set_opik_context

CTX = {"project_id": "P1", "episode_id": "E1", "tags": ["floor_plan_render"]}


def _seen_in_worker() -> dict:
    """worker thread 안에서 기록 코드가 실제로 읽는 신원."""
    return ambient_call_meta()


def _raw_thread_context():
    """감싸지 않은 조회 — 그 thread 에 **남아 있는** 값 그대로."""
    return _get_thread_opik_meta()


def _run(fn) -> dict:
    with ThreadPoolExecutor(max_workers=1) as pool:
        return pool.submit(fn).result()


def test_without_binding_the_worker_sees_no_identity():
    """양성 확인 — 안 감싸면 worker 는 신원을 못 본다(결함 재현)."""
    set_opik_context(CTX)
    try:
        got = _run(_seen_in_worker)
    finally:
        set_opik_context(None)
    assert not got.get("project_id")
    assert not got.get("episode_id")


def test_binding_carries_project_and_episode_into_the_worker():
    set_opik_context(CTX)
    try:
        got = _run(_bind_step_identity(_seen_in_worker))
    finally:
        set_opik_context(None)
    assert got.get("project_id") == "P1"
    assert got.get("episode_id") == "E1"


def test_worker_thread_is_left_clean_for_the_next_task():
    """복원을 **worker 안에서** 잰다 (Codex 리뷰 지적, 비차단).

    앞판은 main thread 의 값을 읽었다 — worker 가 되돌리든 말든 thread-local
    이라 main 은 애초에 안 바뀐다. 그래서 `finally` 를 통째로 지워도 초록이다.
    ★오늘 여러 번 한 실수와 같은 자리: **재려는 곳이 아닌 곳**에서 쟀다.

    같은 executor(`max_workers=1`)에 두 번 제출한다 — 두 작업 모두 같은 한
    worker thread 에서 돈다. ①감싼 호출이 신원을 심었다 되돌리고, ②안 감싼
    raw 조회가 그 thread 에 남은 값을 본다.
    """
    set_opik_context(CTX)
    try:
        with ThreadPoolExecutor(max_workers=1) as pool:
            pool.submit(_bind_step_identity(_seen_in_worker)).result()
            leftover = pool.submit(_raw_thread_context).result()
    finally:
        set_opik_context(None)
    assert not (leftover or {}).get("project_id"), (
        "worker thread 에 신원이 남았다 — 이 pool 의 다음 작업이 남의 계보로 기록된다")


def test_the_caller_thread_is_not_touched():
    """감싸도 호출한 쪽 컨텍스트는 그대로 — worker 밖 동작 불변."""
    set_opik_context(CTX)
    try:
        _run(_bind_step_identity(_seen_in_worker))
        assert _raw_thread_context() == CTX
    finally:
        set_opik_context(None)


def test_no_context_is_a_no_op():
    """캡처할 것이 없으면 기존 동작 그대로 — 비 스텝 경로 무영향."""
    set_opik_context(None)
    sentinel = object()
    assert _bind_step_identity(lambda: sentinel)() is sentinel


def test_the_step_actually_wraps_its_worker():
    """조립부가 실제로 이 래퍼를 쓰는지 — 함수만 만들고 안 부르면 그만이다.

    ★오늘 같은 실수를 여러 번 했다: 만들어 놓고 호출부에 안 붙이면
     시험은 초록인데 프로덕션은 그대로다.
    """
    import ast
    import pathlib

    src = pathlib.Path(
        __file__).resolve().parents[2] / "app/core/steps/floor_plan_render_step.py"
    tree = ast.parse(src.read_text(encoding="utf-8"))
    called = {
        n.func.id for n in ast.walk(tree)
        if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
    }
    assert "_bind_step_identity" in called, (
        "래퍼를 만들었는데 worker 제출부에서 안 부른다")
