"""감사 도구의 자료원이 span 으로 옮겨 간다."""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[3]))

from tools.opik_prompt_audit.audit.fetch import (  # noqa: E402
    is_test_origin, step_of)


def test_step_from_axis_tag():
    assert step_of({"tags": ["step:scene_detail", "model:gpt-5.6-sol"]}) \
        == "scene_detail"


def test_step_falls_back_to_legacy_trace_name():
    """지난 자료(v1)도 계속 읽혀야 한다 — 대조가 끊기면 안 된다."""
    row = {"metadata": {"trace_name": "마지막 임무 > 1회 > scene_detail"}}
    assert step_of(row) == "scene_detail"


def test_call_step_reaches_parent_even_when_span_name_is_set():
    """★litellm span 의 name 은 `{model}_{obj}_{created}` 라 **절대 비지 않는다**.

    step_of 를 먼저 부르면 그 이름으로 떨어져 부모를 영영 안 본다.
    """
    from tools.opik_prompt_audit.audit.fetch import step_of_call

    span = {"trace_id": "T1", "tags": ["model:gpt-5.6-sol"],
            "name": "gpt-5.6-sol_chat.completion_1755000000"}
    index = {"T1": {"id": "T1",
                    "metadata": {"trace_name": "P > 1회 > scene_detail"}}}
    assert step_of_call(span, index) == "scene_detail"


def test_call_step_prefers_own_axis_tag():
    from tools.opik_prompt_audit.audit.fetch import step_of_call

    span = {"trace_id": "T1", "tags": ["step:shot_director"], "name": "x_y_1"}
    index = {"T1": {"id": "T1", "tags": ["step:scene_detail"]}}
    assert step_of_call(span, index) == "shot_director"


def test_call_step_falls_back_to_span_name_last():
    from tools.opik_prompt_audit.audit.fetch import step_of_call

    span = {"trace_id": "T9", "tags": [], "name": "still_recipe_roll/gpt-image-2"}
    assert step_of_call(span, {}) == "still_recipe_roll/gpt-image-2"


def test_step_falls_back_to_name():
    assert step_of({"name": "still_recipe_roll/gpt-image-2"}) \
        == "still_recipe_roll/gpt-image-2"


def test_axis_tag_wins_over_legacy():
    row = {"tags": ["step:shot_director"],
           "metadata": {"trace_name": "a > b > scene_detail"}}
    assert step_of(row) == "shot_director"


def test_test_origin_no_identity_zero_duration():
    """갈래 ① — scope 밖에서 부른 시험. 신원 없고 시간 0."""
    assert is_test_origin({"metadata": {"step": "s", "duration_ms": 0}}) is True
    assert is_test_origin({"metadata": {
        "step": "s", "duration_ms": 120}}) is False
    assert is_test_origin({"metadata": {}}) is False


def test_test_origin_fake_identity():
    """★갈래 ② — 가짜 신원을 달고 나간 시험.

    generation_context("p-i2i", …) 안에서 부르면 project_id="p-i2i" 가 실려
    갈래 ① 을 통과한다.
    """
    assert is_test_origin({"metadata": {
        "step": "i2i_edit", "duration_ms": 0, "project_id": "p-i2i"}}) is True
    assert is_test_origin({"metadata": {
        "step": "i2i_edit", "duration_ms": 340, "project_id": "p-i2i"}}) is True


def test_real_project_id_is_never_filtered():
    """진짜 UUID 신원은 무슨 일이 있어도 안 거른다 — 실기록을 지우면 최악이다."""
    assert is_test_origin({"metadata": {
        "step": "still_recipe", "duration_ms": 0,
        "project_id": "5bddbdfc-2681-42a6-9837-43f35f60049d"}}) is False


def test_test_span_is_filtered_via_its_parent():
    """★★span 만 보면 시험 기록을 못 거른다 — 부모를 함께 봐야 한다.

    ImageTracer 는 trace metadata 에만 step 을 넣고 span metadata 에는 안
    넣는다. is_test_origin 은 "step" 을 요구하므로 span 은 무조건 통과한다.
    부모 trace 만 지워지고 자식 span 이 새 감사 SOT 에 그대로 들어간다 —
    관문 「시험 기록 0건」이 거짓 통과한다.
    """
    from tools.opik_prompt_audit.audit.fetch import select_call_rows

    test_span = {"trace_id": "T-test",
                 "metadata": {"duration_ms": 0, "ref_count": 0},
                 "input": {"prompt": "x"}}
    real_span = {"trace_id": "T-real",
                 "metadata": {"duration_ms": 340,
                              "project_id": "5bddbdfc-2681-42a6-9837-43f35f60049d"}}
    index = {
        "T-test": {"id": "T-test",
                   "metadata": {"step": "i2i_edit", "duration_ms": 0}},
        "T-real": {"id": "T-real",
                   "metadata": {"step": "still_recipe", "duration_ms": 340,
                                "project_id": "5bddbdfc-2681-42a6-9837-43f35f60049d"}},
    }
    out = select_call_rows([test_span, real_span], index)
    assert out == [real_span], "시험 span 이 살아남았다"


