"""직접 호출(record_provider_call)도 부모 trace 밑 span 이 된다."""
import pytest


class _FakeSpan:
    def __init__(self, calls, **kw):
        self.calls = calls
        calls.append(("span", kw))

    def end(self, **kw):
        self.calls.append(("span_end", kw))


class _FakeTrace:
    def __init__(self, calls, **kw):
        self.calls = calls
        calls.append(("trace", kw))

    def span(self, **kw):
        return _FakeSpan(self.calls, **kw)

    def end(self, **kw):
        self.calls.append(("trace_end", kw))


@pytest.fixture
def tracer(monkeypatch):
    calls = []

    class _Client:
        def trace(self, **kw):
            return _FakeTrace(calls, **kw)

        def span(self, **kw):
            return _FakeSpan(calls, **kw)

    from app.modules.llm.image_tracer import ImageTracer
    t = ImageTracer.__new__(ImageTracer)
    t._client = _Client()
    t._explicit_context = None
    return t, calls


def test_v1_makes_its_own_trace(monkeypatch, tracer):
    from app.core import config
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", False)
    t, calls = tracer
    t.log(step="still_recipe_roll", model="x-ai/grok-imagine-image-2.0",
          prompt="p", duration_ms=120)
    kinds = [k for k, _ in calls]
    assert kinds[0] == "trace"
    assert calls[0][1]["name"] == "still_recipe_roll/x-ai/grok-imagine-image-2.0"


def test_v2_attaches_span_to_open_parent(monkeypatch, tracer):
    from app.core import config
    from app.modules.llm import opik_trace
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", True)
    t, calls = tracer

    handle = opik_trace.TraceHandle(uid="0190-parent", name="still:S1sh3",
                                    thread_id="ep_x")
    token = opik_trace.bind_trace(handle)
    try:
        t.log(step="still_recipe_roll", model="x-ai/grok-imagine-image-2.0",
              prompt="p", duration_ms=120, provider="openrouter",
              params={"role": "still_recipe_roll"})
    finally:
        opik_trace.reset_trace(token)

    kinds = [k for k, _ in calls]
    assert "trace" not in kinds, "부모가 있는데 trace 를 또 만들었다"
    span_kw = dict(calls[0][1])
    assert span_kw["trace_id"] == "0190-parent"
    assert span_kw["name"] == "still_recipe_roll · x-ai/grok-imagine-image-2.0"


def test_v2_without_parent_still_records(monkeypatch, tracer):
    """부모가 없어도 기록은 남아야 한다 — 기록이 사라지는 것이 최악이다."""
    from app.core import config
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", True)
    t, calls = tracer
    t.log(step="pdf_validation", model="gpt-5.6-sol", prompt="p",
          duration_ms=10)
    assert [k for k, _ in calls][0] == "trace"


def test_v2_fallback_trace_keeps_thread_id(monkeypatch, tracer):
    """★부모 없이 홀로 서는 trace 도 주행 묶음 안에 있어야 한다.

    v2 에서 StepRunner 는 `session_id` 를 지우고 `thread_id` 를 넣는데
    (step_runner.build_opik_metadata), image step 은 그 dict 를 그대로
    ImageTracer 의 명시 context 로 넘긴다(image_steps._setup_image_tracer_context).
    `session_id` 만 읽으면 그 trace 는 thread 가 None 이 되어 에피소드
    묶음 밖으로 떨어진다 (2026-08-24 Codex BLOCK 3).
    """
    from app.core import config
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", True)
    t, calls = tracer
    t._explicit_context = {"thread_id": "마지막 임무_1회_f5372927",
                           "tags": ["step:scene_image_pipeline"]}
    t.log(step="still_recipe", model="probe", prompt="p", duration_ms=1)

    kind, kw = calls[0]
    assert kind == "trace"
    assert kw["thread_id"] == "마지막 임무_1회_f5372927"


def test_v1_still_reads_session_id(monkeypatch, tracer):
    """설정 OFF 는 지금 그대로 — session_id 를 읽는다(바이트 동일)."""
    from app.core import config
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", False)
    t, calls = tracer
    t._explicit_context = {"session_id": "RT-1", "tags": ["scene_detail"]}
    t.log(step="still_recipe", model="probe", prompt="p", duration_ms=1)
    assert calls[0][1]["thread_id"] == "RT-1"


def test_v2_tags_are_all_prefixed(monkeypatch, tracer):
    from app.core import config
    from app.modules.llm import opik_trace
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", True)
    t, calls = tracer
    handle = opik_trace.TraceHandle(uid="0190-p", name="n")
    token = opik_trace.bind_trace(handle)
    try:
        t.log(step="s", model="m", prompt="p", duration_ms=1,
              provider="openrouter", status="error", error="boom")
    finally:
        opik_trace.reset_trace(token)
    tags = calls[0][1]["tags"]
    assert all(":" in x for x in tags), tags
    assert "status:error" in tags


def test_v2_fallback_span_carries_axis_tags(monkeypatch, tracer):
    """★부모 없이 홀로 선 trace 의 **span** 도 축 태그를 지녀야 한다.

    감사 도구는 **span 만 센다**(trace 는 묶음·이름을 얻는 데만 쓴다 —
    `fetch_spans` docstring). 그래서 fallback 이 trace 에만 태그를 달고
    span 을 맨몸으로 두면 그 호출은 「어느 스텝인지 모름」으로 남는다.

    2026-08-25 실측: 최소 검증판 주행에서 span 26/252 가 스텝 축을 잃었고,
    그중 11 건(`ref_image_gen` 10 · `floor_plan` 1)이 이 자리였다.
    """
    from app.core import config
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", True)
    t, calls = tracer
    t.log(step="ref_image_gen", model="gemini-3.1-flash-image-preview",
          prompt="p", duration_ms=10, provider="gemini")

    span_kw = dict(next(kw for k, kw in calls if k == "span"))
    tags = span_kw.get("tags") or []
    assert any(str(x).startswith("step:") for x in tags), \
        f"fallback span 에 스텝 축이 없다: {tags!r}"
    assert any(str(x).startswith("model:") for x in tags), \
        f"fallback span 에 모델 축이 없다: {tags!r}"


def test_v1_fallback_span_payload_unchanged(monkeypatch, tracer):
    """설정 OFF 는 지금 그대로 둔다 — 「되돌리기는 한 줄」 안전판.

    v2 를 끄면 이전 주행과 같은 payload 가 나가야 한다. 그래서 v1 span 에는
    태그를 **새로 달지 않는다**(이 결함은 v2 경로에서만 고친다).
    """
    from app.core import config
    monkeypatch.setattr(config.settings, "opik_trace_v2_enabled", False)
    t, calls = tracer
    t.log(step="ref_image_gen", model="probe", prompt="p", duration_ms=1)
    span_kw = dict(next(kw for k, kw in calls if k == "span"))
    assert "tags" not in span_kw, "v1 span payload 가 바뀌었다"
