"""Tests for Ollama adapter.

All tests use mocked HTTP responses — no real Ollama instance required.
"""

import pytest
from unittest.mock import MagicMock, patch, PropertyMock

from pulse.message import PulseMessage
from pulse.adapter import AdapterError, AdapterConnectionError

from pulse_ollama import OllamaAdapter


# --- Mock Helpers ---


def make_mock_response(
    content="Hello from Llama!",
    model="llama3.2",
    prompt_eval_count=10,
    eval_count=8,
    total_duration=500000000,
):
    """Create a mock requests.Response for Ollama /api/chat."""
    mock = MagicMock()
    mock.status_code = 200
    mock.raise_for_status = MagicMock()
    mock.json.return_value = {
        "model": model,
        "message": {"role": "assistant", "content": content},
        "done": True,
        "prompt_eval_count": prompt_eval_count,
        "eval_count": eval_count,
        "total_duration": total_duration,
    }
    return mock


def make_tags_response(models=None):
    """Create a mock response for /api/tags (model list)."""
    mock = MagicMock()
    mock.status_code = 200
    mock.raise_for_status = MagicMock()
    if models is None:
        models = [
            {"name": "llama3.2:latest"},
            {"name": "mistral:latest"},
            {"name": "phi3:latest"},
        ]
    mock.json.return_value = {"models": models}
    return mock


# --- Fixtures ---


@pytest.fixture
def adapter():
    """Create OllamaAdapter with mocked HTTP session."""
    a = OllamaAdapter(model="llama3.2")
    a._session = MagicMock()
    a.connected = True
    # Default: tags returns OK, chat returns OK
    a._session.get.return_value = make_tags_response()
    a._session.post.return_value = make_mock_response()
    return a


@pytest.fixture
def query_message():
    return PulseMessage(
        action="ACT.QUERY.DATA",
        parameters={"query": "What is PULSE Protocol?"},
        sender="test-agent",
    )


@pytest.fixture
def sentiment_message():
    return PulseMessage(
        action="ACT.ANALYZE.SENTIMENT",
        parameters={"text": "I love this local AI!"},
        sender="test-agent",
    )


@pytest.fixture
def create_message():
    return PulseMessage(
        action="ACT.CREATE.TEXT",
        parameters={"instructions": "Write a haiku about open source AI"},
        sender="test-agent",
    )


@pytest.fixture
def translate_message():
    return PulseMessage(
        action="ACT.TRANSFORM.TRANSLATE",
        parameters={"text": "Hello, world!", "target_language": "German"},
        sender="test-agent",
    )


# --- Test Initialization ---


class TestOllamaAdapterInit:

    def test_default_init(self):
        adapter = OllamaAdapter()
        assert adapter.name == "ollama"
        assert adapter.default_model == "llama3.2"
        assert adapter.base_url == "http://localhost:11434"
        assert adapter.timeout == 120
        assert adapter.connected is False

    def test_custom_model(self):
        adapter = OllamaAdapter(model="mistral")
        assert adapter.default_model == "mistral"

    def test_custom_host(self):
        adapter = OllamaAdapter(host="http://192.168.1.100:11434")
        assert adapter.base_url == "http://192.168.1.100:11434"

    def test_custom_timeout(self):
        adapter = OllamaAdapter(timeout=300)
        assert adapter.timeout == 300

    def test_repr(self):
        adapter = OllamaAdapter(model="phi3")
        r = repr(adapter)
        assert "phi3" in r
        assert "localhost" in r
        assert "False" in r


# --- Test Connection ---


class TestOllamaConnection:

    @patch("pulse_ollama.adapter._requests.Session")
    def test_connect_success(self, mock_session_cls):
        mock_session = MagicMock()
        mock_session_cls.return_value = mock_session
        mock_session.get.return_value = make_tags_response()

        adapter = OllamaAdapter()
        adapter.connect()
        assert adapter.connected is True
        mock_session.get.assert_called_once_with(
            "http://localhost:11434/api/tags", timeout=5
        )

    @patch("pulse_ollama.adapter._requests.Session")
    def test_connect_fails_if_ollama_not_running(self, mock_session_cls):
        import requests as req
        mock_session = MagicMock()
        mock_session_cls.return_value = mock_session
        mock_session.get.side_effect = req.exceptions.ConnectionError("refused")

        adapter = OllamaAdapter()
        with pytest.raises(AdapterConnectionError, match="ollama serve"):
            adapter.connect()
        assert adapter.connected is False

    @patch("pulse_ollama.adapter._requests.Session")
    def test_connect_fails_on_timeout(self, mock_session_cls):
        import requests as req
        mock_session = MagicMock()
        mock_session_cls.return_value = mock_session
        mock_session.get.side_effect = req.exceptions.Timeout()

        adapter = OllamaAdapter()
        with pytest.raises(AdapterConnectionError, match="did not respond"):
            adapter.connect()

    def test_disconnect(self, adapter):
        adapter.disconnect()
        assert adapter.connected is False
        assert adapter._session is None


# --- Test list_models ---


