"""Unit tests for DataAPIClient.

This module contains comprehensive tests for the Data API client,
including authentication, tool search, and tool execution.
"""

from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from hezor_common.data_model.searching.data.api_models import (
    ExecuteResponse,
    SearchResponse,
    ToolSchema,
)
from hezor_common.transfer.datahub_sdk.base.constants import (
    DEFAULT_DATA_API_BASE_URL,
)
from hezor_common.transfer.datahub_sdk.base.data_api_client import (
    DataAPIClient,
)
from hezor_common.transfer.datahub_sdk.base.meta_info import MetaInfo


@pytest.fixture
def mock_meta_info():
    """Create a mock MetaInfo object."""
    return MetaInfo(
        subject="测试主体",
        subject_code="test_001",
        caller_id="user_123",
        data_coverage="202401-202412",
        creation_slug="test_model",
        creation_name="测试模型",
    )


@pytest.fixture
def mock_search_response():
    """Create a mock SearchResponse."""
    from hezor_common.data_model.searching.data.api_models import ParameterSchema

    tool_schema = ToolSchema(
        name="test_tool",
        desc="Test tool description",
        params=ParameterSchema(type="object", desc="Test parameters"),
        returns="Test return value",
    )
    return SearchResponse(tools=[tool_schema])


@pytest.fixture
def mock_execute_response():
    """Create a mock ExecuteResponse."""
    return ExecuteResponse(data={"result": "success", "value": 42})


class TestDataAPIClientInitialization:
    """Tests for DataAPIClient initialization."""

    def test_init_with_defaults(self):
        """Test initialization with default parameters."""
        client = DataAPIClient()
        assert client.base_url == DEFAULT_DATA_API_BASE_URL.rstrip("/")
        assert client.timeout == 120.0
        assert client.api_key == "test-api-key"  # DEFAULT_DATA_API_KEY
        assert client.meta_info is None
        assert client.private_key_path is None
        assert client.password is None
        assert client.meta_info_expires_in == 3600

    def test_init_with_custom_params(self, mock_meta_info):
        """Test initialization with custom parameters."""
        client = DataAPIClient(
            base_url="http://custom.api.com",
            timeout=60.0,
            api_key="test-key",
            meta_info=mock_meta_info,
            private_key_path="test_key.pem",
            password=b"password",
            meta_info_expires_in=7200,
        )
        assert client.base_url == "http://custom.api.com"
        assert client.timeout == 60.0
        assert client.api_key == "test-key"
        assert client.meta_info == mock_meta_info
        assert client.private_key_path == "test_key.pem"
        assert client.password == b"password"
        assert client.meta_info_expires_in == 7200

    def test_init_strips_trailing_slash(self):
        """Test that trailing slash is stripped from base_url."""
        client = DataAPIClient(base_url="http://api.com/")
        assert client.base_url == "http://api.com"


class TestDataAPIClientContextManager:
    """Tests for async context manager functionality."""

    @pytest.mark.asyncio
    async def test_context_manager_creates_client(self):
        """Test that async context manager creates HTTP client."""
        async with DataAPIClient() as client:
            assert client._client is not None
            assert isinstance(client._client, httpx.AsyncClient)

    @pytest.mark.asyncio
    async def test_context_manager_closes_client(self):
        """Test that async context manager closes HTTP client."""
        client = DataAPIClient()
        async with client:
            pass
        assert client._client is None

    def test_client_property_raises_without_initialization(self):
        """Test that client property raises error when not initialized."""
        client = DataAPIClient()
        with pytest.raises(RuntimeError, match="Client not initialized"):
            _ = client.client


