"""Tests for the sync YutoriClient."""

from unittest.mock import MagicMock, patch

import httpx
import pytest

from yutori import APIError, AuthenticationError, YutoriClient


class TestYutoriClientInit:
    def test_init_with_api_key(self):
        client = YutoriClient(api_key="yt-test-key")
        assert client._api_key == "yt-test-key"
        assert client._base_url == "https://api.yutori.com/v1"
        client.close()

    def test_init_without_api_key_raises(self, monkeypatch):
        monkeypatch.delenv("YUTORI_API_KEY", raising=False)
        monkeypatch.setattr("yutori.auth.credentials.load_config", lambda: None)
        with pytest.raises(AuthenticationError):
            YutoriClient(api_key="")

    def test_init_with_none_api_key_raises(self, monkeypatch):
        monkeypatch.delenv("YUTORI_API_KEY", raising=False)
        monkeypatch.setattr("yutori.auth.credentials.load_config", lambda: None)
        with pytest.raises(AuthenticationError):
            YutoriClient(api_key=None)

    def test_init_from_env_var(self, monkeypatch):
        monkeypatch.setenv("YUTORI_API_KEY", "yt-env-key")
        client = YutoriClient()
        assert client._api_key == "yt-env-key"
        client.close()

    def test_init_with_custom_base_url(self):
        client = YutoriClient(api_key="yt-test", base_url="https://custom.api.com/v1/")
        assert client._base_url == "https://custom.api.com/v1"
        client.close()

    def test_context_manager(self):
        with YutoriClient(api_key="yt-test") as client:
            assert client._api_key == "yt-test"


class TestYutoriClientGetUsage:
    USAGE_RESPONSE = {
        "num_active_scouts": 2,
        "active_scout_ids": ["id-1", "id-2"],
        "rate_limits": {
            "requests_today": 100,
            "daily_limit": 10000,
            "remaining_requests": 9900,
            "reset_at": "2026-03-04T00:00:00+00:00",
            "status": "available",
        },
        "n1_rate_limits": {
            "requests_today": 50,
            "daily_limit": 50000,
            "remaining_requests": 49950,
            "reset_at": "2026-03-04T00:00:00+00:00",
            "per_second_limit": 20,
        },
        "activity": {
            "period": "24h",
            "scout_runs": 10,
            "browsing_tasks": 3,
            "research_tasks": 2,
            "n1_calls": 50,
        },
    }

    def _mock_usage_response(self, period: str = "24h"):
        import json

        data = {**self.USAGE_RESPONSE, "activity": {**self.USAGE_RESPONSE["activity"], "period": period}}
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = json.dumps(data).encode()
        mock_response.json.return_value = data
        return mock_response

    def test_get_usage_success(self):
        with patch.object(httpx.Client, "get", return_value=self._mock_usage_response()):
            client = YutoriClient(api_key="yt-test")
            result = client.get_usage()
            assert result["num_active_scouts"] == 2
            assert result["activity"]["period"] == "24h"
            assert result["rate_limits"]["status"] == "available"
            client.close()

    def test_get_usage_with_period(self):
        with patch.object(httpx.Client, "get", return_value=self._mock_usage_response("7d")) as mock_get:
            client = YutoriClient(api_key="yt-test")
            result = client.get_usage(period="7d")
            assert result["activity"]["period"] == "7d"
            # Verify period is passed as query param
            call_kwargs = mock_get.call_args[1]
            assert call_kwargs["params"] == {"period": "7d"}
            client.close()

    def test_get_usage_no_period_sends_no_params(self):
        with patch.object(httpx.Client, "get", return_value=self._mock_usage_response()) as mock_get:
            client = YutoriClient(api_key="yt-test")
            client.get_usage()
            call_kwargs = mock_get.call_args[1]
            assert call_kwargs["params"] == {}
            client.close()

    def test_get_usage_auth_error(self):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 401
        mock_response.text = "Unauthorized"

        with patch.object(httpx.Client, "get", return_value=mock_response):
            client = YutoriClient(api_key="yt-invalid")
            with pytest.raises(AuthenticationError):
                client.get_usage()
            client.close()

    def test_get_usage_forbidden_auth_error(self):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 403
        mock_response.text = "Forbidden"

        with patch.object(httpx.Client, "get", return_value=mock_response):
            client = YutoriClient(api_key="yt-invalid")
            with pytest.raises(AuthenticationError):
                client.get_usage()
            client.close()


