"""Unit tests for the Chat resource and models"""

import json
import pytest
from unittest.mock import MagicMock, patch
from pydantic import ValidationError

from octen import Octen
from octen.models import (
    ChatMessage,
    WebSearchOptions,
    ResponseFormat,
    Tool,
    ToolFunction,
    ChatCompletion,
    StreamEvent,
    Usage,
)
from octen.core.exceptions import (
    OctenAuthenticationError,
    OctenRateLimitError,
    OctenAPIError,
    OctenStreamError,
    OctenValidationError,
)


# -- Fixtures ------------------------------------------------------------------

@pytest.fixture
def client():
    """Create a client with a dummy API key."""
    return Octen(api_key="octen-test-key")


@pytest.fixture
def mock_http_client():
    """Create a mock HTTPClient."""
    return MagicMock()


@pytest.fixture
def sample_completion_response():
    """A minimal valid non-streaming response payload."""
    return {
        "id": "chatcmpl-abc123",
        "model": "openai/gpt-5.4",
        "created": 1710000000,
        "object": "chat.completion",
        "choices": [
            {
                "index": 0,
                "finish_reason": "stop",
                "message": {
                    "role": "assistant",
                    "content": "Hello! How can I help you today?",
                },
            }
        ],
        "meta": {
            "usage": {
                "prompt_tokens": 10,
                "completion_tokens": 8,
                "total_tokens": 18,
                "num_search_queries": 0,
            }
        },
    }


@pytest.fixture
def sample_search_response():
    """A response payload that includes search results."""
    return {
        "id": "chatcmpl-search123",
        "model": "openai/gpt-5.4",
        "created": 1710000001,
        "object": "chat.completion",
        "choices": [
            {
                "index": 0,
                "finish_reason": "stop",
                "message": {"role": "assistant", "content": "Here are today's highlights..."},
            }
        ],
        "search_results": [
            {
                "query": "tech news today",
                "results": [
                    {
                        "title": "AI breakthrough announced",
                        "url": "https://example.com/news/1",
                        "highlight": "Researchers announced a major AI breakthrough...",
                    },
                    {
                        "title": "New Python release",
                        "url": "https://example.com/news/2",
                        "highlight": "Python 3.14 released with new features...",
                    },
                ],
            }
        ],
        "meta": {
            "usage": {
                "prompt_tokens": 200,
                "completion_tokens": 50,
                "total_tokens": 250,
                "num_search_queries": 1,
                "full_content_tokens": 0,
            }
        },
        "citations": "1,2",
    }


# -- Model validation tests ----------------------------------------------------

class TestChatMessage:
    def test_valid_user_message(self):
        msg = ChatMessage(role="user", content="Hello")
        assert msg.role == "user"
        assert msg.content == "Hello"

    def test_valid_system_message(self):
        msg = ChatMessage(role="system", content="You are helpful")
        assert msg.role == "system"

    def test_multimodal_content(self):
        content = [
            {"type": "text", "text": "Describe this image"},
            {"type": "image_url", "image_url": {"url": "https://example.com/img.png"}},
        ]
        msg = ChatMessage(role="user", content=content)
        assert isinstance(msg.content, list)

    def test_tool_message(self):
        msg = ChatMessage(role="tool", content='{"result": "ok"}', tool_call_id="call_abc")
        assert msg.tool_call_id == "call_abc"


class TestWebSearchOptions:
    def test_defaults(self):
        opts = WebSearchOptions()
        assert opts.safesearch == "off"

    def test_valid_safesearch(self):
        opts = WebSearchOptions(safesearch="strict")
        assert opts.safesearch == "strict"

    def test_invalid_safesearch(self):
        with pytest.raises(ValidationError):
            WebSearchOptions(safesearch="medium")

    def test_full_options(self):
        from octen.models.chat import ChatFullContentOptions
        opts = WebSearchOptions(
            safesearch="off",
            count=10,
            country="CN",
            include_domains=["example.com"],
            full_content=ChatFullContentOptions(enable=True, max_tokens=500),
        )
        assert opts.count == 10
        assert opts.full_content.enable is True


class TestResponseFormat:
    def test_text_format(self):
        fmt = ResponseFormat(type="text")
        assert fmt.type == "text"

    def test_json_object_format(self):
        fmt = ResponseFormat(type="json_object")
        assert fmt.type == "json_object"

    def test_invalid_type(self):
        with pytest.raises(ValidationError):
            ResponseFormat(type="xml")


class TestChatCompletion:
    def test_parse_basic(self, sample_completion_response):
        completion = ChatCompletion(**sample_completion_response)
        assert completion.id == "chatcmpl-abc123"
        assert completion.model == "openai/gpt-5.4"
        assert len(completion.choices) == 1
        assert completion.choices[0].message.content == "Hello! How can I help you today?"

    def test_text_property(self, sample_completion_response):
        completion = ChatCompletion(**sample_completion_response)
        assert completion.text == "Hello! How can I help you today?"

    def test_usage_property(self, sample_completion_response):
        completion = ChatCompletion(**sample_completion_response)
        assert completion.usage is not None
        assert completion.usage.total_tokens == 18
        assert completion.usage.prompt_tokens == 10
        assert completion.usage.completion_tokens == 8

    def test_search_results(self, sample_search_response):
        completion = ChatCompletion(**sample_search_response)
        assert completion.search_results is not None
        assert len(completion.search_results) == 1
        assert completion.search_results[0].query == "tech news today"
        assert len(completion.search_results[0].results) == 2
        assert completion.citations == "1,2"

    def test_no_usage_when_meta_missing(self):
        response = {
            "id": "x",
            "model": "openai/gpt-5.4",
            "created": 1234,
            "choices": [{"index": 0, "message": {"role": "assistant", "content": "hi"}}],
        }
        completion = ChatCompletion(**response)
        assert completion.usage is None


