"""`router_completion` 을 직접 부르는 자리도 축 태그를 실어야 한다.

★표준 조립부(`_build_opik_metadata`)는 `call_structured` 계열 **셋**만 탄다.
`router_completion` 을 직접 부르는 넷은 그것을 건너뛰어, 나가는 태그가
맨 이름 하나뿐이었다 — 그러면 감사 도구에서 그 호출은 「어느 스텝인지
모름」으로 남는다.

2026-08-25 실측(최소 검증판 주행): span 26/252 가 스텝 축을 잃었고
그중 14 건이 `ref_validation` 이었다.

여기서 재는 것은 **나가는 payload** 다 — `router_completion` 에 실제로
넘어간 kwargs 를 가로채서 본다.
"""
import pytest

from app.modules.llm.opik_trace import is_axis_tag


@pytest.fixture
def captured(monkeypatch):
    """`router_completion` 을 가로채 나가는 kwargs 를 모은다."""
    calls = []

    class _Msg:
        content = '{"ok": true}'

    class _Choice:
        message = _Msg()

    class _Resp:
        choices = [_Choice()]

    def _fake(*, model, **kwargs):
        calls.append({"model": model, **kwargs})
        return _Resp()

    import app.modules.llm.llm_client as lc
    monkeypatch.setattr(lc, "router_completion", _fake)
    return calls


def _axis_of(md):
    return [t for t in ((md or {}).get("opik", {}).get("tags") or [])
            if is_axis_tag(t)]


def test_ref_lvm_call_carries_axis_tags(monkeypatch, captured):
    """참조 검증(LVM)이 축 태그를 싣는다 — 실측 14건이 이 자리였다."""
    from app.core import config
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", True)

    from app.modules.pipeline import ref_image_pipeline as rip
    rip._call_gpt_lvm(
        image_bytes=b"\x89PNG",
        text_prompt="p",
        response_schema={"type": "object"},
        schema_name="ref_validation",
        opik_tags=["ref_validation"],
    )

    assert captured, "router_completion 이 안 불렸다"
    md = captured[0].get("metadata")
    axis = _axis_of(md)
    assert axis, f"축 태그가 하나도 없다: {md!r}"
    assert any(t.startswith("op:") or t.startswith("step:") for t in axis), \
        f"스텝/호출단계 축이 없다: {axis!r}"


def test_ref_lvm_attaches_to_open_parent(monkeypatch, captured):
    """부모 trace 가 열려 있으면 span 으로 붙는다 — `chat.completion` 이 사라진다."""
    from app.core import config
    from app.modules.llm import opik_trace
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", True)

    from app.modules.pipeline import ref_image_pipeline as rip
    handle = opik_trace.TraceHandle(uid="0190-parent", name="step:ref_image_gen")
    token = opik_trace.bind_trace(handle)
    try:
        rip._call_gpt_lvm(
            image_bytes=b"\x89PNG", text_prompt="p",
            response_schema={"type": "object"},
            schema_name="ref_validation", opik_tags=["ref_validation"],
        )
    finally:
        opik_trace.reset_trace(token)

    md = captured[0].get("metadata") or {}
    csd = md.get("opik", {}).get("current_span_data")
    assert csd and csd.get("trace_id") == "0190-parent", \
        f"부모에 안 붙었다: {md!r}"


def test_text_cleaner_carries_axis_tags():
    """대본 정리도 기록에서 이름을 가져야 한다 — 전에는 metadata 자체가 없었다.

    ★2026-08-28: 종전 판은 `router_completion(` 뒤 **600자 창** 안에서
     `metadata=` 를 문자열로 찾았다. 그 자리에 주석 몇 줄만 넣어도 `metadata=`
     가 창 밖으로 밀려 **계약은 그대로인데 빨강**이 된다(실제로 그렇게 깨졌다).
     창 대신 AST 로 그 호출의 키워드를 본다 — 주석·줄바꿈·인자 순서에 안 흔들리고
     호출이 여러 개여도 전부 검사한다.

    ★나가는 값 자체는 `test_record_taxonomy_a1.py::
     test_pdf_extract_lineage_rides_the_thread_local` 이 끝점에서 잰다.
     여기서는 **호출부가 인자를 붙였는가**만 본다 — 둘은 다른 갈래다.
    """
    import ast
    import pathlib

    import app.modules.pipeline.text_cleaner as tc

    tree = ast.parse(pathlib.Path(tc.__file__).read_text(encoding="utf-8"))
    calls = [n for n in ast.walk(tree)
             if isinstance(n, ast.Call)
             and getattr(n.func, "id", None) == "router_completion"]
    assert calls, "text_cleaner 가 router_completion 을 안 부른다"
    for call in calls:
        kws = {k.arg for k in call.keywords}
        assert "metadata" in kws, (
            f"text_cleaner:{call.lineno} 의 router_completion 호출에 "
            f"metadata 가 없다 — 붙은 것: {sorted(k for k in kws if k)}")
