"""유료 호출의 토큰이 **DB 칸까지** 가나 (2026-08-29).

## 무엇이 빠져 있었나

`record_provider_call` 은 `usage_of_openai_response` 가 만든 값을 **Opik
에는 넘기면서 DB `log_llm_call` 에는 안 넘겼다.** 두 칸
(`input_tokens`/`output_tokens`)은 `log_llm_call` 이 **이미 받는데**
호출부가 안 준 것이다.

그래서 `llm_call_log` 의 토큰이 전부 0 이었고, 「판정 시간이 출력 토큰에
비례한다」(+0.996)를 재려고 **Opik 을 따로 봐야 했다.** DB 만 보고
「토큰을 안 쓴다」로 읽으면 정반대 결론이 나온다.

## 재는 것 — 이건 **끝점 시험이 아니다**

실제 DB 에 쓰지 않는다. `log_llm_call` **호출부가 무엇을 넘기는지**를
가로채서 본다. DB 왕복까지 태우는 시험은 따로다 — 그렇게 부르지 않는다.

  ① usage 가 있으면 두 칸이 **실제 값**으로 간다
  ② usage 가 없으면 **None** 이 간다 — 0 을 적으면 「안 썼다」와
     「모른다」가 구분되지 않는다
  ③ Opik 쪽은 종전대로 usage 를 통째로 받는다 (한쪽만 고치지 않았다)
"""
from __future__ import annotations

from typing import Any, Dict, List


def _capture(monkeypatch) -> Dict[str, List[Dict[str, Any]]]:
    """`log_llm_call` 과 Opik `log` 가 **무엇을 받는지** 가로챈다."""
    seen: Dict[str, List[Dict[str, Any]]] = {"db": [], "opik": []}

    import app.modules.llm.llm_logger as llm_logger

    def _fake_log(**kw):
        seen["db"].append(kw)
        return "call-id-1"

    monkeypatch.setattr(llm_logger, "log_llm_call", _fake_log)

    import app.modules.llm.image_tracer as it

    class _FakeTracer:
        def log(self, **kw):
            seen["opik"].append(kw)

    monkeypatch.setattr(it, "get_image_tracer", lambda: _FakeTracer())
    monkeypatch.setattr(it, "ambient_call_meta", lambda: {})
    return seen


def _call(monkeypatch, usage):
    from app.modules.llm.image_tracer import record_provider_call

    seen = _capture(monkeypatch)
    record_provider_call(
        step="still_recipe_judge", model="x-ai/grok-4.6",
        operation="still_recipe_judge_x", prompt="p",
        status="success", duration_ms=110_000, usage=usage,
        # ★서명을 조립부에서 그대로 — `meta` 는 필수 keyword 다
        meta={"project_id": "p1", "episode_id": "e1"},
    )
    return seen


def test_the_token_columns_carry_the_real_numbers(monkeypatch):
    seen = _call(monkeypatch, {
        "prompt_tokens": 6547, "completion_tokens": 4037,
        "total_tokens": 10584,
    })
    assert len(seen["db"]) == 1, "DB 기록이 안 불렸다"
    db = seen["db"][0]
    assert db["input_tokens"] == 6547, (
        "입력 토큰이 DB 로 안 간다 — 그러면 「무엇이 시간을 만드나」를 "
        "DB 로 못 묻는다")
    assert db["output_tokens"] == 4037, "출력 토큰이 DB 로 안 간다"


def test_missing_usage_writes_none_not_zero(monkeypatch):
    """★0 을 적으면 「안 썼다」와 「모른다」가 구분되지 않는다."""
    for usage in ({}, None):
        seen = _call(monkeypatch, usage)
        db = seen["db"][0]
        assert db["input_tokens"] is None, f"usage={usage!r} 인데 0 을 적었다"
        assert db["output_tokens"] is None


def test_opik_still_receives_the_whole_usage(monkeypatch):
    """★한쪽만 고치지 않았다 — Opik 은 캐시 칸까지 통째로 받는다."""
    u = {"prompt_tokens": 10, "completion_tokens": 20,
         "cached_tokens": 9}
    seen = _call(monkeypatch, u)
    assert len(seen["opik"]) == 1
    assert seen["opik"][0]["usage"] == u, (
        "Opik 쪽 usage 가 달라졌다 — 이 판은 DB 칸만 잇는 것이다")


def test_the_logger_actually_has_those_columns():
    """★없는 인자를 넘기면 TypeError 로 늦게 드러난다 — 서명으로 본다."""
    import inspect

    from app.modules.llm.llm_logger import log_llm_call

    ps = inspect.signature(log_llm_call).parameters
    assert "input_tokens" in ps and "output_tokens" in ps, (
        "`log_llm_call` 이 두 칸을 안 받는다 — 그러면 이 판의 전제가 "
        "틀린 것이니 여기서 멈춰야 한다")
