"""StepRunner.build_opik_metadata — extra_tags / extra_metadata 병합 정책 검증.

v0.5.14 도입. 동적 ID(scene_index 등)는 태그 카디널리티를 폭발시키므로
metadata로 이동. 예약 키는 caller가 덮어쓰지 못하게 보호.
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from app.core.step_runner import StepRunner


@pytest.fixture
def runner():
    inst = StepRunner.__new__(StepRunner)
    inst.step_id = "scene_detail"
    inst.project_id = "p1"
    inst.episode_id = "e1"
    inst.db = MagicMock()
    inst.run_id = "r1"
    inst.manifest = {}
    inst.project_config = {}
    inst.opik_context = {
        "run_tag": "run-2026-04-20",
        "project_name": "금월도 1부",
        "episode_title": "Episode 1",
    }
    return inst


def test_base_metadata_with_context(runner):
    meta = runner.build_opik_metadata()
    assert meta["session_id"] == "run-2026-04-20"
    assert meta["trace_name"] == "금월도 1부 > Episode 1 > scene_detail"
    assert meta["project_name_tag"] == "금월도 1부"
    assert meta["episode_title_tag"] == "Episode 1"
    assert "scene_detail" in meta["tags"]
    assert "금월도 1부" in meta["tags"]
    assert "Episode 1" in meta["tags"]


def test_empty_context(runner):
    """opik_context가 비어 있으면 기본 tags(step_id만)만."""
    runner.opik_context = {}
    meta = runner.build_opik_metadata()
    assert meta["tags"] == ["scene_detail"]
    assert "session_id" not in meta
    assert "trace_name" not in meta


def test_extra_tags_appended(runner):
    meta = runner.build_opik_metadata(extra_tags=["retry", "gpt_fallback"])
    assert "retry" in meta["tags"]
    assert "gpt_fallback" in meta["tags"]
    # 기본 태그도 여전히 존재
    assert "scene_detail" in meta["tags"]


def test_extra_metadata_merged(runner):
    meta = runner.build_opik_metadata(extra_metadata={"scene_index": 12, "shot_index": 7})
    assert meta["scene_index"] == 12
    assert meta["shot_index"] == 7


def test_extra_metadata_protects_reserved_keys(runner):
    """예약 키(session_id, trace_name, tags 등)는 extra_metadata로 덮어쓰기 불가.

    v0.5.14 rationale: caller가 실수로 session_id를 추가하면 Opik 쓰레드 그룹핑이
    깨진다. build_opik_metadata 규약이 이 키를 보호해야 함.
    """
    meta = runner.build_opik_metadata(
        extra_metadata={
            "session_id": "malicious",      # 덮어쓰기 시도
            "trace_name": "evil-name",      # 덮어쓰기 시도
            "tags": ["overridden"],          # 덮어쓰기 시도
            "custom_field": "OK",            # 신규 키는 허용
        }
    )
    # 기존 값 유지
    assert meta["session_id"] == "run-2026-04-20"
    assert "scene_detail" in meta["tags"]
    assert "overridden" not in meta["tags"]
    assert meta["trace_name"] == "금월도 1부 > Episode 1 > scene_detail"
    # 신규 키는 병합
    assert meta["custom_field"] == "OK"


def test_both_extra_params(runner):
    meta = runner.build_opik_metadata(
        extra_tags=["verify"],
        extra_metadata={"scene_index": 12},
    )
    assert "verify" in meta["tags"]
    assert meta["scene_index"] == 12


def test_extra_metadata_none_ignored(runner):
    """extra_metadata=None일 때 예외/변경 없음."""
    meta = runner.build_opik_metadata(extra_metadata=None)
    # 기본 필드만 존재
    assert "tags" in meta
    # 예외 없이 반환
    assert isinstance(meta, dict)


def test_extra_tags_none_ignored(runner):
    """extra_tags=None일 때 기본 태그만."""
    meta = runner.build_opik_metadata(extra_tags=None)
    assert meta["tags"] == ["scene_detail", "금월도 1부", "Episode 1"]
