"""Unit tests for ha_call_event tool."""

from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest
from fastmcp.exceptions import ToolError

from ha_mcp.client.rest_client import HomeAssistantConnectionError
from ha_mcp.tools.tools_service import ServiceTools


def _make_tools(fire_event_return: dict | Exception) -> ServiceTools:
    client = MagicMock()
    if isinstance(fire_event_return, Exception):
        client.fire_event = AsyncMock(side_effect=fire_event_return)
    else:
        client.fire_event = AsyncMock(return_value=fire_event_return)
    tools = ServiceTools.__new__(ServiceTools)
    tools._client = client
    tools._device_tools = MagicMock()
    return tools


class TestHaCallEvent:
    async def test_fires_event_with_no_data(self):
        tools = _make_tools({"message": "Event my_event fired."})
        result = await tools.ha_call_event("my_event")
        assert result["success"] is True
        assert result["event_type"] == "my_event"
        assert "fired" in result["message"]
        tools._client.fire_event.assert_called_once_with("my_event", None)

    async def test_fires_event_with_dict_data(self):
        tools = _make_tools({"message": "Event custom_event fired."})
        result = await tools.ha_call_event("custom_event", {"key": "value"})
        assert result["success"] is True
        assert result["event_type"] == "custom_event"
        tools._client.fire_event.assert_called_once_with(
            "custom_event", {"key": "value"}
        )

    async def test_fires_event_with_json_string_data(self):
        tools = _make_tools({"message": "Event my_event fired."})
        result = await tools.ha_call_event("my_event", '{"temperature": 22}')
        assert result["success"] is True
        assert result["event_type"] == "my_event"
        tools._client.fire_event.assert_called_once_with(
            "my_event", {"temperature": 22}
        )

    async def test_raises_tool_error_on_list_data(self):
        tools = _make_tools({"message": "ok"})
        with pytest.raises(ToolError):
            await tools.ha_call_event("my_event", "[1, 2, 3]")

    async def test_raises_tool_error_on_invalid_json_string(self):
        """Invalid JSON raises ToolError via the ValueError → invalid_json path."""
        tools = _make_tools({"message": "ok"})
        with pytest.raises(ToolError):
            await tools.ha_call_event("my_event", "{not valid json")

    async def test_returns_fallback_message_when_response_empty(self):
        tools = _make_tools({})
        result = await tools.ha_call_event("my_event")
        assert result["success"] is True
        assert result["event_type"] == "my_event"
        assert "my_event" in result["message"]

    async def test_propagates_connection_error_as_tool_error(self):
        tools = _make_tools(ConnectionError("HA unreachable"))
        with pytest.raises(ToolError):
            await tools.ha_call_event("my_event")

    async def test_event_type_passed_to_client(self):
        tools = _make_tools({"message": "Event zone_entered fired."})
        await tools.ha_call_event("zone_entered", {"zone": "home"})
        call_args = tools._client.fire_event.call_args
        assert call_args[0][0] == "zone_entered"
        assert call_args[0][1] == {"zone": "home"}

    async def test_raises_tool_error_on_empty_event_type(self):
        """Empty event_type is rejected before hitting the wire."""
        tools = _make_tools({"message": "ok"})
        with pytest.raises(ToolError):
            await tools.ha_call_event("")

    async def test_raises_tool_error_on_whitespace_event_type(self):
        """Whitespace-only event_type is rejected before hitting the wire."""
        tools = _make_tools({"message": "ok"})
        with pytest.raises(ToolError):
            await tools.ha_call_event("   ")

    async def test_raises_tool_error_on_event_type_with_slash(self):
        """event_type containing a slash produces a malformed URL — rejected early."""
        tools = _make_tools({"message": "ok"})
        with pytest.raises(ToolError):
            await tools.ha_call_event("bad/event")

    async def test_reraises_tool_error_from_client(self):
        """ToolError from fire_event propagates unchanged (not swallowed by except Exception)."""
        tools = _make_tools(ToolError("already structured"))
        with pytest.raises(ToolError, match="already structured"):
            await tools.ha_call_event("my_event")

    async def test_timeout_returns_partial_success_with_warning(self):
        """Connection-timeout from fire_event surfaces as partial-success with a top-level warnings list.

        Pins the post-#1332 contract on the timeout branch (`tools_service.py:495+`):
        the migrated `warnings: list[str]` shape replaces the legacy singular
        `warning` string and must contain the dispatched-may-still-deliver note.
        """
        conn_error = HomeAssistantConnectionError("connection timed out")
        conn_error.__cause__ = httpx.TimeoutException("Read timed out")
        tools = _make_tools(conn_error)

        result = await tools.ha_call_event("my_event")

        assert result["success"] is True
        assert result["partial"] is True
        assert result["event_type"] == "my_event"
        warnings = result.get("warnings")
        assert isinstance(warnings, list) and warnings, (
            f"Expected non-empty warnings list, got: {result!r}"
        )
        assert any("timed out" in w.lower() for w in warnings), (
            f"Expected timeout-related warning content; got: {warnings!r}"
        )
