"""
Unit tests for orchestration_lifespan (Step 15: polling lifecycle).

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

from unittest.mock import AsyncMock, patch

import pytest

from mcp_proxy_adapter.core.orchestration.orchestration_lifespan import (
    _get_orchestration_config,
    _proxy_base_url_from_config,
    shutdown_orchestration_polling,
    startup_orchestration_polling,
)
from mcp_proxy_adapter.core.orchestration.snapshot_holder import SnapshotHolder


class TestProxyBaseUrlFromConfig:
    """Tests for _proxy_base_url_from_config."""

    def test_empty_config_returns_none(self) -> None:
        """Empty config returns None."""
        assert _proxy_base_url_from_config({}) is None

    def test_registration_register_url_strips_path(self) -> None:
        """register_url path is stripped to get base URL."""
        config = {
            "registration": {"register_url": "http://localhost:3005/register"},
        }
        assert _proxy_base_url_from_config(config) == "http://localhost:3005"

    def test_registration_proxy_url_used(self) -> None:
        """proxy_url is used when register_url missing."""
        config = {
            "registration": {"proxy_url": "https://proxy.example.com:443"},
        }
        assert _proxy_base_url_from_config(config) == "https://proxy.example.com:443"

    def test_register_url_preferred_over_proxy_url(self) -> None:
        """register_url is preferred when both present."""
        config = {
            "registration": {
                "register_url": "http://host:80/register",
                "proxy_url": "http://other:90",
            },
        }
        assert _proxy_base_url_from_config(config) == "http://host:80"

    def test_missing_registration_returns_none(self) -> None:
        """No registration key returns None."""
        assert _proxy_base_url_from_config({"other": {}}) is None

    def test_empty_string_url_returns_none(self) -> None:
        """Empty or whitespace URL returns None."""
        config = {"registration": {"register_url": "   "}}
        assert _proxy_base_url_from_config(config) is None


class TestGetOrchestrationConfig:
    """Tests for _get_orchestration_config."""

    def test_none_config_returns_defaults(self) -> None:
        """None config returns default adapter (discovery disabled, default intervals)."""
        cfg = _get_orchestration_config(None)
        assert cfg.discovery_enabled is False
        assert cfg.polling_interval == 30.0
        assert cfg.initial_poll_timeout == 60.0
        assert cfg.static_servers is None

    def test_empty_config_returns_defaults(self) -> None:
        """Empty config returns defaults."""
        cfg = _get_orchestration_config({})
        assert cfg.discovery_enabled is False

    def test_orchestration_section_parsed(self) -> None:
        """Orchestration section is parsed correctly."""
        config = {
            "orchestration": {
                "discovery_enabled": True,
                "polling_interval": 15.0,
                "initial_poll_timeout": 20.0,
            },
        }
        cfg = _get_orchestration_config(config)
        assert cfg.discovery_enabled is True
        assert cfg.polling_interval == 15.0
        assert cfg.initial_poll_timeout == 20.0


@pytest.mark.asyncio
class TestStartupShutdownOrchestrationPolling:
    """Tests for startup and shutdown lifecycle."""

    async def test_startup_discovery_disabled_no_thread(self) -> None:
        """When discovery is disabled, no polling thread is started."""
        config = {"orchestration": {"discovery_enabled": False}}
        holder = SnapshotHolder()
        await startup_orchestration_polling(config, holder)
        # Module state should still be clean (no thread started)
        import mcp_proxy_adapter.core.orchestration.orchestration_lifespan as mod

        assert mod._polling_thread is None
        assert mod._stop_event is None

    async def test_startup_no_proxy_url_no_thread(self) -> None:
        """When discovery enabled but no proxy URL, no thread is started."""
        config = {
            "orchestration": {"discovery_enabled": True},
            "registration": {},
        }
        holder = SnapshotHolder()
        await startup_orchestration_polling(config, holder)
        import mcp_proxy_adapter.core.orchestration.orchestration_lifespan as mod

        assert mod._polling_thread is None
        assert mod._stop_event is None

    async def test_startup_then_shutdown_graceful(self) -> None:
        """Startup with discovery and proxy_url starts thread; shutdown stops it."""
        config = {
            "orchestration": {
                "discovery_enabled": True,
                "polling_interval": 60.0,
                "initial_poll_timeout": 2.0,
            },
            "registration": {"register_url": "http://localhost:9999/register"},
        }
        holder = SnapshotHolder()
        with patch(
            "mcp_proxy_adapter.core.orchestration.orchestration_lifespan.run_one_cycle",
            new_callable=AsyncMock,
            return_value=True,
        ), patch(
            "mcp_proxy_adapter.core.orchestration.orchestration_lifespan.run_polling_loop",
        ) as mock_loop:
            mock_loop.return_value = None
            await startup_orchestration_polling(config, holder)

        import mcp_proxy_adapter.core.orchestration.orchestration_lifespan as mod

        assert mod._polling_thread is not None
        assert mod._stop_event is not None
        assert mod._polling_thread.is_alive() or not mod._polling_thread.is_alive()

        await shutdown_orchestration_polling()

        assert mod._polling_thread is None
        assert mod._stop_event is None

    async def test_shutdown_idempotent_when_not_started(self) -> None:
        """Shutdown when polling was never started is a no-op."""
        await shutdown_orchestration_polling()
        import mcp_proxy_adapter.core.orchestration.orchestration_lifespan as mod

        assert mod._polling_thread is None
        assert mod._stop_event is None