class TestDataAPIClientHeaders:
    """Tests for request header generation."""

    def test_get_headers_default(self):
        """Test default headers without authentication."""
        client = DataAPIClient(api_key=None)  # Explicitly set to None
        headers = client._get_headers()

        assert headers["Content-Type"] == "application/json"
        assert "Authorization" not in headers
        assert "X-META-INFO" not in headers

    def test_get_headers_with_api_key(self):
        """Test headers with API key."""
        client = DataAPIClient(api_key="test-api-key")
        headers = client._get_headers()

        assert headers["Content-Type"] == "application/json"
        assert headers["Authorization"] == "Bearer test-api-key"
        assert "X-META-INFO" not in headers

    @patch("hezor_common.transfer.datahub_sdk.base.meta_info.MetaInfo.to_request_header")
    def test_get_headers_with_meta_info(self, mock_to_request_header, mock_meta_info):
        """Test headers with MetaInfo."""
        mock_to_request_header.return_value = {"X-META-INFO": "test-jwt-token"}

        client = DataAPIClient(
            meta_info=mock_meta_info,
            private_key_path="test_key.pem",
            password=b"password",
            meta_info_expires_in=7200,
        )
        headers = client._get_headers()

        assert headers["Content-Type"] == "application/json"
        assert headers["X-META-INFO"] == "test-jwt-token"
        mock_to_request_header.assert_called_once_with(
            private_key_path="test_key.pem",
            password=b"password",
            expires_in=7200,
        )

    def test_get_headers_raises_without_private_key(self, mock_meta_info):
        """Test that ValueError is raised when meta_info provided without private_key_path."""
        client = DataAPIClient(meta_info=mock_meta_info)

        with pytest.raises(ValueError, match="private_key_path is required"):
            client._get_headers()

    @patch("hezor_common.transfer.datahub_sdk.base.meta_info.MetaInfo.to_request_header")
    def test_get_headers_with_all_auth(self, mock_to_request_header, mock_meta_info):
        """Test headers with both API key and MetaInfo."""
        mock_to_request_header.return_value = {"X-META-INFO": "test-jwt-token"}

        client = DataAPIClient(
            api_key="test-api-key",
            meta_info=mock_meta_info,
            private_key_path="test_key.pem",
        )
        headers = client._get_headers()

        assert headers["Content-Type"] == "application/json"
        assert headers["Authorization"] == "Bearer test-api-key"
        assert headers["X-META-INFO"] == "test-jwt-token"


class TestDataAPIClientSearchTools:
    """Tests for search_tools method."""

    @pytest.mark.asyncio
    async def test_search_tools_success(self, mock_search_response):
        """Test successful tool search."""
        mock_response = MagicMock()
        mock_response.json.return_value = mock_search_response.model_dump()

        async with DataAPIClient() as client:
            client._client.get = AsyncMock(return_value=mock_response)  # type: ignore[union-attr]

            result = await client.search_tools("test query", top_k=5)

            assert isinstance(result, SearchResponse)
            assert len(result.tools) == 1
            assert result.tools[0].name == "test_tool"

            client._client.get.assert_called_once()  # type: ignore[union-attr]
            call_args = client._client.get.call_args  # type: ignore[union-attr]
            assert call_args.args[0] == f"{DEFAULT_DATA_API_BASE_URL}/api/search"
            assert call_args.kwargs["params"] == {"query": "test query", "top_k": 5}

    @pytest.mark.asyncio
    async def test_search_tools_with_headers(self, mock_search_response):
        """Test tool search includes authentication headers."""
        mock_response = MagicMock()
        mock_response.json.return_value = mock_search_response.model_dump()

        async with DataAPIClient(api_key="test-key") as client:
            client._client.get = AsyncMock(return_value=mock_response)  # type: ignore[union-attr]

            await client.search_tools("test query")

            call_args = client._client.get.call_args  # type: ignore[union-attr]
            headers = call_args.kwargs["headers"]
            assert headers["Authorization"] == "Bearer test-key"

    @pytest.mark.asyncio
    async def test_search_tools_http_error(self):
        """Test tool search handles HTTP errors."""
        async with DataAPIClient() as client:
            client._client.get = AsyncMock(  # type: ignore[union-attr]
                side_effect=httpx.HTTPStatusError(
                    "Error", request=MagicMock(), response=MagicMock()
                )
            )

            with pytest.raises(httpx.HTTPStatusError):
                await client.search_tools("test query")

    @pytest.mark.asyncio
    async def test_search_tools_invalid_response(self):
        """Test tool search handles invalid response format."""
        mock_response = MagicMock()
        mock_response.json.return_value = {"invalid": "data"}

        async with DataAPIClient() as client:
            client._client.get = AsyncMock(return_value=mock_response)  # type: ignore[union-attr]

            with pytest.raises(ValueError, match="Invalid search response format"):
                await client.search_tools("test query")


