"""Tests for OpenAI adapter.

All tests use mocked OpenAI responses — no real API calls.
"""

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

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

from pulse_openai import OpenAIAdapter


# --- Mock Helpers ---


def make_mock_response(content="Hello!", model="gpt-4o-mini", prompt_tokens=10,
                       completion_tokens=5, finish_reason="stop"):
    """Create a mock OpenAI ChatCompletion response."""
    mock = MagicMock()
    mock.choices = [MagicMock()]
    mock.choices[0].message.content = content
    mock.choices[0].finish_reason = finish_reason
    mock.model = model
    mock.usage.prompt_tokens = prompt_tokens
    mock.usage.completion_tokens = completion_tokens
    mock.usage.total_tokens = prompt_tokens + completion_tokens
    return mock


# --- Fixtures ---


@pytest.fixture
def adapter():
    """Create an OpenAIAdapter with mocked client."""
    a = OpenAIAdapter(api_key="sk-test-key")
    a._client = MagicMock()
    a.connected = True
    return a


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


@pytest.fixture
def sentiment_message():
    """Create a sentiment analysis message."""
    return PulseMessage(
        action="ACT.ANALYZE.SENTIMENT",
        parameters={"text": "I absolutely love this product!"},
        sender="test-agent",
    )


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


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


@pytest.fixture
def summarize_message():
    """Create a summarization message."""
    return PulseMessage(
        action="ACT.TRANSFORM.SUMMARIZE",
        parameters={"text": "Long article text here..."},
        sender="test-agent",
    )


@pytest.fixture
def pattern_message():
    """Create a pattern analysis message."""
    return PulseMessage(
        action="ACT.ANALYZE.PATTERN",
        parameters={"data": "1, 2, 4, 8, 16, 32"},
        sender="test-agent",
    )


# --- Test Initialization ---


class TestOpenAIAdapterInit:
    """Test adapter initialization."""

    def test_basic_init(self):
        """Test creation with API key."""
        adapter = OpenAIAdapter(api_key="sk-test")
        assert adapter.name == "openai"
        assert adapter.default_model == "gpt-4o-mini"
        assert adapter.connected is False

    def test_custom_model(self):
        """Test creation with custom model."""
        adapter = OpenAIAdapter(api_key="sk-test", model="gpt-4o")
        assert adapter.default_model == "gpt-4o"

    def test_custom_base_url(self):
        """Test creation with custom base URL."""
        adapter = OpenAIAdapter(api_key="sk-test", base_url="https://my-proxy.com")
        assert adapter.base_url == "https://my-proxy.com"

    def test_repr(self):
        """Test string representation."""
        adapter = OpenAIAdapter(api_key="sk-test")
        r = repr(adapter)
        assert "gpt-4o-mini" in r
        assert "False" in r


# --- Test Connection ---


class TestOpenAIConnection:
    """Test connect/disconnect."""

    def test_connect_without_key_raises(self):
        """Test that connect without API key raises error."""
        adapter = OpenAIAdapter()
        with pytest.raises(AdapterConnectionError, match="API key required"):
            adapter.connect()

    @patch("pulse_openai.adapter.OpenAI")
    def test_connect_creates_client(self, mock_openai_cls):
        """Test that connect creates OpenAI client."""
        adapter = OpenAIAdapter(api_key="sk-test")
        adapter.connect()
        assert adapter.connected is True
        assert adapter._client is not None

    def test_disconnect(self, adapter):
        """Test disconnect clears client."""
        adapter.disconnect()
        assert adapter.connected is False
        assert adapter._client is None


# --- Test to_native ---


