import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agency_swarm.tools.utils import from_openapi_schema, validate_openapi_spec


@pytest.fixture
def base_spec():
    return {"servers": [{"url": "https://api.example.com"}], "paths": {}}


@pytest.fixture
def mock_tool_setup():
    with patch("agency_swarm.tools.utils.FunctionTool") as mock_func:
        tool = MagicMock()
        mock_func.return_value = tool
        yield mock_func, tool


class TestFromOpenAPISchema:
    def test_basic_schema_conversion(self, base_spec, mock_tool_setup):
        mock_func, tool = mock_tool_setup
        base_spec["paths"]["/users"] = {"get": {"operationId": "getUsers", "description": "Get users"}}

        tools = from_openapi_schema(base_spec)
        assert len(tools) == 1 and tools[0] == tool

        args = mock_func.call_args.kwargs
        assert args["name"] == "getUsers" and args["description"] == "Get users"

    def test_string_input_with_body(self, base_spec, mock_tool_setup):
        mock_func, _ = mock_tool_setup
        base_spec["paths"]["/test"] = {
            "post": {
                "operationId": "create",
                "description": "Create",
                "requestBody": {"content": {"application/json": {"schema": {"type": "object"}}}},
            }
        }

        from_openapi_schema(json.dumps(base_spec))
        schema = mock_func.call_args.kwargs["params_json_schema"]
        assert "requestBody" in schema["properties"]

    @pytest.mark.parametrize("strict,expected", [(True, True), (False, False)])
    def test_strict_mode(self, base_spec, mock_tool_setup, strict, expected):
        mock_func, _ = mock_tool_setup
        base_spec["paths"]["/test"] = {"get": {"operationId": "test", "description": "Test"}}

        with patch("agency_swarm.tools.utils.ensure_strict_json_schema"):
            from_openapi_schema(base_spec, strict=strict)
            assert mock_func.call_args.kwargs["strict_json_schema"] == expected

    def test_parameter_handling(self, base_spec, mock_tool_setup):
        mock_func, _ = mock_tool_setup
        base_spec["paths"]["/test"] = {
            "get": {
                "operationId": "test",
                "description": "Test",
                "parameters": [
                    {"name": "legacy", "type": "string", "required": False},
                    {"name": "new", "schema": {"type": "integer"}, "required": True},
                ],
            }
        }

        from_openapi_schema(base_spec)
        schema = mock_func.call_args.kwargs["params_json_schema"]
        params = schema["properties"]["parameters"]

        assert "legacy" in params["properties"] and "new" in params["properties"]
        assert "new" in params["required"] and "legacy" not in params["required"]

    @pytest.mark.asyncio
    async def test_invoke_get_request(self, base_spec, mock_tool_setup):
        mock_func, _ = mock_tool_setup
        base_spec["paths"]["/users/{id}"] = {
            "get": {
                "operationId": "getUser",
                "description": "Get user",
                "parameters": [{"name": "id", "schema": {"type": "string"}, "required": True}],
            }
        }

        with patch("agency_swarm.tools.utils.httpx.AsyncClient") as mock_client_cls:
            client = AsyncMock()
            response = MagicMock()
            response.json.return_value = {"id": "123"}
            client.request.return_value = response
            mock_client_cls.return_value.__aenter__.return_value = client

            from_openapi_schema(base_spec)
            invoke_func = mock_func.call_args.kwargs["on_invoke_tool"]

            result = await invoke_func(MagicMock(), json.dumps({"parameters": {"id": "123"}}))

            client.request.assert_called_once_with(
                "GET", "https://api.example.com/users/123", params={}, json=None, headers={}
            )
            assert result == {"id": "123"}

    @pytest.mark.asyncio
    async def test_invoke_post_request(self, base_spec, mock_tool_setup):
        mock_func, _ = mock_tool_setup
        base_spec["paths"]["/users"] = {
            "post": {
                "operationId": "createUser",
                "description": "Create user",
                "requestBody": {"content": {"application/json": {"schema": {"type": "object"}}}},
            }
        }

        with patch("agency_swarm.tools.utils.httpx.AsyncClient") as mock_client_cls:
            client = AsyncMock()
            mock_response = MagicMock()
            mock_response.json.return_value = {"id": "456"}
            client.request.return_value = mock_response
            mock_client_cls.return_value.__aenter__.return_value = client

            from_openapi_schema(base_spec)
            invoke_func = mock_func.call_args.kwargs["on_invoke_tool"]

            await invoke_func(MagicMock(), json.dumps({"requestBody": {"name": "test"}}))

            client.request.assert_called_once_with(
                "POST", "https://api.example.com/users", params={}, json={"name": "test"}, headers={}
            )

    @pytest.mark.asyncio
    async def test_non_json_response(self, base_spec, mock_tool_setup):
        mock_func, _ = mock_tool_setup
        base_spec["paths"]["/text"] = {"get": {"operationId": "getText", "description": "Get text"}}

        with patch("agency_swarm.tools.utils.httpx.AsyncClient") as mock_client_cls:
            client = AsyncMock()
            response = MagicMock()
            response.json.side_effect = Exception("Not JSON")
            response.text = "plain text"
            client.request.return_value = response
            mock_client_cls.return_value.__aenter__.return_value = client

            from_openapi_schema(base_spec)
            invoke_func = mock_func.call_args.kwargs["on_invoke_tool"]

            result = await invoke_func(MagicMock(), json.dumps({"parameters": {}}))
            assert result == "plain text"

    def test_multiple_operations(self, base_spec, mock_tool_setup):
        mock_func, _ = mock_tool_setup
        base_spec["paths"] = {
            "/users": {
                "get": {"operationId": "getUsers", "description": "Get users"},
                "post": {"operationId": "createUser", "description": "Create user"},
            },
            "/posts": {"get": {"operationId": "getPosts", "description": "Get posts"}},
        }

        tools = from_openapi_schema(base_spec)
        assert len(tools) == 3 and mock_func.call_count == 3


class TestValidateOpenAPISpec:
    @pytest.mark.parametrize(
        "spec,should_pass",
        [
            ({"paths": {"/users": {"get": {"operationId": "getUsers", "description": "Get users"}}}}, True),
            ({"info": {"title": "API"}}, False),  # Missing paths
            ({"paths": {"/users": "invalid"}}, False),  # Invalid path item
            ({"paths": {"/users": {"get": {"description": "Get users"}}}}, False),  # Missing operationId
            ({"paths": {"/users": {"get": {"operationId": "getUsers"}}}}, False),  # Missing description
        ],
    )
    def test_validation(self, spec, should_pass):
        if should_pass:
            result = validate_openapi_spec(json.dumps(spec))
            assert result == spec
        else:
            with pytest.raises(ValueError):
                validate_openapi_spec(json.dumps(spec))
