"""Tests for the Home Assistant gateway adapter.

Tests real logic: state change formatting, event filtering pipeline,
cooldown behavior, config integration, and adapter initialization.
"""

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

import pytest

from gateway.config import (
    GatewayConfig,
    Platform,
    PlatformConfig,
)
from gateway.platforms.homeassistant import (
    HomeAssistantAdapter,
    check_ha_requirements,
)


# ---------------------------------------------------------------------------
# check_ha_requirements
# ---------------------------------------------------------------------------


class TestCheckRequirements:
    def test_returns_false_without_token(self, monkeypatch):
        monkeypatch.delenv("HASS_TOKEN", raising=False)
        assert check_ha_requirements() is False

    def test_returns_true_with_token(self, monkeypatch):
        monkeypatch.setenv("HASS_TOKEN", "test-token")
        assert check_ha_requirements() is True

    @patch("gateway.platforms.homeassistant.AIOHTTP_AVAILABLE", False)
    def test_returns_false_without_aiohttp(self, monkeypatch):
        monkeypatch.setenv("HASS_TOKEN", "test-token")
        assert check_ha_requirements() is False


# ---------------------------------------------------------------------------
# _format_state_change - pure function, all domain branches
# ---------------------------------------------------------------------------


class TestFormatStateChange:
    @staticmethod
    def fmt(entity_id, old_state, new_state):
        return HomeAssistantAdapter._format_state_change(entity_id, old_state, new_state)

    def test_climate_includes_temperatures(self):
        msg = self.fmt(
            "climate.thermostat",
            {"state": "off"},
            {"state": "heat", "attributes": {
                "friendly_name": "Main Thermostat",
                "current_temperature": 21.5,
                "temperature": 23,
            }},
        )
        assert "Main Thermostat" in msg
        assert "'off'" in msg and "'heat'" in msg
        assert "21.5" in msg and "23" in msg

    def test_sensor_includes_unit(self):
        msg = self.fmt(
            "sensor.temperature",
            {"state": "22.5"},
            {"state": "25.1", "attributes": {
                "friendly_name": "Living Room Temp",
                "unit_of_measurement": "C",
            }},
        )
        assert "22.5C" in msg and "25.1C" in msg
        assert "Living Room Temp" in msg

    def test_sensor_without_unit(self):
        msg = self.fmt(
            "sensor.count",
            {"state": "5"},
            {"state": "10", "attributes": {"friendly_name": "Counter"}},
        )
        assert "5" in msg and "10" in msg

    def test_binary_sensor_on(self):
        msg = self.fmt(
            "binary_sensor.motion",
            {"state": "off"},
            {"state": "on", "attributes": {"friendly_name": "Hallway Motion"}},
        )
        assert "triggered" in msg
        assert "Hallway Motion" in msg

    def test_binary_sensor_off(self):
        msg = self.fmt(
            "binary_sensor.door",
            {"state": "on"},
            {"state": "off", "attributes": {"friendly_name": "Front Door"}},
        )
        assert "cleared" in msg

    def test_light_turned_on(self):
        msg = self.fmt(
            "light.bedroom",
            {"state": "off"},
            {"state": "on", "attributes": {"friendly_name": "Bedroom Light"}},
        )
        assert "turned on" in msg

    def test_switch_turned_off(self):
        msg = self.fmt(
            "switch.heater",
            {"state": "on"},
            {"state": "off", "attributes": {"friendly_name": "Heater"}},
        )
        assert "turned off" in msg

    def test_fan_domain_uses_light_switch_branch(self):
        msg = self.fmt(
            "fan.ceiling",
            {"state": "off"},
            {"state": "on", "attributes": {"friendly_name": "Ceiling Fan"}},
        )
        assert "turned on" in msg

    def test_alarm_panel(self):
        msg = self.fmt(
            "alarm_control_panel.home",
            {"state": "disarmed"},
            {"state": "armed_away", "attributes": {"friendly_name": "Home Alarm"}},
        )
        assert "Home Alarm" in msg
        assert "armed_away" in msg and "disarmed" in msg

    def test_generic_domain_includes_entity_id(self):
        msg = self.fmt(
            "automation.morning",
            {"state": "off"},
            {"state": "on", "attributes": {"friendly_name": "Morning Routine"}},
        )
        assert "automation.morning" in msg
        assert "Morning Routine" in msg

    def test_same_state_returns_none(self):
        assert self.fmt(
            "sensor.temp",
            {"state": "22"},
            {"state": "22", "attributes": {"friendly_name": "Temp"}},
        ) is None

    def test_empty_new_state_returns_none(self):
        assert self.fmt("light.x", {"state": "on"}, {}) is None

    def test_no_old_state_uses_unknown(self):
        msg = self.fmt(
            "light.new",
            None,
            {"state": "on", "attributes": {"friendly_name": "New Light"}},
        )
        assert msg is not None
        assert "New Light" in msg

    def test_uses_entity_id_when_no_friendly_name(self):
        msg = self.fmt(
            "sensor.unnamed",
            {"state": "1"},
            {"state": "2", "attributes": {}},
        )
        assert "sensor.unnamed" in msg