class TestToNative:
    """Test PULSE → OpenAI conversion."""

    def test_query_message(self, adapter, query_message):
        """Test converting a query to OpenAI format."""
        native = adapter.to_native(query_message)

        assert native["model"] == "gpt-4o-mini"
        assert len(native["messages"]) == 1
        assert native["messages"][0]["role"] == "user"
        assert "What is PULSE Protocol?" in native["messages"][0]["content"]

    def test_sentiment_message(self, adapter, sentiment_message):
        """Test sentiment analysis includes system prompt."""
        native = adapter.to_native(sentiment_message)

        assert len(native["messages"]) == 2
        assert native["messages"][0]["role"] == "system"
        assert "sentiment" in native["messages"][0]["content"].lower()
        assert native["messages"][1]["content"] == "I absolutely love this product!"

    def test_create_message(self, adapter, create_message):
        """Test text generation conversion."""
        native = adapter.to_native(create_message)

        assert native["messages"][-1]["content"] == "Write a haiku about AI"
        assert native["model"] == "gpt-4o"  # Default for CREATE.TEXT

    def test_translate_message(self, adapter, translate_message):
        """Test translation includes target language."""
        native = adapter.to_native(translate_message)

        user_content = native["messages"][-1]["content"]
        assert "Spanish" in user_content
        assert "Hello, world!" in user_content

    def test_summarize_message(self, adapter, summarize_message):
        """Test summarization conversion."""
        native = adapter.to_native(summarize_message)

        assert len(native["messages"]) == 2
        assert native["messages"][0]["role"] == "system"
        assert "summarize" in native["messages"][0]["content"].lower()

    def test_pattern_message(self, adapter, pattern_message):
        """Test pattern analysis conversion."""
        native = adapter.to_native(pattern_message)

        user_content = native["messages"][-1]["content"]
        assert "1, 2, 4, 8, 16, 32" in user_content

    def test_custom_model_override(self, adapter):
        """Test that model can be overridden in parameters."""
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"query": "test", "model": "gpt-4o"},
        )
        native = adapter.to_native(msg)
        assert native["model"] == "gpt-4o"

    def test_temperature_passed(self, adapter):
        """Test that temperature is forwarded."""
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"query": "test", "temperature": 0.7},
        )
        native = adapter.to_native(msg)
        assert native["temperature"] == 0.7

    def test_max_tokens_passed(self, adapter):
        """Test that max_tokens is forwarded."""
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"query": "test", "max_tokens": 500},
        )
        native = adapter.to_native(msg)
        assert native["max_tokens"] == 500

    def test_custom_system_prompt(self, adapter):
        """Test custom system prompt overrides default."""
        msg = PulseMessage(
            action="ACT.ANALYZE.SENTIMENT",
            parameters={"text": "hello", "system_prompt": "You are a pirate."},
        )
        native = adapter.to_native(msg)
        assert native["messages"][0]["content"] == "You are a pirate."

    def test_missing_content_raises(self, adapter):
        """Test error when no content parameter provided."""
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"temperature": 0.5},  # No query, text, etc.
        )
        with pytest.raises(AdapterError, match="Missing content parameter"):
            adapter.to_native(msg)

    def test_prompt_fallback(self, adapter):
        """Test 'prompt' as fallback content parameter."""
        msg = PulseMessage(
            action="ACT.QUERY.DATA",
            parameters={"prompt": "Tell me a joke"},
        )
        native = adapter.to_native(msg)
        assert native["messages"][-1]["content"] == "Tell me a joke"


# --- Test call_api ---


class TestCallAPI:
    """Test OpenAI API calls (mocked)."""

    def test_successful_call(self, adapter):
        """Test successful API call returns parsed response."""
        mock_response = make_mock_response(
            content="PULSE is a semantic protocol.",
            model="gpt-4o-mini",
            prompt_tokens=15,
            completion_tokens=8,
        )
        adapter._client.chat.completions.create.return_value = mock_response

        result = adapter.call_api({
            "model": "gpt-4o-mini",
            "messages": [{"role": "user", "content": "What is PULSE?"}],
        })

        assert result["content"] == "PULSE is a semantic protocol."
        assert result["model"] == "gpt-4o-mini"
        assert result["usage"]["prompt_tokens"] == 15
        assert result["usage"]["completion_tokens"] == 8
        assert result["usage"]["total_tokens"] == 23
        assert result["finish_reason"] == "stop"

    def test_auto_connect_on_call(self):
        """Test that call_api auto-connects if not connected."""
        adapter = OpenAIAdapter(api_key="sk-test")

        with patch("pulse_openai.adapter.OpenAI") as mock_cls:
            mock_client = MagicMock()
            mock_cls.return_value = mock_client
            mock_client.chat.completions.create.return_value = make_mock_response()

            result = adapter.call_api({
                "model": "gpt-4o-mini",
                "messages": [{"role": "user", "content": "test"}],
            })

            assert adapter.connected is True
            assert result["content"] == "Hello!"

    def test_auth_error(self, adapter):
        """Test authentication error handling."""
        from openai import AuthenticationError

        mock_resp = MagicMock()
        mock_resp.status_code = 401
        adapter._client.chat.completions.create.side_effect = AuthenticationError(
            message="Invalid API key",
            response=mock_resp,
            body=None,
        )

        with pytest.raises(AdapterError, match="authentication failed"):
            adapter.call_api({"model": "gpt-4o-mini", "messages": []})

    def test_rate_limit_error(self, adapter):
        """Test rate limit error handling."""
        from openai import RateLimitError

        mock_resp = MagicMock()
        mock_resp.status_code = 429
        adapter._client.chat.completions.create.side_effect = RateLimitError(
            message="Rate limit exceeded",
            response=mock_resp,
            body=None,
        )

        with pytest.raises(AdapterError, match="rate limit"):
            adapter.call_api({"model": "gpt-4o-mini", "messages": []})

    def test_connection_error(self, adapter):
        """Test connection error handling."""
        from openai import APIConnectionError

        adapter._client.chat.completions.create.side_effect = APIConnectionError(
            request=MagicMock(),
        )

        with pytest.raises(AdapterConnectionError, match="Cannot reach"):
            adapter.call_api({"model": "gpt-4o-mini", "messages": []})


