# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""Tests for the per-model Anthropic tool-version dispatch helpers in
``core.inference.external_provider``.

Anthropic ships date-pinned tool versions per model family; sending the
wrong-dated variant to a model 400s upstream. Pins the dispatch matrix for
web_search/web_fetch/code_execution helpers, the ``_stream_anthropic`` body
integration, and the unchanged code-execution beta header."""

import asyncio
import json

import httpx
import pytest

from core.inference import external_provider as ep_mod
from core.inference.external_provider import (
    ExternalProviderClient,
    _anthropic_code_execution_version,
    _anthropic_web_fetch_version,
    _anthropic_web_search_version,
)


# ── helper-level dispatch matrix ────────────────────────────────────


@pytest.mark.parametrize(
    "model,expected",
    [
        ("claude-opus-4-7", "web_search_20260209"),
        ("claude-opus-4-6", "web_search_20260209"),
        ("claude-sonnet-4-6", "web_search_20260209"),
        ("claude-opus-4-5-20251101", "web_search_20250305"),
        ("claude-sonnet-4-5-20250929", "web_search_20250305"),
        ("claude-haiku-4-5-20251001", "web_search_20250305"),
        ("claude-opus-4-1-20250805", "web_search_20250305"),
        ("claude-opus-4-20250514", "web_search_20250305"),
        ("claude-sonnet-4-20250514", "web_search_20250305"),
        ("claude-3-5-sonnet-20241022", "web_search_20250305"),
    ],
)
def test_web_search_version_dispatch(model, expected):
    assert _anthropic_web_search_version(model) == expected


@pytest.mark.parametrize(
    "model,expected",
    [
        ("claude-opus-4-7", "web_fetch_20260209"),
        ("claude-opus-4-6", "web_fetch_20260209"),
        ("claude-sonnet-4-6", "web_fetch_20260209"),
        ("claude-opus-4-5-20251101", "web_fetch_20250910"),
        ("claude-sonnet-4-5-20250929", "web_fetch_20250910"),
        ("claude-haiku-4-5-20251001", "web_fetch_20250910"),
        ("claude-opus-4-1-20250805", "web_fetch_20250910"),
    ],
)
def test_web_fetch_version_dispatch(model, expected):
    assert _anthropic_web_fetch_version(model) == expected


@pytest.mark.parametrize(
    "model,expected",
    [
        ("claude-opus-4-7", "code_execution_20260120"),
        ("claude-opus-4-6", "code_execution_20260120"),
        ("claude-sonnet-4-6", "code_execution_20260120"),
        ("claude-opus-4-5-20251101", "code_execution_20260120"),
        ("claude-sonnet-4-5-20250929", "code_execution_20260120"),
        # Haiku 4.5 only lists the legacy version in the model table.
        ("claude-haiku-4-5-20251001", "code_execution_20250825"),
        ("claude-opus-4-1-20250805", "code_execution_20250825"),
        # Deprecated 4.0 lineage still works on the legacy version.
        ("claude-opus-4-20250514", "code_execution_20250825"),
        ("claude-sonnet-4-20250514", "code_execution_20250825"),
    ],
)
def test_code_execution_version_dispatch(model, expected):
    assert _anthropic_code_execution_version(model) == expected


# ── streaming integration: outbound body carries the right versions ──


def _drive(coro):
    return asyncio.new_event_loop().run_until_complete(coro)


def _mock_http_client(monkeypatch, handler):
    monkeypatch.setattr(
        ep_mod,
        "_http_client",
        httpx.AsyncClient(transport = httpx.MockTransport(handler)),
    )


def _make_client() -> ExternalProviderClient:
    return ExternalProviderClient(
        provider_type = "anthropic",
        base_url = "https://api.anthropic.com/v1",
        api_key = "sk-ant-test",
    )


def _capture_outbound(monkeypatch, model: str) -> dict:
    captured: dict = {}

    def handler(request: httpx.Request) -> httpx.Response:
        captured["body"] = json.loads(request.content.decode("utf-8"))
        captured["headers"] = dict(request.headers)
        return httpx.Response(
            200,
            content = b'event: message_stop\ndata: {"type": "message_stop"}\n\n',
            headers = {"content-type": "text/event-stream"},
        )

    _mock_http_client(monkeypatch, handler)

    async def run():
        client = _make_client()
        async for _ in client._stream_anthropic(
            messages = [{"role": "user", "content": "hi"}],
            model = model,
            temperature = 0.7,
            top_p = 0.95,
            max_tokens = 64,
            enabled_tools = ["web_search", "code_execution"],
        ):
            pass
        await client.close()

    _drive(run())
    return captured


def test_outbound_body_uses_new_versions_on_opus_4_7(monkeypatch):
    captured = _capture_outbound(monkeypatch, "claude-opus-4-7")
    tool_types = {t.get("type") for t in (captured["body"].get("tools") or [])}
    assert "web_search_20260209" in tool_types
    assert "code_execution_20260120" in tool_types
    assert "web_search_20250305" not in tool_types
    assert "code_execution_20250825" not in tool_types
    # One beta header gates both _20250825 and _20260120.
    assert "code-execution-2025-08-25" in captured["headers"].get("anthropic-beta", "")


def test_outbound_body_falls_back_on_haiku_4_5(monkeypatch):
    captured = _capture_outbound(monkeypatch, "claude-haiku-4-5-20251001")
    tool_types = {t.get("type") for t in (captured["body"].get("tools") or [])}
    # Haiku 4.5 only accepts the legacy versions.
    assert "web_search_20250305" in tool_types
    assert "code_execution_20250825" in tool_types
    assert "web_search_20260209" not in tool_types
    assert "code_execution_20260120" not in tool_types


def test_outbound_body_mixes_versions_on_sonnet_4_5(monkeypatch):
    # Sonnet 4.5 gets the new code_execution but the old web_search.
    captured = _capture_outbound(monkeypatch, "claude-sonnet-4-5-20250929")
    tool_types = {t.get("type") for t in (captured["body"].get("tools") or [])}
    assert "web_search_20250305" in tool_types
    assert "code_execution_20260120" in tool_types
    assert "web_search_20260209" not in tool_types
    assert "code_execution_20250825" not in tool_types