class TestScoutsNamespace:
    def test_scouts_list(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"scouts": []}'
        mock_response.json.return_value = {"scouts": []}

        with patch.object(httpx.Client, "get", return_value=mock_response) as mock_get:
            result = client.scouts.list(limit=10, status="active")
            assert result == {"scouts": []}
            mock_get.assert_called_once()
            params = mock_get.call_args[1]["params"]
            assert params["page_size"] == 10
            assert params["limit"] == 10
            assert params["status"] == "active"

    def test_scouts_get(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"id": "scout-123", "query": "test"}'
        mock_response.json.return_value = {"id": "scout-123", "query": "test"}

        with patch.object(httpx.Client, "get", return_value=mock_response) as mock_get:
            result = client.scouts.get("scout-123")
            assert result["id"] == "scout-123"
            mock_get.assert_called_once()

    def test_scouts_create(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"id": "new-scout", "query": "Monitor site"}'
        mock_response.json.return_value = {"id": "new-scout", "query": "Monitor site"}

        with patch.object(httpx.Client, "post", return_value=mock_response) as mock_post:
            result = client.scouts.create(
                query="Monitor site",
                output_interval=3600,
                webhook_url="https://webhook.test",
            )
            assert result["id"] == "new-scout"
            mock_post.assert_called_once()
            call_kwargs = mock_post.call_args
            payload = call_kwargs[1]["json"]
            assert payload["query"] == "Monitor site"
            assert payload["output_interval"] == 3600
            assert payload["webhook_url"] == "https://webhook.test"

    def test_scouts_update_status(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"id": "scout-123", "status": "paused"}'
        mock_response.json.return_value = {"id": "scout-123", "status": "paused"}

        with patch.object(httpx.Client, "post", return_value=mock_response) as mock_post:
            result = client.scouts.update("scout-123", status="paused")
            assert result["status"] == "paused"
            mock_post.assert_called_once()
            assert "/pause" in mock_post.call_args[0][0]

    def test_scouts_update_fields(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"id": "scout-123", "query": "new query"}'
        mock_response.json.return_value = {"id": "scout-123", "query": "new query"}

        with patch.object(httpx.Client, "patch", return_value=mock_response) as mock_patch:
            result = client.scouts.update("scout-123", query="new query")
            assert result["query"] == "new query"
            mock_patch.assert_called_once()

    def test_scouts_update_status_and_fields_raises(self, client):
        with pytest.raises(ValueError, match="Cannot update status and other fields simultaneously"):
            client.scouts.update("scout-123", status="paused", query="new query")

    def test_scouts_delete(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b""

        with patch.object(httpx.Client, "delete", return_value=mock_response) as mock_delete:
            result = client.scouts.delete("scout-123")
            assert result == {}
            mock_delete.assert_called_once()

    def test_scouts_get_updates(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"updates": [], "cursor": null}'
        mock_response.json.return_value = {"updates": [], "cursor": None}

        with patch.object(httpx.Client, "get", return_value=mock_response):
            result = client.scouts.get_updates("scout-123", limit=5)
            assert "updates" in result


class TestBrowsingNamespace:
    def test_browsing_create(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"task_id": "task-123", "status": "queued"}'
        mock_response.json.return_value = {"task_id": "task-123", "status": "queued"}

        with patch.object(httpx.Client, "post", return_value=mock_response) as mock_post:
            result = client.browsing.create(
                task="Click the login button",
                start_url="https://example.com",
                max_steps=10,
            )
            assert result["task_id"] == "task-123"
            payload = mock_post.call_args[1]["json"]
            assert payload["task"] == "Click the login button"
            assert payload["start_url"] == "https://example.com"
            assert payload["max_steps"] == 10

    def test_browsing_get(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"task_id": "task-123", "status": "succeeded"}'
        mock_response.json.return_value = {"task_id": "task-123", "status": "succeeded"}

        with patch.object(httpx.Client, "get", return_value=mock_response):
            result = client.browsing.get("task-123")
            assert result["status"] == "succeeded"


class TestResearchNamespace:
    def test_research_create(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"task_id": "research-123", "status": "queued"}'
        mock_response.json.return_value = {"task_id": "research-123", "status": "queued"}

        with patch.object(httpx.Client, "post", return_value=mock_response) as mock_post:
            result = client.research.create(
                query="Find AI startup funding",
                user_timezone="America/Los_Angeles",
            )
            assert result["task_id"] == "research-123"
            payload = mock_post.call_args[1]["json"]
            assert payload["query"] == "Find AI startup funding"

    def test_research_get(self, client):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.content = b'{"task_id": "research-123", "status": "succeeded"}'
        mock_response.json.return_value = {"task_id": "research-123", "status": "succeeded"}

        with patch.object(httpx.Client, "get", return_value=mock_response):
            result = client.research.get("research-123")
            assert result["status"] == "succeeded"


class TestPydanticSchemaIntegration:
    """Test that Pydantic models are resolved to JSON schema dicts in payloads."""

    def _make_mock_response(self):
        mock = MagicMock(spec=httpx.Response)
        mock.status_code = 200
        mock.content = b'{"task_id": "t-1"}'
        mock.json.return_value = {"task_id": "t-1"}
        return mock

    class _FakeModel:
        @classmethod
        def model_json_schema(cls):
            return {"type": "object", "properties": {"name": {"type": "string"}}}

    def test_browsing_create_with_model_class(self, client):
        with patch.object(httpx.Client, "post", return_value=self._make_mock_response()) as mock_post:
            client.browsing.create(task="t", start_url="https://x.com", output_schema=self._FakeModel)
            payload = mock_post.call_args[1]["json"]
            assert payload["output_schema"] == {"type": "object", "properties": {"name": {"type": "string"}}}

    def test_browsing_create_with_model_instance(self, client):
        with patch.object(httpx.Client, "post", return_value=self._make_mock_response()) as mock_post:
            client.browsing.create(task="t", start_url="https://x.com", output_schema=self._FakeModel())
            payload = mock_post.call_args[1]["json"]
            assert payload["output_schema"] == {"type": "object", "properties": {"name": {"type": "string"}}}

    def test_research_create_with_model_class(self, client):
        with patch.object(httpx.Client, "post", return_value=self._make_mock_response()) as mock_post:
            client.research.create(query="q", output_schema=self._FakeModel)
            payload = mock_post.call_args[1]["json"]
            assert payload["output_schema"] == {"type": "object", "properties": {"name": {"type": "string"}}}

    def test_scouts_create_with_model_class(self, client):
        mock = self._make_mock_response()
        mock.content = b'{"id": "s-1"}'
        mock.json.return_value = {"id": "s-1"}
        with patch.object(httpx.Client, "post", return_value=mock) as mock_post:
            client.scouts.create(query="q", output_schema=self._FakeModel)
            payload = mock_post.call_args[1]["json"]
            assert payload["output_schema"] == {"type": "object", "properties": {"name": {"type": "string"}}}

    def test_scouts_update_with_model_class(self, client):
        mock = self._make_mock_response()
        mock.content = b'{"id": "s-1"}'
        mock.json.return_value = {"id": "s-1"}
        with patch.object(httpx.Client, "patch", return_value=mock) as mock_patch:
            client.scouts.update("s-1", output_schema=self._FakeModel)
            payload = mock_patch.call_args[1]["json"]
            assert payload["output_schema"] == {"type": "object", "properties": {"name": {"type": "string"}}}

    def test_scouts_update_with_model_instance(self, client):
        mock = self._make_mock_response()
        mock.content = b'{"id": "s-1"}'
        mock.json.return_value = {"id": "s-1"}
        with patch.object(httpx.Client, "patch", return_value=mock) as mock_patch:
            client.scouts.update("s-1", output_schema=self._FakeModel())
            payload = mock_patch.call_args[1]["json"]
            assert payload["output_schema"] == {"type": "object", "properties": {"name": {"type": "string"}}}


class TestChatNamespace:
    def test_chat_completions(self):
        from openai.types.chat import ChatCompletion, ChatCompletionMessage
        from openai.types.chat.chat_completion import Choice

        mock_completion = ChatCompletion(
            id="chatcmpl-123",
            choices=[
                Choice(
                    finish_reason="stop",
                    index=0,
                    message=ChatCompletionMessage(role="assistant", content="click"),
                )
            ],
            created=1234567890,
            model="n1-latest",
            object="chat.completion",
        )

        with patch("yutori._sync.chat.OpenAI") as MockOpenAI:
            mock_openai_client = MagicMock()
            mock_openai_client.chat.completions.create.return_value = mock_completion
            MockOpenAI.return_value = mock_openai_client

            client = YutoriClient(api_key="yt-test")
            result = client.chat.completions.create(
                messages=[{"role": "user", "content": "Click login"}],
                model="n1-latest",
            )
            assert result.choices[0].message.content == "click"
            mock_openai_client.chat.completions.create.assert_called_once()
            call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
            assert call_kwargs["model"] == "n1-latest"
            client.close()

    def test_n1_helper_create_trimmed_public_helper_uses_trimmed_copy(self):
        from copy import deepcopy

        from openai.types.chat import ChatCompletion, ChatCompletionMessage
        from openai.types.chat.chat_completion import Choice

        from yutori.n1 import create_trimmed
        from yutori.n1.payload import trimmed_messages_to_fit

        mock_completion = ChatCompletion(
            id="chatcmpl-123",
            choices=[
                Choice(
                    finish_reason="stop",
                    index=0,
                    message=ChatCompletionMessage(role="assistant", content="click"),
                )
            ],
            created=1234567890,
            model="n1-latest",
            object="chat.completion",
        )
        original_messages = [
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "Check the page"},
                    {"type": "image_url", "image_url": {"url": "A" * 5000}},
                ],
            },
            {
                "role": "tool",
                "content": [
                    {"type": "text", "text": "Tool output"},
                    {"type": "image_url", "image_url": {"url": "A" * 5000}},
                ],
            },
        ]
        original_snapshot = deepcopy(original_messages)

        with patch("yutori._sync.chat.OpenAI") as MockOpenAI:
            mock_openai_client = MagicMock()
            mock_openai_client.chat.completions.create.return_value = mock_completion
            MockOpenAI.return_value = mock_openai_client

            client = YutoriClient(api_key="yt-test")
            result = create_trimmed(
                client.chat.completions,
                original_messages,
                max_bytes=100,
                keep_recent=1,
            )
            assert result.choices[0].message.content == "click"
            client.close()

        call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
        sent_messages = call_kwargs["messages"]
        assert sent_messages is not original_messages
        assert sent_messages == trimmed_messages_to_fit(original_messages, max_bytes=100, keep_recent=1)[0]
        assert original_messages == original_snapshot

    def test_n1_payload_helper_supports_standard_create_pattern(self):
        from copy import deepcopy

        from openai.types.chat import ChatCompletion, ChatCompletionMessage
        from openai.types.chat.chat_completion import Choice

        from yutori.n1 import trimmed_messages_to_fit

        mock_completion = ChatCompletion(
            id="chatcmpl-123",
            choices=[
                Choice(
                    finish_reason="stop",
                    index=0,
                    message=ChatCompletionMessage(role="assistant", content="click"),
                )
            ],
            created=1234567890,
            model="n1-latest",
            object="chat.completion",
        )
        original_messages = [
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "Check the page"},
                    {"type": "image_url", "image_url": {"url": "A" * 5000}},
                ],
            },
            {
                "role": "tool",
                "content": [
                    {"type": "text", "text": "Tool output"},
                    {"type": "image_url", "image_url": {"url": "A" * 5000}},
                ],
            },
        ]
        original_snapshot = deepcopy(original_messages)
        trimmed_messages, _, _ = trimmed_messages_to_fit(original_messages, max_bytes=100, keep_recent=1)

        with patch("yutori._sync.chat.OpenAI") as MockOpenAI:
            mock_openai_client = MagicMock()
            mock_openai_client.chat.completions.create.return_value = mock_completion
            MockOpenAI.return_value = mock_openai_client

            client = YutoriClient(api_key="yt-test")
            result = client.chat.completions.create(
                model="n1-latest",
                messages=trimmed_messages,
            )
            assert result.choices[0].message.content == "click"
            client.close()

        call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
        assert call_kwargs["messages"] == trimmed_messages
        assert original_messages == original_snapshot


class TestErrorHandling:
    def test_api_error_on_400(self):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 400
        mock_response.text = "Bad request"

        with patch.object(httpx.Client, "get", return_value=mock_response):
            client = YutoriClient(api_key="yt-test")
            with pytest.raises(APIError) as exc_info:
                client.get_usage()
            assert exc_info.value.status_code == 400
            client.close()

    def test_api_error_on_500(self):
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 500
        mock_response.text = "Internal server error"

        with patch.object(httpx.Client, "get", return_value=mock_response):
            client = YutoriClient(api_key="yt-test")
            with pytest.raises(APIError) as exc_info:
                client.get_usage()
            assert exc_info.value.status_code == 500
            client.close()
