"""Tests for the custom Home Assistant tools."""

import json
import socket
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest
from httpx import Response

from mindroom.constants import RuntimePaths, resolve_runtime_paths
from mindroom.credentials import CredentialsManager
from mindroom.custom_tools.homeassistant import HomeAssistantTools
from mindroom.homeassistant_url_validation import homeassistant_url_error_detail
from mindroom.server_fetch_url import ServerFetchUrlError
from mindroom.tool_system.metadata import get_tool_by_name
from mindroom.tool_system.worker_routing import resolve_worker_target
from tests.conftest import TEST_PASSWORD


@pytest.fixture
def mock_credentials_manager(tmp_path: Path) -> CredentialsManager:
    """Create a mock credentials manager with test data."""
    manager = CredentialsManager(base_path=tmp_path / "test_creds")

    # Save test Home Assistant credentials
    test_creds = {
        "instance_url": "http://127.0.0.1:8123",
        "access_token": TEST_PASSWORD,
        "allow_private_url": True,
    }
    manager.save_credentials("homeassistant", test_creds)
    return manager


@pytest.fixture
def runtime_paths(tmp_path: Path) -> RuntimePaths:
    """Create an explicit runtime context for tool construction tests."""
    return resolve_runtime_paths(config_path=tmp_path / "config.yaml", storage_path=tmp_path, process_env={})


@pytest.fixture
def ha_tools_with_mocked_creds(mock_credentials_manager: CredentialsManager) -> HomeAssistantTools:
    """Create HomeAssistantTools instance with mocked credentials."""
    return HomeAssistantTools(credentials_manager=mock_credentials_manager)