# ---------------------------------------------------------------------------
# Adapter initialization from config
# ---------------------------------------------------------------------------


class TestAdapterInit:
    def test_url_and_token_from_config_extra(self, monkeypatch):
        monkeypatch.delenv("HASS_URL", raising=False)
        monkeypatch.delenv("HASS_TOKEN", raising=False)

        config = PlatformConfig(
            enabled=True,
            token="config-token",
            extra={"url": "http://192.168.1.50:8123"},
        )
        adapter = HomeAssistantAdapter(config)
        assert adapter._hass_token == "config-token"
        assert adapter._hass_url == "http://192.168.1.50:8123"

    def test_url_fallback_to_env(self, monkeypatch):
        monkeypatch.setenv("HASS_URL", "http://env-host:8123")
        monkeypatch.setenv("HASS_TOKEN", "env-tok")

        config = PlatformConfig(enabled=True, token="env-tok")
        adapter = HomeAssistantAdapter(config)
        assert adapter._hass_url == "http://env-host:8123"

    def test_trailing_slash_stripped(self):
        config = PlatformConfig(
            enabled=True, token="t",
            extra={"url": "http://ha.local:8123/"},
        )
        adapter = HomeAssistantAdapter(config)
        assert adapter._hass_url == "http://ha.local:8123"

    def test_watch_filters_parsed(self):
        config = PlatformConfig(
            enabled=True, token="t",
            extra={
                "watch_domains": ["climate", "binary_sensor"],
                "watch_entities": ["sensor.special"],
                "ignore_entities": ["sensor.uptime", "sensor.cpu"],
                "cooldown_seconds": 120,
            },
        )
        adapter = HomeAssistantAdapter(config)
        assert adapter._watch_domains == {"climate", "binary_sensor"}
        assert adapter._watch_entities == {"sensor.special"}
        assert adapter._ignore_entities == {"sensor.uptime", "sensor.cpu"}
        assert adapter._cooldown_seconds == 120

    def test_defaults_when_no_extra(self, monkeypatch):
        monkeypatch.setenv("HASS_TOKEN", "tok")
        config = PlatformConfig(enabled=True, token="tok")
        adapter = HomeAssistantAdapter(config)
        assert adapter._watch_domains == set()
        assert adapter._watch_entities == set()
        assert adapter._ignore_entities == set()
        assert adapter._cooldown_seconds == 30


# ---------------------------------------------------------------------------
# Event filtering pipeline (_handle_ha_event)
#
# We mock handle_message (not our code, it's the base class pipeline) to
# capture the MessageEvent that _handle_ha_event produces.
# ---------------------------------------------------------------------------


def _make_adapter(**extra) -> HomeAssistantAdapter:
    config = PlatformConfig(enabled=True, token="tok", extra=extra)
    adapter = HomeAssistantAdapter(config)
    adapter.handle_message = AsyncMock()
    return adapter


def _make_event(entity_id, old_state, new_state, old_attrs=None, new_attrs=None):
    return {
        "data": {
            "entity_id": entity_id,
            "old_state": {"state": old_state, "attributes": old_attrs or {}},
            "new_state": {"state": new_state, "attributes": new_attrs or {"friendly_name": entity_id}},
        }
    }


