"""LLM görev yönlendirme testleri.

En kritik test `test_no_confidential_task_leaves_anthropic` — hukuki veri
sınırının regresyona karşı kilidi. Bu test kırmızıya dönerse yayınlanmamış
buluş verisi üçüncü bir sağlayıcıya gidiyor demektir.
"""

from __future__ import annotations

import pytest

from app.services.llm import gemini
from app.services.llm.router import (
    Provider,
    Sensitivity,
    TaskSpec,
    UnknownTaskError,
    all_tasks,
    cache_control_for,
    resolve,
    system_block,
)

# ---------------------------------------------------------------------------
# Veri sınırı — hukuki invariant
# ---------------------------------------------------------------------------


def test_no_confidential_task_leaves_anthropic() -> None:
    """CONFIDENTIAL görevlerin hiçbiri Anthropic dışına çıkamaz.

    Patent taslakları yayınlanmamış buluş açıklamasıdır; başvuru öncesi ifşa
    yenilik kaybına yol açar (bkz. .claude/CLAUDE.md § Hukuki Kurallar).
    """
    for task, spec in all_tasks().items():
        if spec.sensitivity is Sensitivity.CONFIDENTIAL:
            assert spec.provider is Provider.ANTHROPIC, (
                f"{task} gizli veri işliyor ama {spec.provider} sağlayıcısına "
                "yönlendirilmiş"
            )


def test_confidential_task_on_foreign_provider_is_rejected() -> None:
    """Invariant sadece kayıt için değil, TaskSpec'in kendisi için geçerli."""
    with pytest.raises(ValueError, match="CONFIDENTIAL"):
        TaskSpec(
            task="test.leak",
            provider=Provider.GOOGLE,
            model="gemini-2.5-flash",
            sensitivity=Sensitivity.CONFIDENTIAL,
            max_tokens=100,
        )


def test_public_task_may_use_any_provider() -> None:
    spec = TaskSpec(
        task="test.public",
        provider=Provider.GOOGLE,
        model="gemini-2.5-flash",
        sensitivity=Sensitivity.PUBLIC,
        max_tokens=100,
    )
    assert spec.provider is Provider.GOOGLE


def test_only_hit_translation_is_public() -> None:
    """Gemini'ye giden görev listesi bilinçli olarak tek kalemdir.

    Yeni bir PUBLIC görev eklemek veri sınırı kararıdır — bu testin
    güncellenmesi o kararın bilinçli olduğunu belgeler.
    """
    public = {t for t, s in all_tasks().items() if s.sensitivity is Sensitivity.PUBLIC}
    assert public == {"translation.hits"}


def test_max_tokens_must_be_positive() -> None:
    with pytest.raises(ValueError, match="max_tokens"):
        TaskSpec(
            task="test.bad",
            provider=Provider.ANTHROPIC,
            model="claude-haiku-4-5",
            sensitivity=Sensitivity.CONFIDENTIAL,
            max_tokens=0,
        )


# ---------------------------------------------------------------------------
# Çözümleme
# ---------------------------------------------------------------------------


def test_resolve_returns_registered_spec() -> None:
    spec = resolve("drafting.stream")
    assert spec.task == "drafting.stream"
    assert spec.provider is Provider.ANTHROPIC
    assert spec.model.startswith("claude-opus")


def test_unknown_task_raises() -> None:
    with pytest.raises(UnknownTaskError):
        resolve("nope.not_a_task")


def test_task_id_matches_spec_key() -> None:
    """Kayıt anahtarı ile spec.task aynı olmalı — usage log'u buna dayanır."""
    for key, spec in all_tasks().items():
        assert key == spec.task


def test_env_override_changes_model(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("PY_MODEL_DRAFTING_CLAIMS", "claude-sonnet-5")
    assert resolve("drafting.claims").model == "claude-sonnet-5"


def test_env_override_cannot_change_provider(monkeypatch: pytest.MonkeyPatch) -> None:
    """Sağlayıcı ortam değişkeniyle değiştirilemez — veri sınırı kod kararıdır."""
    monkeypatch.setenv("PY_MODEL_DRAFTING_STREAM", "gemini-2.5-flash")
    spec = resolve("drafting.stream")
    # Model string'i değişse bile sağlayıcı Anthropic kalır; çağıran taraf
    # Anthropic SDK'sını kullanmaya devam eder.
    assert spec.provider is Provider.ANTHROPIC


def test_blank_env_override_is_ignored(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("PY_MODEL_DRAFTING_CLAIMS", "   ")
    assert resolve("drafting.claims").model == "claude-sonnet-4-6"


# ---------------------------------------------------------------------------
# Cache eşiği
# ---------------------------------------------------------------------------


def test_short_prompt_gets_no_cache_control_on_haiku() -> None:
    """Haiku 4.5'in eşiği 4096 token — kısa prompt cache'lenemez.

    Eskiden bu rotalarda cache_control vardı ve sessizce hiçbir şey yapmıyordu.
    """
    assert cache_control_for("claude-haiku-4-5", "kısa bir sistem prompt'u") is None


def test_long_prompt_gets_cache_control() -> None:
    long_prompt = "x" * 20000
    assert cache_control_for("claude-haiku-4-5", long_prompt) == {"type": "ephemeral"}


def test_opus_5_has_lower_cache_floor_than_haiku() -> None:
    """Opus 5 eşiği 512; Haiku 4.5 eşiği 4096. Aynı prompt farklı sonuç verir."""
    prompt = "y" * 2000  # ~1000 tahmini token
    assert cache_control_for("claude-opus-5", prompt) == {"type": "ephemeral"}
    assert cache_control_for("claude-haiku-4-5", prompt) is None


def test_unknown_model_uses_conservative_floor() -> None:
    assert cache_control_for("some-unreleased-model", "y" * 2000) is None


def test_system_block_shape() -> None:
    block = system_block("claude-opus-5", "z" * 5000)
    assert len(block) == 1
    assert block[0]["type"] == "text"
    assert block[0]["cache_control"] == {"type": "ephemeral"}

    bare = system_block("claude-haiku-4-5", "kısa")
    assert "cache_control" not in bare[0]
    assert bare[0]["text"] == "kısa"


# ---------------------------------------------------------------------------
# Gemini kullanım adaptörü
# ---------------------------------------------------------------------------


class _FakeUsage:
    prompt_token_count = 1000
    candidates_token_count = 250
    cached_content_token_count = 400


def test_gemini_usage_maps_to_anthropic_shape() -> None:
    rec = gemini.usage_as_record(_FakeUsage())
    # prompt_token_count cache'li kısmı DA içerir; çift saymamak için düşülür.
    assert rec["input_tokens"] == 600
    assert rec["output_tokens"] == 250
    assert rec["cache_read_input_tokens"] == 400
    assert rec["cache_creation_input_tokens"] == 0


def test_gemini_usage_handles_none() -> None:
    assert gemini.usage_as_record(None) == {}


def test_gemini_usage_handles_missing_fields() -> None:
    class Empty:
        pass

    rec = gemini.usage_as_record(Empty())
    assert rec["input_tokens"] == 0
    assert rec["output_tokens"] == 0
