import unittest
from unittest.mock import AsyncMock, patch

import httpx

from src.snowglobe.tools.api import (
    create_tool_executions_batch_async,
    create_tool_executions_batch_sync,
)
from src.snowglobe.tools.models import ToolCall


def _response(
    status_code: int,
    *,
    json_body=None,
    text_body: str = "",
) -> httpx.Response:
    request = httpx.Request("POST", "https://example.com/api/tool-executions")
    if json_body is not None:
        return httpx.Response(status_code, request=request, json=json_body)
    return httpx.Response(status_code, request=request, text=text_body)


class TestCreateToolExecutionsBatchSync(unittest.TestCase):
    @patch("src.snowglobe.tools.api.build_auth_headers", return_value={})
    @patch(
        "src.snowglobe.tools.api.get_registered_tool_specs",
        return_value=[
            {
                "tool_name": "my_tool",
                "description": "Test tool",
                "input_schema": {"type": "object", "properties": {}},
            }
        ],
    )
    @patch("src.snowglobe.tools.api._sync_request_with_retries")
    def test_retries_without_tool_specs_after_5xx(
        self,
        mock_request_with_retries,
        _mock_get_registered_tool_specs,
        _mock_build_auth_headers,
    ):
        mock_request_with_retries.side_effect = [
            _response(500, text_body="server boom"),
            _response(
                200,
                json_body={
                    "tool_executions": [{"id": "exec-1", "tool_name": "my_tool"}],
                    "timestamp": "2026-03-04T00:00:00Z",
                },
            ),
        ]

        result = create_tool_executions_batch_sync(
            test_id="test-123",
            application_id="app-123",
            tool_calls=[ToolCall(tool_name="my_tool", arguments={"a": 1})],
        )

        self.assertIsNotNone(result)
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0].id, "exec-1")
        self.assertEqual(mock_request_with_retries.call_count, 2)
        first_payload = mock_request_with_retries.call_args_list[0].kwargs["json"]
        second_payload = mock_request_with_retries.call_args_list[1].kwargs["json"]
        self.assertIn("tool_specs", first_payload)
        self.assertNotIn("tool_specs", second_payload)

    @patch("src.snowglobe.tools.api.build_auth_headers", return_value={})
    @patch("src.snowglobe.tools.api.get_registered_tool_specs", return_value=[])
    @patch("src.snowglobe.tools.api._sync_request_with_retries")
    def test_does_not_retry_when_no_tool_specs(
        self,
        mock_request_with_retries,
        _mock_get_registered_tool_specs,
        _mock_build_auth_headers,
    ):
        mock_request_with_retries.return_value = _response(500, text_body="server boom")

        with self.assertRaises(httpx.HTTPStatusError):
            create_tool_executions_batch_sync(
                test_id="test-123",
                application_id="app-123",
                tool_calls=[ToolCall(tool_name="my_tool", arguments={"a": 1})],
            )

        self.assertEqual(mock_request_with_retries.call_count, 1)
        payload = mock_request_with_retries.call_args.kwargs["json"]
        self.assertNotIn("tool_specs", payload)

    @patch("src.snowglobe.tools.api.build_auth_headers", return_value={})
    @patch(
        "src.snowglobe.tools.api.get_registered_tool_specs",
        return_value=[
            {
                "tool_name": "my_tool",
                "description": "Test tool",
                "input_schema": {"type": "object", "properties": {}},
            }
        ],
    )
    @patch("src.snowglobe.tools.api._sync_request_with_retries")
    def test_returns_none_on_404(
        self,
        mock_request_with_retries,
        _mock_get_registered_tool_specs,
        _mock_build_auth_headers,
    ):
        mock_request_with_retries.return_value = _response(404, text_body="not found")

        result = create_tool_executions_batch_sync(
            test_id="test-123",
            application_id="app-123",
            tool_calls=[ToolCall(tool_name="my_tool", arguments={"a": 1})],
        )

        self.assertIsNone(result)
        self.assertEqual(mock_request_with_retries.call_count, 1)


class TestCreateToolExecutionsBatchAsync(unittest.IsolatedAsyncioTestCase):
    @patch("src.snowglobe.tools.api.build_auth_headers", return_value={})
    @patch(
        "src.snowglobe.tools.api.get_registered_tool_specs",
        return_value=[
            {
                "tool_name": "my_tool",
                "description": "Test tool",
                "input_schema": {"type": "object", "properties": {}},
            }
        ],
    )
    @patch(
        "src.snowglobe.tools.api._async_request_with_retries", new_callable=AsyncMock
    )
    async def test_retries_without_tool_specs_after_5xx(
        self,
        mock_request_with_retries,
        _mock_get_registered_tool_specs,
        _mock_build_auth_headers,
    ):
        mock_request_with_retries.side_effect = [
            _response(500, text_body="server boom"),
            _response(
                200,
                json_body={
                    "tool_executions": [{"id": "exec-1", "tool_name": "my_tool"}],
                    "timestamp": "2026-03-04T00:00:00Z",
                },
            ),
        ]

        result = await create_tool_executions_batch_async(
            test_id="test-123",
            application_id="app-123",
            tool_calls=[ToolCall(tool_name="my_tool", arguments={"a": 1})],
            client=AsyncMock(),
        )

        self.assertIsNotNone(result)
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0].id, "exec-1")
        self.assertEqual(mock_request_with_retries.await_count, 2)
        first_payload = mock_request_with_retries.await_args_list[0].kwargs["json"]
        second_payload = mock_request_with_retries.await_args_list[1].kwargs["json"]
        self.assertIn("tool_specs", first_payload)
        self.assertNotIn("tool_specs", second_payload)