class TestDataAPIClientExecuteTool:
    """Tests for execute_tool method."""

    @pytest.mark.asyncio
    async def test_execute_tool_success(self, mock_execute_response):
        """Test successful tool execution."""
        mock_response = MagicMock()
        mock_response.json.return_value = mock_execute_response.model_dump()

        async with DataAPIClient() as client:
            client._client.post = AsyncMock(return_value=mock_response)  # type: ignore[union-attr]

            result = await client.execute_tool("test_tool", {"arg1": "value1", "arg2": 42})

            assert isinstance(result, ExecuteResponse)
            assert result.data == {"result": "success", "value": 42}

            client._client.post.assert_called_once()  # type: ignore[union-attr]
            call_args = client._client.post.call_args  # type: ignore[union-attr]
            assert call_args.args[0] == f"{DEFAULT_DATA_API_BASE_URL}/api/execute"

            request_json = call_args.kwargs["json"]
            assert request_json["tool"] == "test_tool"
            assert request_json["args"] == {"arg1": "value1", "arg2": 42}

    @pytest.mark.asyncio
    async def test_execute_tool_with_headers(self, mock_execute_response):
        """Test tool execution includes authentication headers."""
        mock_response = MagicMock()
        mock_response.json.return_value = mock_execute_response.model_dump()

        async with DataAPIClient(api_key="test-key") as client:
            client._client.post = AsyncMock(return_value=mock_response)  # type: ignore[union-attr]

            await client.execute_tool("test_tool", {})

            call_args = client._client.post.call_args  # type: ignore[union-attr]
            headers = call_args.kwargs["headers"]
            assert headers["Authorization"] == "Bearer test-key"

    @pytest.mark.asyncio
    async def test_execute_tool_http_error(self):
        """Test tool execution handles HTTP errors."""
        async with DataAPIClient() as client:
            client._client.post = AsyncMock(  # type: ignore[union-attr]
                side_effect=httpx.HTTPStatusError(
                    "Error", request=MagicMock(), response=MagicMock()
                )
            )

            with pytest.raises(httpx.HTTPStatusError):
                await client.execute_tool("test_tool", {})

    @pytest.mark.asyncio
    async def test_execute_tool_invalid_response(self):
        """Test tool execution handles invalid response format."""
        mock_response = MagicMock()
        # ExecuteResponse requires specific fields, not just any dict
        mock_response.json.return_value = "not a dict"

        async with DataAPIClient() as client:
            client._client.post = AsyncMock(return_value=mock_response)  # type: ignore[union-attr]

            with pytest.raises(ValueError, match="Invalid execution response format"):
                await client.execute_tool("test_tool", {})


class TestDataAPIClientExecuteToolFromJson:
    """Tests for execute_tool_from_json method."""

    @pytest.mark.asyncio
    async def test_execute_tool_from_json_success(self, mock_execute_response):
        """Test successful tool execution from JSON."""
        mock_response = MagicMock()
        mock_response.json.return_value = mock_execute_response.model_dump()

        request_json = '{"tool": "test_tool", "args": {"arg1": "value1"}}'

        async with DataAPIClient() as client:
            client._client.post = AsyncMock(return_value=mock_response)  # type: ignore[union-attr]

            result = await client.execute_tool_from_json(request_json)

            assert isinstance(result, ExecuteResponse)
            assert result.data == {"result": "success", "value": 42}

            client._client.post.assert_called_once()  # type: ignore[union-attr]

    @pytest.mark.asyncio
    async def test_execute_tool_from_json_invalid_json(self):
        """Test tool execution handles invalid JSON."""
        async with DataAPIClient() as client:
            with pytest.raises(ValueError, match="Invalid JSON format"):
                await client.execute_tool_from_json("invalid json")

    @pytest.mark.asyncio
    async def test_execute_tool_from_json_http_error(self):
        """Test tool execution from JSON handles HTTP errors."""
        request_json = '{"tool": "test_tool", "args": {}}'

        async with DataAPIClient() as client:
            client._client.post = AsyncMock(  # type: ignore[union-attr]
                side_effect=httpx.HTTPStatusError(
                    "Error", request=MagicMock(), response=MagicMock()
                )
            )

            with pytest.raises(httpx.HTTPStatusError):
                await client.execute_tool_from_json(request_json)


class TestDataAPIClientIntegration:
    """Integration tests combining multiple features."""

    @pytest.mark.asyncio
    async def test_full_workflow_with_authentication(
        self, mock_meta_info, mock_search_response, mock_execute_response
    ):
        """Test complete workflow with authentication."""
        search_mock = MagicMock()
        search_mock.json.return_value = mock_search_response.model_dump()

        execute_mock = MagicMock()
        execute_mock.json.return_value = mock_execute_response.model_dump()

        with patch(
            "hezor_common.transfer.datahub_sdk.base.meta_info.MetaInfo.to_request_header"
        ) as mock_header:
            mock_header.return_value = {"X-META-INFO": "test-jwt"}

            async with DataAPIClient(
                api_key="test-key",
                meta_info=mock_meta_info,
                private_key_path="test_key.pem",
            ) as client:
                client._client.get = AsyncMock(return_value=search_mock)  # type: ignore[union-attr]
                client._client.post = AsyncMock(return_value=execute_mock)  # type: ignore[union-attr]

                # Search tools
                search_result = await client.search_tools("test query")
                assert len(search_result.tools) == 1

                # Execute tool
                execute_result = await client.execute_tool(
                    search_result.tools[0].name, {"param": "value"}
                )
                assert isinstance(execute_result.data, dict)
                assert execute_result.data["result"] == "success"

                # Verify both calls included authentication
                for call in [client._client.get, client._client.post]:  # type: ignore[union-attr]
                    headers = call.call_args.kwargs["headers"]  # type: ignore[union-attr]
                    assert headers["Authorization"] == "Bearer test-key"
                    assert headers["X-META-INFO"] == "test-jwt"