def test_orphan_span_is_kept():
    """부모를 못 찾아도 버리지 않는다 — 못 찾은 것과 시험인 것은 다르다."""
    from tools.opik_prompt_audit.audit.fetch import select_call_rows

    sp = {"trace_id": "T-unknown", "metadata": {"duration_ms": 120}}
    assert select_call_rows([sp], {}) == [sp]


def test_trace_index_widens_the_window():
    """★부모 창을 안 넓히면 경계 직전 시작한 긴 trace 의 span 이 부모를 잃는다.

    부모를 잃으면 시험 판별도 legacy 스텝 fallback 도 함께 깨진다.
    """
    import inspect

    from tools.opik_prompt_audit.audit import fetch
    src = inspect.getsource(fetch.fetch_trace_index)
    assert "pad_hours" in src, "부모 조회 창을 안 넓힌다"


def test_caveat_helper_exists():
    """헬퍼가 한계를 적는다 — 다만 이것만으로는 리포트에 실리는지 모른다."""
    from tools.opik_prompt_audit.audit.fetch import test_filter_caveat
    assert "완전하지 않다" in test_filter_caveat()


def _rendered(tmp_path, meta):
    """★render() 를 실제로 불러 **나온 HTML** 을 본다.

    2026-08-24 Codex BLOCK 2. 앞선 시험은 helper 문자열만 검사해서,
    main 이 caveat 를 넘겨도 report 가 그것을 **안 읽는다**는 사실을 못
    잡았다. 「조립하는 자리」가 아니라 「나오는 것」을 재야 한다 —
    이 판에서 세 번째로 같은 자리에서 걸렸다.
    """
    from tools.opik_prompt_audit.audit.report import render

    steps = {"scene_detail": {
        "calls": 3, "system_avg": 10, "user_avg": 10,
        "system_total": 30, "user_total": 30, "total_chars": 60,
        "system_static_ratio": 0.5, "user_static_ratio": 0.5,
        "system_static_lines": [], "user_static_lines": [],
    }}
    out = render(tmp_path / "r", meta, steps, [], {}, {})
    return out.read_text(encoding="utf-8")


def test_report_calls_the_unit_a_span_not_a_trace(tmp_path):
    """★측정 정의가 아직 「trace=Opik 기록 1건」이면 결과를 오독시킨다.

    머리말은 「호출 N건」인데 다음 줄이 trace 단위라고 설명하면, 같은
    리포트 안에서 두 단위가 섞인다.
    """
    html = _rendered(tmp_path, {"since": "S", "call_count": 564})
    assert "호출=Opik span 1건" in html
    assert "trace=Opik 기록 1건" not in html


def test_report_shows_excluded_test_rows(tmp_path):
    """★몇 건을 뺐는지가 리포트에 있어야 한다 — 선택 목록만 넘기면 유실된다."""
    html = _rendered(tmp_path, {
        "since": "S", "call_count": 564, "raw_count": 732,
        "excluded_test_rows": 168})
    assert "168" in html and "732" in html
    assert "시험 기록 제외" in html


def test_report_renders_the_caveat(tmp_path):
    """★한계 문구가 실제로 HTML 에 나와야 한다."""
    from tools.opik_prompt_audit.audit.fetch import test_filter_caveat

    html = _rendered(tmp_path, {
        "since": "S", "call_count": 1, "caveat": test_filter_caveat()})
    assert "완전하지 않다" in html


def test_main_passes_the_counts_to_render():
    """main 이 raw/selected 차이와 caveat 를 meta 로 넘기는지."""
    import inspect

    from tools.opik_prompt_audit import main as audit_main
    src = inspect.getsource(audit_main)
    for key in ("raw_count", "excluded_test_rows", "caveat"):
        assert key in src, f"main 이 {key} 를 안 넘긴다"


def test_counting_uses_spans_only():
    """★★관문 14 — 같은 v1 호출을 두 번 세면 안 된다.

    ImageTracer.log 는 호출 하나마다 trace 를 만들고 그 밑에 span 도 만든다.
    litellm 도 trace_id 가 없으면 trace 를 만들고 span 은 **항상** 만든다.
    즉 v1 호출 한 건이 trace 1 + span 1 로 남아 있다 — 둘을 합치면 두 배가
    되고, 그 숫자로는 「팩이 얼마나 커졌나」를 못 묻는다.

    이 시험은 **소비처가 trace 를 세지 않는지**를 소스로 못박는다.
    """
    import inspect

    from tools.opik_prompt_audit import main as audit_main
    src = inspect.getsource(audit_main)
    assert "fetch_spans" in src, "main 이 span 을 안 걷는다"
    assert "select_call_rows" in src, "main 이 시험 기록을 안 거른다"
    assert "call_count" in src, "세는 이름이 아직 trace 기준이다"