class TestEventFilteringPipeline:
    @pytest.mark.asyncio
    async def test_ignored_entity_not_forwarded(self):
        adapter = _make_adapter(ignore_entities=["sensor.uptime"])
        await adapter._handle_ha_event(_make_event("sensor.uptime", "100", "101"))
        adapter.handle_message.assert_not_called()

    @pytest.mark.asyncio
    async def test_unwatched_domain_not_forwarded(self):
        adapter = _make_adapter(watch_domains=["climate"])
        await adapter._handle_ha_event(_make_event("light.bedroom", "off", "on"))
        adapter.handle_message.assert_not_called()

    @pytest.mark.asyncio
    async def test_watched_domain_forwarded(self):
        adapter = _make_adapter(watch_domains=["climate"], cooldown_seconds=0)
        await adapter._handle_ha_event(
            _make_event("climate.thermostat", "off", "heat",
                        new_attrs={"friendly_name": "Thermostat", "current_temperature": 20, "temperature": 22})
        )
        adapter.handle_message.assert_called_once()

        # Verify the actual MessageEvent text content
        msg_event = adapter.handle_message.call_args[0][0]
        assert "Thermostat" in msg_event.text
        assert "heat" in msg_event.text
        assert msg_event.source.platform == Platform.HOMEASSISTANT
        assert msg_event.source.chat_id == "ha_events"

    @pytest.mark.asyncio
    async def test_watched_entity_forwarded(self):
        adapter = _make_adapter(watch_entities=["sensor.important"], cooldown_seconds=0)
        await adapter._handle_ha_event(
            _make_event("sensor.important", "10", "20",
                        new_attrs={"friendly_name": "Important Sensor", "unit_of_measurement": "W"})
        )
        adapter.handle_message.assert_called_once()
        msg_event = adapter.handle_message.call_args[0][0]
        assert "10W" in msg_event.text and "20W" in msg_event.text

    @pytest.mark.asyncio
    async def test_no_filters_passes_everything(self):
        adapter = _make_adapter(cooldown_seconds=0)
        await adapter._handle_ha_event(_make_event("cover.blinds", "closed", "open"))
        adapter.handle_message.assert_called_once()

    @pytest.mark.asyncio
    async def test_same_state_not_forwarded(self):
        adapter = _make_adapter(cooldown_seconds=0)
        await adapter._handle_ha_event(_make_event("light.x", "on", "on"))
        adapter.handle_message.assert_not_called()

    @pytest.mark.asyncio
    async def test_empty_entity_id_skipped(self):
        adapter = _make_adapter()
        await adapter._handle_ha_event({"data": {"entity_id": ""}})
        adapter.handle_message.assert_not_called()

    @pytest.mark.asyncio
    async def test_message_event_has_correct_source(self):
        adapter = _make_adapter(cooldown_seconds=0)
        await adapter._handle_ha_event(
            _make_event("light.test", "off", "on",
                        new_attrs={"friendly_name": "Test Light"})
        )
        msg_event = adapter.handle_message.call_args[0][0]
        assert msg_event.source.user_name == "Home Assistant"
        assert msg_event.source.chat_type == "channel"
        assert msg_event.message_id.startswith("ha_light.test_")


# ---------------------------------------------------------------------------
# Cooldown behavior
# ---------------------------------------------------------------------------


class TestCooldown:
    @pytest.mark.asyncio
    async def test_cooldown_blocks_rapid_events(self):
        adapter = _make_adapter(cooldown_seconds=60)

        event = _make_event("sensor.temp", "20", "21",
                            new_attrs={"friendly_name": "Temp"})
        await adapter._handle_ha_event(event)
        assert adapter.handle_message.call_count == 1

        # Second event immediately after should be blocked
        event2 = _make_event("sensor.temp", "21", "22",
                             new_attrs={"friendly_name": "Temp"})
        await adapter._handle_ha_event(event2)
        assert adapter.handle_message.call_count == 1  # Still 1

    @pytest.mark.asyncio
    async def test_cooldown_expires(self):
        adapter = _make_adapter(cooldown_seconds=1)

        event = _make_event("sensor.temp", "20", "21",
                            new_attrs={"friendly_name": "Temp"})
        await adapter._handle_ha_event(event)
        assert adapter.handle_message.call_count == 1

        # Simulate time passing beyond cooldown
        adapter._last_event_time["sensor.temp"] = time.time() - 2

        event2 = _make_event("sensor.temp", "21", "22",
                             new_attrs={"friendly_name": "Temp"})
        await adapter._handle_ha_event(event2)
        assert adapter.handle_message.call_count == 2

    @pytest.mark.asyncio
    async def test_different_entities_independent_cooldowns(self):
        adapter = _make_adapter(cooldown_seconds=60)

        await adapter._handle_ha_event(
            _make_event("sensor.a", "1", "2", new_attrs={"friendly_name": "A"})
        )
        await adapter._handle_ha_event(
            _make_event("sensor.b", "3", "4", new_attrs={"friendly_name": "B"})
        )
        # Both should pass - different entities
        assert adapter.handle_message.call_count == 2

        # Same entity again - should be blocked
        await adapter._handle_ha_event(
            _make_event("sensor.a", "2", "3", new_attrs={"friendly_name": "A"})
        )
        assert adapter.handle_message.call_count == 2  # Still 2

    @pytest.mark.asyncio
    async def test_zero_cooldown_passes_all(self):
        adapter = _make_adapter(cooldown_seconds=0)

        for i in range(5):
            await adapter._handle_ha_event(
                _make_event("sensor.temp", str(i), str(i + 1),
                            new_attrs={"friendly_name": "Temp"})
            )
        assert adapter.handle_message.call_count == 5


