"""Tests for LLMClient.call() format contract.

The contract: call(prompt, text) sends prompt and text to the LLM.
No client may add quotes, modify content, or change semantics.
When text is empty, send only prompt as single message.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from neo_cortex.llm import GeminiClient, GroqClient, OpenAICompatibleClient, ClaudeCodeClient, ZhipuClient


class TestGeminiClientFormat:
    @pytest.mark.asyncio
    async def test_call_does_not_add_quotes(self):
        """GeminiClient must NOT wrap text in quotes."""
        client = GeminiClient("fake-key")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "candidates": [{"content": {"parts": [{"text": "ok"}]}}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("Classify this:", "hello world")

        call_args = client._client.post.call_args
        payload = call_args[1]["json"]
        sent_text = payload["contents"][0]["parts"][0]["text"]
        assert '"hello world"' not in sent_text
        assert "Classify this:" in sent_text
        assert "hello world" in sent_text

    @pytest.mark.asyncio
    async def test_call_with_empty_text(self):
        """When text is empty, send only prompt."""
        client = GeminiClient("fake-key")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "candidates": [{"content": {"parts": [{"text": "ok"}]}}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("Just a prompt", "")

        call_args = client._client.post.call_args
        payload = call_args[1]["json"]
        sent_text = payload["contents"][0]["parts"][0]["text"]
        assert sent_text.strip() == "Just a prompt"

    def test_model_param_overrides_config(self):
        """GeminiClient accepts model param for runtime switching."""
        client = GeminiClient("fake-key", model="gemini-2.5-flash-lite")
        assert client._model == "gemini-2.5-flash-lite"
        assert "gemini-2.5-flash-lite" in client.name


class TestGroqClientFormat:
    @pytest.mark.asyncio
    async def test_call_uses_system_user_messages(self):
        """GroqClient should use system+user messages (it's OpenAI-compatible)."""
        client = GroqClient("fake-key")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "choices": [{"message": {"content": "ok"}}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("System prompt", "User text")

        call_args = client._client.post.call_args
        payload = call_args[1]["json"]
        messages = payload["messages"]
        assert len(messages) == 2
        assert messages[0]["role"] == "system"
        assert messages[0]["content"] == "System prompt"
        assert messages[1]["role"] == "user"
        assert messages[1]["content"] == "User text"

    @pytest.mark.asyncio
    async def test_call_with_empty_text(self):
        """When text is empty, send single user message."""
        client = GroqClient("fake-key")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "choices": [{"message": {"content": "ok"}}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("Full prompt here", "")

        call_args = client._client.post.call_args
        payload = call_args[1]["json"]
        messages = payload["messages"]
        assert len(messages) == 1
        assert messages[0]["role"] == "user"
        assert messages[0]["content"] == "Full prompt here"

    def test_model_param_overrides_config(self):
        """GroqClient accepts model param for runtime switching."""
        client = GroqClient("fake-key", model="llama-3.1-8b-instant")
        assert client._model == "llama-3.1-8b-instant"
        assert "llama-3.1-8b-instant" in client.name


class TestOpenAICompatibleFormat:
    @pytest.mark.asyncio
    async def test_call_with_empty_text(self):
        """When text is empty, send single user message."""
        client = OpenAICompatibleClient("http://localhost:11434/v1", "", "llama3.2")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "choices": [{"message": {"content": "ok"}}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("Full prompt here", "")

        call_args = client._client.post.call_args
        payload = call_args[1]["json"]
        messages = payload["messages"]
        assert len(messages) == 1
        assert messages[0]["role"] == "user"
        assert messages[0]["content"] == "Full prompt here"


class TestClaudeCodeClientFormat:
    @pytest.mark.asyncio
    async def test_call_with_empty_text(self):
        """When text is empty, send only prompt (no trailing newline)."""
        client = ClaudeCodeClient(model="haiku")

        mock_msg = MagicMock()
        mock_msg.content = "ok"
        mock_sdk = AsyncMock()
        mock_sdk.query = AsyncMock()

        async def mock_receive():
            yield mock_msg
        mock_sdk.receive_response = mock_receive

        client._client = mock_sdk

        await client.call("Just a prompt", "")
        args, kwargs = mock_sdk.query.call_args
        assert args[0] == "Just a prompt"
        assert "session_id" in kwargs

    @pytest.mark.asyncio
    async def test_call_with_text_joins_with_newline(self):
        """When text is provided, join with newline."""
        client = ClaudeCodeClient(model="haiku")

        mock_msg = MagicMock()
        mock_msg.content = "ok"
        mock_sdk = AsyncMock()
        mock_sdk.query = AsyncMock()

        async def mock_receive():
            yield mock_msg
        mock_sdk.receive_response = mock_receive

        client._client = mock_sdk

        await client.call("System instruction", "User data")
        args, kwargs = mock_sdk.query.call_args
        assert args[0] == "System instruction\nUser data"
        assert "session_id" in kwargs


class TestZhipuClientFormat:
    @pytest.mark.asyncio
    async def test_call_uses_anthropic_format(self):
        """ZhipuClient sends system+user via Anthropic-compatible endpoint."""
        client = ZhipuClient("fake-key", model="glm-4.7")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "content": [{"type": "text", "text": "result"}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("System prompt", "User text")

        call_args = client._client.post.call_args
        url = call_args[0][0]
        assert "/api/anthropic/" in url
        payload = call_args[1]["json"]
        assert payload["system"] == "System prompt"
        assert payload["messages"][0]["role"] == "user"
        assert payload["messages"][0]["content"] == "User text"
        headers = call_args[1]["headers"]
        assert "x-api-key" in headers
        assert "anthropic-version" in headers

    @pytest.mark.asyncio
    async def test_call_with_empty_text(self):
        """When text is empty, send only prompt as user message."""
        client = ZhipuClient("fake-key", model="glm-4.7")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "content": [{"type": "text", "text": "result"}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("Just a prompt", "")

        call_args = client._client.post.call_args
        payload = call_args[1]["json"]
        assert "system" not in payload
        assert payload["messages"][0]["content"] == "Just a prompt"

    @pytest.mark.asyncio
    async def test_api_key_in_header_not_url(self):
        """API key must be in x-api-key header, not in URL."""
        client = ZhipuClient("secret-key-123", model="glm-4.7")
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {
            "content": [{"type": "text", "text": "ok"}]
        }
        client._client = AsyncMock()
        client._client.post = AsyncMock(return_value=mock_resp)

        await client.call("test", "data")

        call_args = client._client.post.call_args
        url = call_args[0][0]
        assert "secret-key-123" not in url
        headers = call_args[1]["headers"]
        assert headers["x-api-key"] == "secret-key-123"

    def test_model_param(self):
        """ZhipuClient accepts model param."""
        client = ZhipuClient("fake-key", model="glm-5")
        assert client._model == "glm-5"
        assert "glm-5" in client.name

    def test_default_model(self):
        """Default model is glm-4.7."""
        client = ZhipuClient("fake-key")
        assert client._model == "glm-4.7"