class TestListModels:

    def test_list_models(self, adapter):
        models = adapter.list_models()
        assert "llama3.2:latest" in models
        assert "mistral:latest" in models
        assert len(models) == 3

    def test_list_models_empty(self, adapter):
        adapter._session.get.return_value = make_tags_response(models=[])
        models = adapter.list_models()
        assert models == []

    def test_list_models_auto_connects(self):
        adapter = OllamaAdapter()
        with patch("pulse_ollama.adapter._requests.Session") as mock_cls:
            mock_session = MagicMock()
            mock_cls.return_value = mock_session
            mock_session.get.return_value = make_tags_response()
            models = adapter.list_models()
            assert adapter.connected is True


# --- Test to_native ---


class TestToNative:

    def test_query_message(self, adapter, query_message):
        native = adapter.to_native(query_message)
        assert native["model"] == "llama3.2"
        assert native["stream"] is False
        messages = native["messages"]
        assert messages[-1]["role"] == "user"
        assert "What is PULSE Protocol?" in messages[-1]["content"]

    def test_sentiment_has_system_prompt(self, adapter, sentiment_message):
        native = adapter.to_native(sentiment_message)
        roles = [m["role"] for m in native["messages"]]
        assert "system" in roles
        system_content = next(m["content"] for m in native["messages"] if m["role"] == "system")
        assert "sentiment" in system_content.lower()

    def test_query_no_system_prompt(self, adapter, query_message):
        native = adapter.to_native(query_message)
        roles = [m["role"] for m in native["messages"]]
        assert "system" not in roles

    def test_translate_includes_language(self, adapter, translate_message):
        native = adapter.to_native(translate_message)
        user_content = next(m["content"] for m in native["messages"] if m["role"] == "user")
        assert "German" in user_content

    def test_custom_model_override(self, adapter):
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"query": "test", "model": "mistral"},
        )
        native = adapter.to_native(msg)
        assert native["model"] == "mistral"

    def test_temperature_in_options(self, adapter):
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"query": "test", "temperature": 0.8},
        )
        native = adapter.to_native(msg)
        assert native["options"]["temperature"] == 0.8

    def test_custom_system_prompt(self, adapter):
        msg = PulseMessage(
            action="ACT.ANALYZE.SENTIMENT",
            parameters={"text": "hello", "system_prompt": "You are a critic."},
        )
        native = adapter.to_native(msg)
        system_content = next(m["content"] for m in native["messages"] if m["role"] == "system")
        assert system_content == "You are a critic."

    def test_missing_content_raises(self, adapter):
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"temperature": 0.5},
        )
        with pytest.raises(AdapterError, match="Missing content parameter"):
            adapter.to_native(msg)

    def test_instructions_parameter(self, adapter, create_message):
        native = adapter.to_native(create_message)
        user_content = next(m["content"] for m in native["messages"] if m["role"] == "user")
        assert "haiku" in user_content

    def test_data_parameter(self, adapter):
        msg = PulseMessage(
            action="ACT.ANALYZE.PATTERN",
            parameters={"data": "[1, 2, 3, 4, 5]"},
        )
        native = adapter.to_native(msg)
        user_content = next(m["content"] for m in native["messages"] if m["role"] == "user")
        assert "1, 2, 3" in user_content


# --- Test call_api ---


class TestCallAPI:

    def test_successful_call(self, adapter):
        adapter._session.post.return_value = make_mock_response(
            content="PULSE is a semantic protocol.",
            prompt_eval_count=15,
            eval_count=8,
        )
        result = adapter.call_api({
            "model": "llama3.2",
            "messages": [{"role": "user", "content": "What is PULSE?"}],
            "stream": False,
        })
        assert result["content"] == "PULSE is a semantic protocol."
        assert result["usage"]["input_tokens"] == 15
        assert result["usage"]["output_tokens"] == 8
        assert result["usage"]["total_tokens"] == 23
        assert result["done"] is True

    def test_model_not_found_raises(self, adapter):
        mock_r = MagicMock()
        mock_r.status_code = 404
        mock_r.raise_for_status = MagicMock()
        adapter._session.post.return_value = mock_r

        with pytest.raises(AdapterError, match="ollama pull"):
            adapter.call_api({
                "model": "nonexistent-model",
                "messages": [{"role": "user", "content": "test"}],
                "stream": False,
            })

    def test_timeout_raises(self, adapter):
        import requests as req
        adapter._session.post.side_effect = req.exceptions.Timeout()

        with pytest.raises(AdapterError, match="timed out"):
            adapter.call_api({
                "model": "llama3.2",
                "messages": [{"role": "user", "content": "test"}],
                "stream": False,
            })

    def test_connection_error_raises(self, adapter):
        import requests as req
        adapter._session.post.side_effect = req.exceptions.ConnectionError("refused")

        with pytest.raises(AdapterConnectionError, match="Lost connection"):
            adapter.call_api({
                "model": "llama3.2",
                "messages": [{"role": "user", "content": "test"}],
                "stream": False,
            })

    def test_auto_connect(self):
        adapter = OllamaAdapter()
        with patch("pulse_ollama.adapter._requests.Session") as mock_cls:
            mock_session = MagicMock()
            mock_cls.return_value = mock_session
            mock_session.get.return_value = make_tags_response()
            mock_session.post.return_value = make_mock_response(content="hello")

            result = adapter.call_api({
                "model": "llama3.2",
                "messages": [{"role": "user", "content": "test"}],
                "stream": False,
            })
            assert adapter.connected is True
            assert result["content"] == "hello"


