"""prompt version pack atomic (problems.md #6) — drift detection / strict toggle.

`prompt_loader._load_from_file` 가 같은 module 의 stem 별로 독립적으로 최신
version 디렉토리를 탐색하므로, 같은 module 안에서 stem 들이 서로 다른 version
에서 로드되어 prompt/schema contract drift 가 발생할 수 있다.

본 테스트는 다음을 검증한다.

1. **lenient (default)**: stem 이 latest pack 에 없어도 fallback 가능, drift 시
   ``logger.warning`` 1회 emit + ``get_effective_source.module_pack.stem_drift``
   가 True.
2. **strict** (``PROMPT_VERSION_PACK_STRICT=true``): latest pack 에 없으면
   ``RuntimeError`` 명시 raise — fallback 차단.
3. **drift 없음**: stem 이 latest 에 존재하면 warning 없음 + stem_drift=False.
4. **observability**: ``get_effective_source.module_pack`` 의 versions/latest/
   strict_enabled/stem_drift 4 필드 schema.
"""
from __future__ import annotations

import logging
from pathlib import Path

import pytest


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def fresh_drift_cache(monkeypatch):
    """Process-wide drift warning 캐시를 테스트마다 초기화."""
    from app.modules import prompt_loader

    monkeypatch.setattr(prompt_loader, "_DRIFT_WARNED", set())
    return prompt_loader


@pytest.fixture
def isolated_prompts(tmp_path, monkeypatch, fresh_drift_cache):
    """PROMPTS_BASE 를 tmp 로 격리하고 strict ENV 보장 off."""
    monkeypatch.setattr(fresh_drift_cache, "PROMPTS_BASE", tmp_path)
    monkeypatch.delenv("PROMPT_VERSION_PACK_STRICT", raising=False)
    return fresh_drift_cache, tmp_path


# ---------------------------------------------------------------------------
# 1. lenient (default) — drift 가능
# ---------------------------------------------------------------------------


def test_lenient_drift_falls_back_to_older_version(isolated_prompts, caplog):
    """latest pack 에 stem 없으면 이전 version 에서 로드 + warning emit."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "10.x").mkdir(parents=True)
    (tmp / "mod" / "12.x" / "system.md").write_text("v12 system", encoding="utf-8")
    (tmp / "mod" / "10.x" / "schema.json").write_text("{}", encoding="utf-8")

    with caplog.at_level(logging.WARNING, logger="app.modules.prompt_loader"):
        result = loader.load_schema("mod", "schema")

    assert result == {}
    drift_records = [
        r for r in caplog.records
        if "Prompt version pack drift" in r.message and "schema.json" in r.message
    ]
    assert len(drift_records) == 1, f"기대 1회, 실제 {len(drift_records)}회"
    assert "loaded_from=10.x" in drift_records[0].message
    assert "latest_in_module=12.x" in drift_records[0].message


def test_lenient_drift_warning_dedup_across_calls(isolated_prompts, caplog):
    """동일 (module, stem, version) 조합은 process 당 1회만 emit."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "10.x").mkdir(parents=True)
    (tmp / "mod" / "10.x" / "system.md").write_text("legacy", encoding="utf-8")

    with caplog.at_level(logging.WARNING, logger="app.modules.prompt_loader"):
        loader.load_prompt("mod", "system")
        loader.load_prompt("mod", "system")
        loader.load_prompt("mod", "system")

    drift_records = [r for r in caplog.records if "Prompt version pack drift" in r.message]
    assert len(drift_records) == 1, "중복 dedup 실패"


def test_lenient_no_drift_when_stem_in_latest(isolated_prompts, caplog):
    """stem 이 latest 에 있으면 warning 없음 + 기존 동작 보존."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "10.x").mkdir(parents=True)
    (tmp / "mod" / "12.x" / "system.md").write_text("v12", encoding="utf-8")
    (tmp / "mod" / "10.x" / "system.md").write_text("v10", encoding="utf-8")

    with caplog.at_level(logging.WARNING, logger="app.modules.prompt_loader"):
        text = loader.load_prompt("mod", "system")

    assert text == "v12"  # latest 우선
    assert not any("drift" in r.message for r in caplog.records)


def test_lenient_missing_stem_returns_none_silently(isolated_prompts, caplog):
    """모든 version 에 stem 없으면 None 반환, lenient 모드에서는 RuntimeError 없음."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "10.x").mkdir(parents=True)
    (tmp / "mod" / "12.x" / "other.md").write_text("x", encoding="utf-8")

    with caplog.at_level(logging.WARNING, logger="app.modules.prompt_loader"):
        text = loader._load_from_file("mod", "missing_stem")

    assert text is None


# ---------------------------------------------------------------------------
# 2. strict (opt-in) — RuntimeError on drift
# ---------------------------------------------------------------------------


def test_strict_raises_when_stem_missing_in_latest(isolated_prompts, monkeypatch):
    """latest pack 에 stem 없고 이전 version 에는 있을 때 RuntimeError."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "10.x").mkdir(parents=True)
    (tmp / "mod" / "10.x" / "system.md").write_text("legacy", encoding="utf-8")
    monkeypatch.setenv("PROMPT_VERSION_PACK_STRICT", "true")

    with pytest.raises(RuntimeError) as excinfo:
        loader.load_prompt("mod", "system")
    assert "missing in latest pack '12.x'" in str(excinfo.value)
    assert "PROMPT_VERSION_PACK_STRICT" in str(excinfo.value)


def test_strict_raises_when_stem_missing_everywhere(isolated_prompts, monkeypatch):
    """어떤 version 에도 stem 없을 때 RuntimeError (lenient 는 None 반환)."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "12.x" / "other.md").write_text("x", encoding="utf-8")
    monkeypatch.setenv("PROMPT_VERSION_PACK_STRICT", "true")

    with pytest.raises(RuntimeError) as excinfo:
        loader.load_prompt("mod", "missing")
    assert "not found in any version" in str(excinfo.value)