# --- Test from_native ---


class TestFromNative:
    """Test OpenAI → PULSE conversion."""

    def test_response_conversion(self, adapter):
        """Test converting OpenAI response to PULSE message."""
        native = {
            "content": "The answer is 42.",
            "model": "gpt-4o-mini",
            "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
            "finish_reason": "stop",
        }

        response = adapter.from_native(native)

        assert isinstance(response, PulseMessage)
        assert response.content["parameters"]["result"] == "The answer is 42."
        assert response.content["parameters"]["model"] == "gpt-4o-mini"
        assert response.content["parameters"]["usage"]["total_tokens"] == 15
        assert response.content["parameters"]["finish_reason"] == "stop"


# --- Test Full Pipeline ---


class TestFullPipeline:
    """Test complete send pipeline with mocked API."""

    def test_query_pipeline(self, adapter, query_message):
        """Test full query: PULSE → OpenAI → PULSE."""
        adapter._client.chat.completions.create.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:openai"
        assert "PULSE is a semantic" in response.content["parameters"]["result"]

    def test_sentiment_pipeline(self, adapter, sentiment_message):
        """Test sentiment analysis pipeline."""
        adapter._client.chat.completions.create.return_value = make_mock_response(
            content='{"sentiment": "positive", "confidence": 0.95, "explanation": "Strong positive language"}',
        )

        response = adapter.send(sentiment_message)

        assert response.type == "RESPONSE"
        assert "positive" in response.content["parameters"]["result"]

    def test_create_pipeline(self, adapter, create_message):
        """Test text generation pipeline."""
        adapter._client.chat.completions.create.return_value = make_mock_response(
            content="Silicon minds think\nPatterns emerge from chaos\nNew dawn breaks in code",
        )

        response = adapter.send(create_message)

        assert response.type == "RESPONSE"
        assert "Silicon minds" in response.content["parameters"]["result"]

    def test_translate_pipeline(self, adapter, translate_message):
        """Test translation pipeline."""
        adapter._client.chat.completions.create.return_value = make_mock_response(
            content="¡Hola, mundo!",
        )

        response = adapter.send(translate_message)
        assert "¡Hola, mundo!" in response.content["parameters"]["result"]

    def test_pipeline_tracks_requests(self, adapter, query_message):
        """Test that pipeline increments counters."""
        adapter._client.chat.completions.create.return_value = make_mock_response()

        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):
        """Test that errors are tracked."""
        from openai import AuthenticationError

        mock_resp = MagicMock()
        mock_resp.status_code = 401
        adapter._client.chat.completions.create.side_effect = AuthenticationError(
            message="bad key",
            response=mock_resp,
            body=None,
        )

        with pytest.raises(AdapterError):
            adapter.send(query_message)

        assert adapter._error_count == 1
        assert adapter._request_count == 1


# --- Test Supported Actions ---


class TestSupportedActions:
    """Test action support."""

    def test_supported_actions_list(self, adapter):
        """Test listing supported actions."""
        actions = adapter.supported_actions
        assert "ACT.QUERY.DATA" in actions
        assert "ACT.CREATE.TEXT" in actions
        assert "ACT.ANALYZE.SENTIMENT" in actions
        assert "ACT.ANALYZE.PATTERN" in actions
        assert "ACT.TRANSFORM.TRANSLATE" in actions
        assert "ACT.TRANSFORM.SUMMARIZE" in actions
        assert len(actions) == 6

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


# --- Test Health Check ---


class TestHealthCheck:
    """Test health check with OpenAI adapter."""

    def test_health_check(self, adapter, query_message):
        """Test health check after requests."""
        adapter._client.chat.completions.create.return_value = make_mock_response()
        adapter.send(query_message)

        status = adapter.health_check()
        assert status["adapter"] == "openai"
        assert status["connected"] is True
        assert status["requests"] == 1
        assert status["errors"] == 0
        assert status["error_rate"] == 0.0