class TestStreamEvent:
    def test_content_event(self):
        event = StreamEvent(**{
            "type": "content",
            "id": "evt1",
            "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": None}],
        })
        assert event.type == "content"
        assert event.choices[0].delta.content == "Hello"

    def test_search_done_event(self):
        event = StreamEvent(**{
            "type": "search_done",
            "search_results": [{"query": "test", "results": []}],
            "citations": "1",
        })
        assert event.type == "search_done"
        assert event.citations == "1"

    def test_usage_event(self):
        event = StreamEvent(**{
            "type": "usage",
            "meta": {"usage": {"prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150}},
        })
        assert event.usage is not None
        assert event.usage.total_tokens == 150

    def test_error_event(self):
        event = StreamEvent(**{
            "type": "error",
            "error": {"message": "unsupported model: foo/bar", "code": 400},
        })
        assert event.type == "error"
        assert event.error.code == 400
        assert "unsupported model" in event.error.message


# -- Client initialisation tests -----------------------------------------------

class TestClientInit:
    def test_empty_api_key_raises(self):
        with pytest.raises(ValueError, match="api_key cannot be empty"):
            Octen(api_key="")

    def test_repr(self, client):
        assert "Octen" in repr(client)

    def test_context_manager(self):
        with Octen(api_key="octen-test-key") as c:
            assert c is not None

    def test_chat_resource_accessible(self, client):
        assert hasattr(client, "chat")
        from octen.resources.chat import ChatResource
        assert isinstance(client.chat, ChatResource)


# -- Chat resource integration tests -------------------------------------------

class TestChatCreate:
    def test_non_streaming_success(self, mock_http_client, sample_completion_response):
        mock_http_client.request.return_value = sample_completion_response

        with Octen(api_key="octen-test-key") as client:
            client._http_client = mock_http_client
            client.chat._client = mock_http_client

            response = client.chat.create(
                model="openai/gpt-5.4",
                messages=[ChatMessage(role="user", content="Hello")],
                web_search="off",
            )

        assert isinstance(response, ChatCompletion)
        assert response.text == "Hello! How can I help you today?"
        mock_http_client.request.assert_called_once()

    def test_streaming_returns_stream(self, mock_http_client):
        from octen.core.streaming import Stream
        mock_stream = MagicMock(spec=Stream)
        mock_http_client.stream_request.return_value = mock_stream

        with Octen(api_key="octen-test-key") as client:
            client._http_client = mock_http_client
            client.chat._client = mock_http_client

            result = client.chat.create(
                model="openai/gpt-5.4",
                messages=[ChatMessage(role="user", content="Hello")],
                stream=True,
            )

        assert result is mock_stream
        mock_http_client.stream_request.assert_called_once()

    def test_dict_messages_accepted(self, mock_http_client, sample_completion_response):
        mock_http_client.request.return_value = sample_completion_response

        with Octen(api_key="octen-test-key") as client:
            client._http_client = mock_http_client
            client.chat._client = mock_http_client

            response = client.chat.create(
                model="openai/gpt-5.4",
                messages=[{"role": "user", "content": "Hello"}],
            )
        assert isinstance(response, ChatCompletion)

    def test_payload_includes_web_search(self, mock_http_client, sample_completion_response):
        captured = {}

        mock_http_client.request.side_effect = lambda method, endpoint, json=None, **kw: (
            captured.update({"json": json}) or sample_completion_response
        )

        with Octen(api_key="octen-test-key") as client:
            client._http_client = mock_http_client
            client.chat._client = mock_http_client

            client.chat.create(
                model="openai/gpt-5.4",
                messages=[ChatMessage(role="user", content="Hello")],
                web_search="on",
                temperature=0.7,
            )

        payload = captured.get("json", {})
        assert payload.get("web_search") == "on"
        assert payload.get("temperature") == 0.7

    def test_none_params_excluded_from_payload(self, mock_http_client, sample_completion_response):
        captured_payload = {}

        mock_http_client.request.side_effect = lambda method, endpoint, json=None, **kw: (
            captured_payload.update(json or {}) or sample_completion_response
        )

        with Octen(api_key="octen-test-key") as client:
            client._http_client = mock_http_client
            client.chat._client = mock_http_client

            client.chat.create(
                model="openai/gpt-5.4",
                messages=[ChatMessage(role="user", content="Hello")],
            )

        assert "temperature" not in captured_payload
        assert "top_p" not in captured_payload
        assert "max_tokens" not in captured_payload


# -- OctenStreamError tests ----------------------------------------------------

class TestOctenStreamError:
    def test_stream_error_attributes(self):
        err = OctenStreamError(message="model not found", code=404)
        assert err.message == "model not found"
        assert err.code == 404
        assert "model not found" in str(err)
        assert "404" in str(err)


# -- pytest-asyncio tests ------------------------------------------------------

@pytest.mark.asyncio
async def test_async_client_init():
    from octen import AsyncOcten
    async with AsyncOcten(api_key="octen-test-key") as client:
        assert client is not None
        assert hasattr(client, "chat")
        assert "AsyncOcten" in repr(client)