def test_strict_passes_when_stem_in_latest(isolated_prompts, monkeypatch):
    """strict 라도 stem 이 latest 에 있으면 정상 로드."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "12.x" / "system.md").write_text("v12", encoding="utf-8")
    monkeypatch.setenv("PROMPT_VERSION_PACK_STRICT", "true")

    text = loader.load_prompt("mod", "system")
    assert text == "v12"


@pytest.mark.parametrize(
    "value,expected_strict",
    [
        ("true", True),
        ("True", True),
        ("1", True),
        ("yes", True),
        ("on", True),
        ("false", False),
        ("0", False),
        ("", False),
        ("garbage", False),
    ],
)
def test_strict_env_truthy_parsing(monkeypatch, value, expected_strict):
    """PROMPT_VERSION_PACK_STRICT 다양한 값 파싱."""
    from app.modules import prompt_loader

    monkeypatch.setenv("PROMPT_VERSION_PACK_STRICT", value)
    assert prompt_loader._is_version_pack_strict() is expected_strict


def test_strict_unset_means_lenient(monkeypatch):
    """env 미설정 시 strict=False (default)."""
    from app.modules import prompt_loader

    monkeypatch.delenv("PROMPT_VERSION_PACK_STRICT", raising=False)
    assert prompt_loader._is_version_pack_strict() is False


# ---------------------------------------------------------------------------
# 3. get_effective_source.module_pack — observability schema
# ---------------------------------------------------------------------------


class _StubResult:
    def __init__(self, rows):
        self._rows = rows

    def fetchall(self):
        return self._rows


class _StubDB:
    def __init__(self, rows):
        self._rows = rows

    def execute(self, sql, params=None):
        return _StubResult(self._rows)


def test_module_pack_drift_visible_in_effective_source(isolated_prompts):
    """stem_drift=True 가 module_pack 에 노출 + versions/latest 정렬 확인."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "11.x").mkdir(parents=True)
    (tmp / "mod" / "10.x").mkdir(parents=True)
    (tmp / "mod" / "10.x" / "system.md").write_text("legacy", encoding="utf-8")

    out = loader.get_effective_source("mod", "system", _StubDB([]))
    pack = out["module_pack"]
    assert pack["versions"] == ["12.x", "11.x", "10.x"]
    assert pack["latest"] == "12.x"
    assert pack["strict_enabled"] is False
    assert pack["stem_drift"] is True
    assert out["candidates"]["file"]["version"] == "10.x"


def test_module_pack_no_drift_when_stem_in_latest(isolated_prompts):
    """stem_drift=False — file_candidate.version == latest."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "12.x" / "system.md").write_text("v12", encoding="utf-8")

    out = loader.get_effective_source("mod", "system", _StubDB([]))
    assert out["module_pack"]["latest"] == "12.x"
    assert out["module_pack"]["stem_drift"] is False


def test_module_pack_strict_enabled_reflects_env(isolated_prompts, monkeypatch):
    """strict_enabled 가 env 평가 결과 반영."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "1.x").mkdir(parents=True)
    monkeypatch.setenv("PROMPT_VERSION_PACK_STRICT", "1")
    out = loader.get_effective_source("mod", "system", _StubDB([]))
    assert out["module_pack"]["strict_enabled"] is True


def test_module_pack_no_module_dir_returns_empty(isolated_prompts):
    """module dir 미존재 → versions=[] + latest=None + stem_drift=False."""
    loader, _ = isolated_prompts
    out = loader.get_effective_source("missing_mod", "system", _StubDB([]))
    assert out["module_pack"] == {
        "versions": [],
        "latest": None,
        "strict_enabled": False,
        "stem_drift": False,
    }


def test_module_pack_no_file_candidate_no_drift(isolated_prompts):
    """file 후보 없으면 stem_drift=False (drift 가 file_candidate 기준이므로)."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "11.x").mkdir(parents=True)
    out = loader.get_effective_source("mod", "missing_stem", _StubDB([]))
    assert out["candidates"]["file"] is None
    assert out["module_pack"]["stem_drift"] is False
    assert out["module_pack"]["latest"] == "12.x"


# ---------------------------------------------------------------------------
# 4. backward-compat — 기존 caller 변경 없음
# ---------------------------------------------------------------------------


def test_load_prompt_existing_signature_unchanged(isolated_prompts):
    """load_prompt(module, name, db=, **kwargs) 시그니처와 format kwargs 동작 보존."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "1.x").mkdir(parents=True)
    (tmp / "mod" / "1.x" / "tmpl.md").write_text("Hello {greeting}!", encoding="utf-8")

    text = loader.load_prompt("mod", "tmpl", greeting="World")
    assert text == "Hello World!"


def test_load_schema_existing_signature_unchanged(isolated_prompts):
    """load_schema(module, name, db=) 시그니처와 동작 보존."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "1.x").mkdir(parents=True)
    (tmp / "mod" / "1.x" / "schema.json").write_text(
        '{"type": "object"}', encoding="utf-8",
    )

    schema = loader.load_schema("mod", "schema")
    assert schema == {"type": "object"}


def test_get_active_version_uses_pack_helper(isolated_prompts):
    """get_active_version 이 _list_module_versions 와 동일 결과."""
    loader, tmp = isolated_prompts
    (tmp / "mod" / "12.x").mkdir(parents=True)
    (tmp / "mod" / "10.x").mkdir(parents=True)

    assert loader.get_active_version("mod", "any") == "12.x"