# --- Test from_native ---


class TestFromNative:

    def test_response_conversion(self, adapter):
        native = {
            "content": "42 is the answer.",
            "model": "llama3.2",
            "usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
            "done": True,
            "total_duration_ns": 1200000000,
        }
        response = adapter.from_native(native)
        assert isinstance(response, PulseMessage)
        assert response.content["parameters"]["result"] == "42 is the answer."
        assert response.content["parameters"]["model"] == "llama3.2"
        assert response.content["parameters"]["done"] is True
        assert response.content["parameters"]["total_duration_ns"] == 1200000000


# --- Test Full Pipeline ---


class TestFullPipeline:

    def test_query_pipeline(self, adapter, query_message):
        adapter._session.post.return_value = make_mock_response(
            content="PULSE is a semantic protocol for AI communication.",
        )
        response = adapter.send(query_message)

        assert isinstance(response, PulseMessage)
        assert response.type == "RESPONSE"
        assert response.envelope["receiver"] == "test-agent"
        assert response.envelope["sender"] == "adapter:ollama"
        assert "PULSE is a semantic" in response.content["parameters"]["result"]

    def test_sentiment_pipeline(self, adapter, sentiment_message):
        adapter._session.post.return_value = make_mock_response(
            content='{"sentiment": "positive", "confidence": 0.97}',
        )
        response = adapter.send(sentiment_message)
        assert response.type == "RESPONSE"
        assert "positive" in response.content["parameters"]["result"]

    def test_pipeline_tracks_requests(self, adapter, query_message):
        adapter.send(query_message)
        adapter.send(query_message)
        assert adapter._request_count == 2
        assert adapter._error_count == 0

    def test_pipeline_error_tracking(self, adapter, query_message):
        import requests as req
        adapter._session.post.side_effect = req.exceptions.Timeout()
        with pytest.raises(AdapterError):
            adapter.send(query_message)
        assert adapter._error_count == 1


# --- Test Supported Actions ---


class TestSupportedActions:

    def test_supported_actions_list(self, adapter):
        actions = adapter.supported_actions
        assert len(actions) == 6
        assert "ACT.QUERY.DATA" in actions
        assert "ACT.ANALYZE.SENTIMENT" in actions
        assert "ACT.TRANSFORM.TRANSLATE" in actions

    def test_supports_check(self, adapter):
        assert adapter.supports("ACT.QUERY.DATA") is True
        assert adapter.supports("ACT.TRANSACT.REQUEST") is False


# --- Test Provider Switching (key selling point) ---


class TestProviderSwitching:
    """Prove that switching between cloud and local is trivial."""

    def test_same_interface_as_openai(self):
        """OllamaAdapter has same supported_actions as OpenAIAdapter."""
        pytest.importorskip("pulse_openai", reason="pulse_openai not installed")
        from pulse_openai import OpenAIAdapter

        openai = OpenAIAdapter(api_key="sk-test")
        ollama = OllamaAdapter(model="llama3.2")

        assert set(openai.supported_actions) == set(ollama.supported_actions)

    def test_same_interface_as_anthropic(self):
        """OllamaAdapter has same supported_actions as AnthropicAdapter."""
        pytest.importorskip("pulse_anthropic", reason="pulse_anthropic not installed")
        from pulse_anthropic import AnthropicAdapter

        anthropic = AnthropicAdapter(api_key="sk-ant-test")
        ollama = OllamaAdapter(model="llama3.2")

        assert set(anthropic.supported_actions) == set(ollama.supported_actions)

    def test_same_message_works(self, adapter, query_message):
        """Same PULSE message works regardless of provider."""
        response = adapter.send(query_message)
        assert response.type == "RESPONSE"
        result = response.content["parameters"]["result"]
        assert isinstance(result, str)
        assert len(result) > 0


# --- Test Unique Ollama Features ---


class TestOllamaSpecificFeatures:

    def test_no_api_key_needed(self):
        """OllamaAdapter works without any API key."""
        adapter = OllamaAdapter()
        assert not hasattr(adapter, "_api_key") or adapter._api_key is None  # type: ignore

    def test_local_host_default(self):
        """Default host is localhost — data stays on machine."""
        adapter = OllamaAdapter()
        assert "localhost" in adapter.base_url

    def test_remote_host_supported(self):
        """Can point to a remote Ollama server for shared deployments."""
        adapter = OllamaAdapter(host="http://ai-server.company.com:11434")
        assert "ai-server.company.com" in adapter.base_url

    def test_list_models_method_exists(self, adapter):
        """Unique feature: can query installed models."""
        models = adapter.list_models()
        assert isinstance(models, list)
        assert all(isinstance(m, str) for m in models)