# ---------------------------------------------------------------------------
# Config integration (env overrides, round-trip)
# ---------------------------------------------------------------------------


class TestConfigIntegration:
    def test_env_override_creates_ha_platform(self, monkeypatch):
        monkeypatch.setenv("HASS_TOKEN", "env-token")
        monkeypatch.setenv("HASS_URL", "http://10.0.0.5:8123")
        # Clear other platform tokens
        for v in ["TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN"]:
            monkeypatch.delenv(v, raising=False)

        from gateway.config import load_gateway_config
        config = load_gateway_config()

        assert Platform.HOMEASSISTANT in config.platforms
        ha = config.platforms[Platform.HOMEASSISTANT]
        assert ha.enabled is True
        assert ha.token == "env-token"
        assert ha.extra["url"] == "http://10.0.0.5:8123"

    def test_no_env_no_platform(self, monkeypatch):
        for v in ["HASS_TOKEN", "HASS_URL", "TELEGRAM_BOT_TOKEN",
                   "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN"]:
            monkeypatch.delenv(v, raising=False)

        from gateway.config import load_gateway_config
        config = load_gateway_config()
        assert Platform.HOMEASSISTANT not in config.platforms

    def test_config_roundtrip_preserves_extra(self):
        config = GatewayConfig(
            platforms={
                Platform.HOMEASSISTANT: PlatformConfig(
                    enabled=True,
                    token="tok",
                    extra={
                        "url": "http://ha:8123",
                        "watch_domains": ["climate"],
                        "cooldown_seconds": 45,
                    },
                ),
            },
        )
        d = config.to_dict()
        restored = GatewayConfig.from_dict(d)

        ha = restored.platforms[Platform.HOMEASSISTANT]
        assert ha.enabled is True
        assert ha.token == "tok"
        assert ha.extra["watch_domains"] == ["climate"]
        assert ha.extra["cooldown_seconds"] == 45

    def test_connected_platforms_includes_ha(self):
        config = GatewayConfig(
            platforms={
                Platform.HOMEASSISTANT: PlatformConfig(enabled=True, token="tok"),
                Platform.TELEGRAM: PlatformConfig(enabled=False, token="t"),
            },
        )
        connected = config.get_connected_platforms()
        assert Platform.HOMEASSISTANT in connected
        assert Platform.TELEGRAM not in connected


# ---------------------------------------------------------------------------
# send() via REST API
# ---------------------------------------------------------------------------