class TestHomeAssistantTools:
    """Test suite for Home Assistant tools."""

    def test_initialization(self, mock_credentials_manager: CredentialsManager) -> None:
        """Test HomeAssistantTools initialization."""
        ha_tools = HomeAssistantTools(credentials_manager=mock_credentials_manager)

        assert ha_tools.name == "homeassistant"
        assert len(ha_tools.tools) == 11

        expected_methods = [
            "get_entity_state",
            "list_entities",
            "turn_on",
            "turn_off",
            "toggle",
            "set_brightness",
            "set_color",
            "set_temperature",
            "activate_scene",
            "trigger_automation",
            "call_service",
        ]
        method_names = [tool.__name__ for tool in ha_tools.tools]
        for expected in expected_methods:
            assert expected in method_names

    def test_tool_metadata_passes_worker_scope_to_wrapper(
        self,
        mock_credentials_manager: CredentialsManager,
        runtime_paths: RuntimePaths,
    ) -> None:
        """Tool construction must propagate worker routing metadata into the wrapper."""
        tool = get_tool_by_name(
            "homeassistant",
            runtime_paths,
            credentials_manager=mock_credentials_manager,
            worker_target=resolve_worker_target(
                "shared",
                "general",
                execution_identity=None,
                tenant_id=runtime_paths.env_value("CUSTOMER_ID"),
                account_id=runtime_paths.env_value("ACCOUNT_ID"),
            ),
        )

        assert isinstance(tool, HomeAssistantTools)
        assert tool._worker_target is not None
        assert tool._worker_target.worker_scope == "shared"
        assert tool._worker_target.routing_agent_name == "general"

    def test_shared_scope_credentials_survive_local_reload_with_default_worker_allowlist(
        self,
        runtime_paths: RuntimePaths,
        tmp_path: Path,
    ) -> None:
        """Shared-scope local tools should keep shared credentials without worker mirroring config."""
        credentials_manager = CredentialsManager(base_path=tmp_path / "credentials")
        credentials_manager.save_credentials(
            "homeassistant",
            {
                "instance_url": "http://127.0.0.1:8123",
                "access_token": TEST_PASSWORD,
                "allow_private_url": True,
                "_source": "ui",
            },
        )

        tool = get_tool_by_name(
            "homeassistant",
            runtime_paths,
            credentials_manager=credentials_manager,
            worker_target=resolve_worker_target(
                "shared",
                "general",
                execution_identity=None,
                tenant_id=runtime_paths.env_value("CUSTOMER_ID"),
                account_id=runtime_paths.env_value("ACCOUNT_ID"),
            ),
        )

        assert isinstance(tool, HomeAssistantTools)
        assert tool._load_config() == {
            "instance_url": "http://127.0.0.1:8123",
            "access_token": TEST_PASSWORD,
            "allow_private_url": True,
            "_source": "ui",
        }

    def test_tool_metadata_rejects_isolating_worker_scope(
        self,
        mock_credentials_manager: CredentialsManager,
        runtime_paths: RuntimePaths,
    ) -> None:
        """Home Assistant tools are intentionally unsupported for isolating worker scopes."""
        with pytest.raises(ValueError, match="worker_scope=shared"):
            get_tool_by_name(
                "homeassistant",
                runtime_paths,
                credentials_manager=mock_credentials_manager,
                worker_target=resolve_worker_target(
                    "user",
                    "general",
                    execution_identity=None,
                    tenant_id=runtime_paths.env_value("CUSTOMER_ID"),
                    account_id=runtime_paths.env_value("ACCOUNT_ID"),
                ),
            )

    def test_load_config_with_stored_credentials(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test loading configuration from stored credentials."""
        config = ha_tools_with_mocked_creds._load_config()

        assert config is not None
        assert config["instance_url"] == "http://127.0.0.1:8123"
        assert config["access_token"] == TEST_PASSWORD
        assert config["allow_private_url"] is True

        # A second lookup should return the same values.
        config2 = ha_tools_with_mocked_creds._load_config()
        assert config2 == config

    def test_load_config_without_credentials(self) -> None:
        """Test loading configuration when no credentials exist."""
        mock_manager = MagicMock()
        mock_manager.load_credentials.return_value = None
        mock_manager.shared_manager.return_value = mock_manager

        ha_tools = HomeAssistantTools(credentials_manager=mock_manager)
        config = ha_tools._load_config()

        assert config is None

    @pytest.mark.asyncio
    async def test_api_request_without_config(self) -> None:
        """Test API request when no configuration exists."""
        mock_manager = MagicMock()
        mock_manager.load_credentials.return_value = None
        mock_manager.shared_manager.return_value = mock_manager

        ha_tools = HomeAssistantTools(credentials_manager=mock_manager)
        result = await ha_tools._api_request("GET", "/api/states")

        assert result == {"error": "Home Assistant is not configured. Please connect through the dashboard."}

    @pytest.mark.asyncio
    async def test_api_request_missing_credentials(self) -> None:
        """Test API request with incomplete credentials."""
        mock_manager = MagicMock()
        mock_manager.load_credentials.return_value = {"instance_url": "http://localhost"}
        mock_manager.shared_manager.return_value = mock_manager

        ha_tools = HomeAssistantTools(credentials_manager=mock_manager)
        result = await ha_tools._api_request("GET", "/api/states")

        assert result == {"error": "Missing Home Assistant credentials"}

    @pytest.mark.asyncio
    async def test_api_request_rejects_private_url_without_explicit_opt_in(self, tmp_path: Path) -> None:
        """Stored Home Assistant URLs should not fetch internal targets unless explicitly allowed."""
        manager = CredentialsManager(base_path=tmp_path / "credentials")
        manager.save_credentials(
            "homeassistant",
            {
                "instance_url": "http://127.0.0.1:8123",
                "access_token": TEST_PASSWORD,
            },
        )
        ha_tools = HomeAssistantTools(credentials_manager=manager)

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client_class.side_effect = AssertionError("private URL should be rejected before request")

            result = await ha_tools._api_request("GET", "/api/states")

        assert result == {
            "error": homeassistant_url_error_detail(
                ServerFetchUrlError(reason="private_address"),
                allow_private_url=False,
            ),
        }

    @pytest.mark.asyncio
    async def test_api_request_allows_private_url_with_explicit_opt_in(self, tmp_path: Path) -> None:
        """Self-hosted Home Assistant instances can opt into private URL access deliberately."""
        manager = CredentialsManager(base_path=tmp_path / "credentials")
        manager.save_credentials(
            "homeassistant",
            {
                "instance_url": "http://127.0.0.1:8123",
                "access_token": TEST_PASSWORD,
                "allow_private_url": True,
            },
        )
        ha_tools = HomeAssistantTools(credentials_manager=manager)

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = AsyncMock()
            mock_response = Mock(spec=Response)
            mock_response.status_code = 200
            mock_response.text = '{"success": true}'
            mock_response.json.return_value = {"success": True}
            mock_client.request.return_value = mock_response
            mock_client_class.return_value.__aenter__.return_value = mock_client

            result = await ha_tools._api_request("GET", "/api/states")

        assert result == {"success": True}
        mock_client.request.assert_called_once_with(
            method="GET",
            url="http://127.0.0.1:8123/api/states",
            headers={"Authorization": f"Bearer {TEST_PASSWORD}"},
            json=None,
            timeout=10.0,
        )

    @pytest.mark.asyncio
    async def test_api_request_rejects_dns_rebind_at_connect_time(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """The async Home Assistant client should validate the address it actually connects to."""

        class RebindHandler(BaseHTTPRequestHandler):
            def do_GET(self) -> None:
                body = b'{"success": true}'
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.send_header("Content-Length", str(len(body)))
                self.end_headers()
                self.wfile.write(body)

            def log_message(self, format: str, *_args: object) -> None:  # noqa: A002, ARG002
                return

        server = HTTPServer(("127.0.0.1", 0), RebindHandler)
        thread = threading.Thread(target=server.serve_forever, daemon=True)
        thread.start()
        manager = CredentialsManager(base_path=tmp_path / "credentials")
        manager.save_credentials(
            "homeassistant",
            {
                "instance_url": f"http://rebind.test:{server.server_port}",
                "access_token": TEST_PASSWORD,
            },
        )
        ha_tools = HomeAssistantTools(credentials_manager=manager)
        dns_calls = 0

        def fake_getaddrinfo(host: str | bytes, port: int, *_args: object, **_kwargs: object) -> list[object]:
            nonlocal dns_calls
            if host in ("rebind.test", b"rebind.test"):
                dns_calls += 1
                ip_address = "93.184.216.34" if dns_calls == 1 else "127.0.0.1"
                return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", (ip_address, port))]
            return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", ("93.184.216.34", port))]

        monkeypatch.setattr("socket.getaddrinfo", fake_getaddrinfo)

        try:
            result = await ha_tools._api_request("GET", "/api/states")
        finally:
            server.shutdown()
            server.server_close()

        assert result == {
            "error": homeassistant_url_error_detail(
                ServerFetchUrlError(reason="private_address"),
                allow_private_url=False,
            ),
        }
        assert dns_calls >= 2

    @pytest.mark.asyncio
    async def test_api_request_uses_validated_instance_url(self, tmp_path: Path) -> None:
        """The tool should request the normalized URL returned by server-side fetch validation."""
        manager = CredentialsManager(base_path=tmp_path / "credentials")
        manager.save_credentials(
            "homeassistant",
            {
                "instance_url": " http://127.0.0.1:8123 ",
                "access_token": TEST_PASSWORD,
                "allow_private_url": True,
            },
        )
        ha_tools = HomeAssistantTools(credentials_manager=manager)

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = AsyncMock()
            mock_response = Mock(spec=Response)
            mock_response.status_code = 200
            mock_response.text = '{"success": true}'
            mock_response.json.return_value = {"success": True}
            mock_client.request.return_value = mock_response
            mock_client_class.return_value.__aenter__.return_value = mock_client

            result = await ha_tools._api_request("GET", "/api/states")

        assert result == {"success": True}
        mock_client.request.assert_called_once_with(
            method="GET",
            url="http://127.0.0.1:8123/api/states",
            headers={"Authorization": f"Bearer {TEST_PASSWORD}"},
            json=None,
            timeout=10.0,
        )

    @pytest.mark.asyncio
    async def test_api_request_success(self, ha_tools_with_mocked_creds: HomeAssistantTools) -> None:
        """Test successful API request."""
        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = AsyncMock()
            mock_response = Mock(spec=Response)
            mock_response.status_code = 200
            mock_response.json.return_value = {"success": True}
            mock_client.request.return_value = mock_response
            mock_client_class.return_value.__aenter__.return_value = mock_client

            result = await ha_tools_with_mocked_creds._api_request("GET", "/api/states")

            assert result == {"success": True}
            mock_client.request.assert_called_once_with(
                method="GET",
                url="http://127.0.0.1:8123/api/states",
                headers={"Authorization": f"Bearer {TEST_PASSWORD}"},
                json=None,
                timeout=10.0,
            )

    @pytest.mark.asyncio
    async def test_api_request_with_json_data(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test API request with JSON data."""
        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = AsyncMock()
            mock_response = Mock(spec=Response)
            mock_response.status_code = 200
            mock_response.json.return_value = {"success": True}
            mock_client.request.return_value = mock_response
            mock_client_class.return_value.__aenter__.return_value = mock_client

            json_data = {"entity_id": "light.living_room"}
            result = await ha_tools_with_mocked_creds._api_request(
                "POST",
                "/api/services/light/turn_on",
                json_data,
            )

            assert result == {"success": True}
            mock_client.request.assert_called_once_with(
                method="POST",
                url="http://127.0.0.1:8123/api/services/light/turn_on",
                headers={"Authorization": f"Bearer {TEST_PASSWORD}"},
                json=json_data,
                timeout=10.0,
            )

    @pytest.mark.asyncio
    async def test_api_request_error_response(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test API request with error response."""
        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = AsyncMock()
            mock_response = Mock(spec=Response)
            mock_response.status_code = 404
            mock_response.text = "Not found"
            mock_client.request.return_value = mock_response
            mock_client_class.return_value.__aenter__.return_value = mock_client

            result = await ha_tools_with_mocked_creds._api_request("GET", "/api/invalid")

            assert result == {"error": "Home Assistant API returned status 404"}

    @pytest.mark.asyncio
    async def test_api_request_network_error(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test API request with network error."""
        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = AsyncMock()
            mock_client.request.side_effect = Exception("Network error")
            mock_client_class.return_value.__aenter__.return_value = mock_client

            result = await ha_tools_with_mocked_creds._api_request("GET", "/api/states")

            assert result == {"error": "Unexpected error: Network error"}

    @pytest.mark.asyncio
    async def test_get_entity_state(self, ha_tools_with_mocked_creds: HomeAssistantTools) -> None:
        """Test getting entity state."""
        with patch.object(ha_tools_with_mocked_creds, "_api_request") as mock_request:
            mock_request.return_value = {
                "entity_id": "light.living_room",
                "state": "on",
                "attributes": {"brightness": 255},
            }

            result = await ha_tools_with_mocked_creds.get_entity_state("light.living_room")
            result_dict = json.loads(result)

            assert result_dict["entity_id"] == "light.living_room"
            assert result_dict["state"] == "on"
            assert result_dict["attributes"]["brightness"] == 255

            mock_request.assert_called_once_with("GET", "/api/states/light.living_room")

    @pytest.mark.asyncio
    async def test_list_entities(self, ha_tools_with_mocked_creds: HomeAssistantTools) -> None:
        """Test listing entities."""
        with patch.object(ha_tools_with_mocked_creds, "_api_request") as mock_request:
            mock_request.return_value = [
                {
                    "entity_id": "light.living_room",
                    "state": "on",
                    "attributes": {"friendly_name": "Living Room Light"},
                },
                {
                    "entity_id": "switch.bedroom",
                    "state": "off",
                    "attributes": {"friendly_name": "Bedroom Switch"},
                },
            ]

            result = await ha_tools_with_mocked_creds.list_entities()
            result_list = json.loads(result)

            assert len(result_list) == 2
            assert result_list[0]["entity_id"] == "light.living_room"
            assert result_list[0]["friendly_name"] == "Living Room Light"
            assert result_list[1]["entity_id"] == "switch.bedroom"
            assert result_list[1]["friendly_name"] == "Bedroom Switch"

    @pytest.mark.asyncio
    async def test_turn_on(self, ha_tools_with_mocked_creds: HomeAssistantTools) -> None:
        """Test turning on an entity."""
        with patch.object(ha_tools_with_mocked_creds, "_api_request") as mock_request:
            mock_request.return_value = {"success": True}

            result = await ha_tools_with_mocked_creds.turn_on("light.living_room")
            result_dict = json.loads(result)

            assert result_dict["success"] is True
            mock_request.assert_called_once_with(
                "POST",
                "/api/services/light/turn_on",
                {"entity_id": "light.living_room"},
            )

    @pytest.mark.asyncio
    async def test_set_brightness_valid(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test setting brightness with valid value."""
        with patch.object(ha_tools_with_mocked_creds, "_api_request") as mock_request:
            mock_request.return_value = {"success": True}

            result = await ha_tools_with_mocked_creds.set_brightness("light.living_room", 128)
            result_dict = json.loads(result)

            assert result_dict["success"] is True
            mock_request.assert_called_once_with(
                "POST",
                "/api/services/light/turn_on",
                {"entity_id": "light.living_room", "brightness": 128},
            )

    @pytest.mark.asyncio
    async def test_set_brightness_invalid(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test setting brightness with invalid value."""
        result = await ha_tools_with_mocked_creds.set_brightness("light.living_room", 300)
        result_dict = json.loads(result)

        assert result_dict["error"] == "Brightness must be between 0 and 255"

    @pytest.mark.asyncio
    async def test_set_color_valid(self, ha_tools_with_mocked_creds: HomeAssistantTools) -> None:
        """Test setting color with valid RGB values."""
        with patch.object(ha_tools_with_mocked_creds, "_api_request") as mock_request:
            mock_request.return_value = {"success": True}

            result = await ha_tools_with_mocked_creds.set_color("light.living_room", 255, 128, 0)
            result_dict = json.loads(result)

            assert result_dict["success"] is True
            mock_request.assert_called_once_with(
                "POST",
                "/api/services/light/turn_on",
                {"entity_id": "light.living_room", "rgb_color": [255, 128, 0]},
            )

    @pytest.mark.asyncio
    async def test_set_color_invalid(self, ha_tools_with_mocked_creds: HomeAssistantTools) -> None:
        """Test setting color with invalid RGB values."""
        result = await ha_tools_with_mocked_creds.set_color("light.living_room", 255, 300, 0)
        result_dict = json.loads(result)

        assert result_dict["error"] == "RGB values must be between 0 and 255"

    @pytest.mark.asyncio
    async def test_call_service_with_json_data(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test calling a service with JSON data string."""
        with patch.object(ha_tools_with_mocked_creds, "_api_request") as mock_request:
            mock_request.return_value = {"success": True}

            data_json = '{"brightness": 200, "transition": 2}'
            result = await ha_tools_with_mocked_creds.call_service(
                "light",
                "turn_on",
                "light.living_room",
                data_json,
            )
            result_dict = json.loads(result)

            assert result_dict["success"] is True
            mock_request.assert_called_once_with(
                "POST",
                "/api/services/light/turn_on",
                {"entity_id": "light.living_room", "brightness": 200, "transition": 2},
            )

    @pytest.mark.asyncio
    async def test_call_service_invalid_json(
        self,
        ha_tools_with_mocked_creds: HomeAssistantTools,
    ) -> None:
        """Test calling a service with invalid JSON data."""
        result = await ha_tools_with_mocked_creds.call_service(
            "light",
            "turn_on",
            "light.living_room",
            "invalid json{",
        )
        result_dict = json.loads(result)

        assert "Invalid JSON in data parameter" in result_dict["error"]