class TestSendViaRestApi:
    """send() uses REST API (not WebSocket) to avoid race conditions."""

    @staticmethod
    def _mock_aiohttp_session(response_status=200, response_text="OK"):
        """Build a mock aiohttp session + response for async-with patterns.

        aiohttp.ClientSession() is a sync constructor whose return value
        is used as ``async with session:``.  ``session.post(...)`` returns a
        context-manager (not a coroutine), so both layers use MagicMock for
        the call and AsyncMock only for ``__aenter__`` / ``__aexit__``.
        """
        mock_response = MagicMock()
        mock_response.status = response_status
        mock_response.text = AsyncMock(return_value=response_text)
        mock_response.__aenter__ = AsyncMock(return_value=mock_response)
        mock_response.__aexit__ = AsyncMock(return_value=False)

        mock_session = MagicMock()
        mock_session.post = MagicMock(return_value=mock_response)
        mock_session.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session.__aexit__ = AsyncMock(return_value=False)

        return mock_session

    @pytest.mark.asyncio
    async def test_send_success(self):
        adapter = _make_adapter()
        mock_session = self._mock_aiohttp_session(200)

        with patch("gateway.platforms.homeassistant.aiohttp") as mock_aiohttp:
            mock_aiohttp.ClientSession = MagicMock(return_value=mock_session)
            mock_aiohttp.ClientTimeout = lambda total: total

            result = await adapter.send("ha_events", "Test notification")

        assert result.success is True
        # Verify the REST API was called with correct payload
        call_args = mock_session.post.call_args
        assert "/api/services/persistent_notification/create" in call_args[0][0]
        assert call_args[1]["json"]["title"] == "Hermes Agent"
        assert call_args[1]["json"]["message"] == "Test notification"
        assert "Bearer tok" in call_args[1]["headers"]["Authorization"]

    @pytest.mark.asyncio
    async def test_send_http_error(self):
        adapter = _make_adapter()
        mock_session = self._mock_aiohttp_session(401, "Unauthorized")

        with patch("gateway.platforms.homeassistant.aiohttp") as mock_aiohttp:
            mock_aiohttp.ClientSession = MagicMock(return_value=mock_session)
            mock_aiohttp.ClientTimeout = lambda total: total

            result = await adapter.send("ha_events", "Test")

        assert result.success is False
        assert "401" in result.error

    @pytest.mark.asyncio
    async def test_send_truncates_long_message(self):
        adapter = _make_adapter()
        mock_session = self._mock_aiohttp_session(200)
        long_message = "x" * 10000

        with patch("gateway.platforms.homeassistant.aiohttp") as mock_aiohttp:
            mock_aiohttp.ClientSession = MagicMock(return_value=mock_session)
            mock_aiohttp.ClientTimeout = lambda total: total

            await adapter.send("ha_events", long_message)

        sent_message = mock_session.post.call_args[1]["json"]["message"]
        assert len(sent_message) == 4096

    @pytest.mark.asyncio
    async def test_send_does_not_use_websocket(self):
        """send() must use REST API, not the WS connection (race condition fix)."""
        adapter = _make_adapter()
        adapter._ws = AsyncMock()  # Simulate an active WS
        mock_session = self._mock_aiohttp_session(200)

        with patch("gateway.platforms.homeassistant.aiohttp") as mock_aiohttp:
            mock_aiohttp.ClientSession = MagicMock(return_value=mock_session)
            mock_aiohttp.ClientTimeout = lambda total: total

            await adapter.send("ha_events", "Test")

        # WS should NOT have been used for sending
        adapter._ws.send_json.assert_not_called()
        adapter._ws.receive_json.assert_not_called()


# ---------------------------------------------------------------------------
# Toolset integration
# ---------------------------------------------------------------------------


class TestToolsetIntegration:
    def test_homeassistant_toolset_resolves(self):
        from toolsets import resolve_toolset

        tools = resolve_toolset("homeassistant")
        assert set(tools) == {"ha_list_entities", "ha_get_state", "ha_call_service", "ha_list_services"}

    def test_gateway_toolset_includes_ha_tools(self):
        from toolsets import resolve_toolset

        gateway_tools = resolve_toolset("hermes-gateway")
        for tool in ("ha_list_entities", "ha_get_state", "ha_call_service", "ha_list_services"):
            assert tool in gateway_tools

    def test_hermes_core_tools_includes_ha(self):
        from toolsets import _HERMES_CORE_TOOLS

        for tool in ("ha_list_entities", "ha_get_state", "ha_call_service", "ha_list_services"):
            assert tool in _HERMES_CORE_TOOLS


# ---------------------------------------------------------------------------
# WebSocket URL construction
# ---------------------------------------------------------------------------


class TestWsUrlConstruction:
    def test_http_to_ws(self):
        config = PlatformConfig(enabled=True, token="t", extra={"url": "http://ha:8123"})
        adapter = HomeAssistantAdapter(config)
        ws_url = adapter._hass_url.replace("http://", "ws://").replace("https://", "wss://")
        assert ws_url == "ws://ha:8123"

    def test_https_to_wss(self):
        config = PlatformConfig(enabled=True, token="t", extra={"url": "https://ha.example.com"})
        adapter = HomeAssistantAdapter(config)
        ws_url = adapter._hass_url.replace("http://", "ws://").replace("https://", "wss://")
        assert ws_url == "wss://ha.example.com"
