"""Tests for server/bootstrap.py module.

Tests cover application bootstrapping and dependency injection.
"""

from unittest.mock import MagicMock, patch

import pytest

from mcp_hangar.infrastructure.discovery.registry import UnknownDiscoverySourceError, create_source

from mcp_hangar.server.bootstrap import (
    _auto_add_volumes,
    _ensure_data_dir,
    ApplicationContext,
    bootstrap,
    GC_WORKER_INTERVAL_SECONDS,
    HEALTH_CHECK_INTERVAL_SECONDS,
)


class TestConstants:
    """Tests for module constants."""

    def test_gc_worker_interval(self):
        """GC worker interval should be reasonable."""
        assert GC_WORKER_INTERVAL_SECONDS > 0
        assert GC_WORKER_INTERVAL_SECONDS == 30

    def test_health_check_interval(self):
        """Health check interval should be reasonable."""
        assert HEALTH_CHECK_INTERVAL_SECONDS > 0
        assert HEALTH_CHECK_INTERVAL_SECONDS == 60


class TestApplicationContext:
    """Tests for ApplicationContext dataclass."""

    def test_application_context_creation(self):
        """ApplicationContext should be creatable with minimal args."""
        mock_runtime = MagicMock()
        mock_mcp = MagicMock()

        ctx = ApplicationContext(
            runtime=mock_runtime,
            mcp_server=mock_mcp,
        )

        assert ctx.runtime == mock_runtime
        assert ctx.mcp_server == mock_mcp
        assert ctx.background_workers == []
        assert ctx.discovery_orchestrator is None
        assert ctx.config == {}

    def test_application_context_with_workers(self):
        """ApplicationContext should accept background workers."""
        mock_runtime = MagicMock()
        mock_mcp = MagicMock()
        mock_worker = MagicMock()

        ctx = ApplicationContext(
            runtime=mock_runtime,
            mcp_server=mock_mcp,
            background_workers=[mock_worker],
        )

        assert len(ctx.background_workers) == 1
        assert ctx.background_workers[0] == mock_worker

    def test_application_context_shutdown(self):
        """ApplicationContext.shutdown() should stop all components."""
        mock_runtime = MagicMock()
        mock_mcp = MagicMock()
        mock_worker = MagicMock()
        mock_orchestrator = MagicMock()
        mock_runtime.repository.items.return_value = []

        ctx = ApplicationContext(
            runtime=mock_runtime,
            mcp_server=mock_mcp,
            background_workers=[mock_worker],
            discovery_orchestrator=mock_orchestrator,
        )

        ctx.shutdown()

        mock_worker.stop.assert_called_once()

    def test_application_context_shutdown_handles_worker_errors(self):
        """ApplicationContext.shutdown() should handle worker errors gracefully."""
        mock_runtime = MagicMock()
        mock_mcp = MagicMock()
        mock_worker = MagicMock()
        mock_worker.stop.side_effect = Exception("Worker error")
        mock_worker.task = "gc"
        mock_runtime.repository.items.return_value = []

        ctx = ApplicationContext(
            runtime=mock_runtime,
            mcp_server=mock_mcp,
            background_workers=[mock_worker],
        )

        ctx.shutdown()


class TestEnsureDataDir:
    """Tests for _ensure_data_dir function."""

    def test_creates_data_dir_when_missing(self, tmp_path, monkeypatch):
        """Should create data directory when it doesn't exist."""
        monkeypatch.chdir(tmp_path)

        _ensure_data_dir()

        data_dir = tmp_path / "data"
        assert data_dir.exists()
        assert data_dir.is_dir()

    def test_does_nothing_when_dir_exists(self, tmp_path, monkeypatch):
        """Should not fail when data directory already exists."""
        monkeypatch.chdir(tmp_path)
        data_dir = tmp_path / "data"
        data_dir.mkdir()

        # Should not raise
        _ensure_data_dir()

        assert data_dir.exists()


class TestCreateBackgroundWorkers:
    """Tests for _create_background_workers function."""

    def test_creates_two_workers(self):
        """Should create GC, health check, and metrics snapshot workers."""
        mock_worker_class = MagicMock()
        mock_snapshot_class = MagicMock()

        with patch("mcp_hangar.server.bootstrap.workers.BackgroundWorker", mock_worker_class):
            with patch("mcp_hangar.server.bootstrap.workers.MetricsSnapshotWorker", mock_snapshot_class):
                with patch("mcp_hangar.server.bootstrap.workers.get_runtime") as mock_get_runtime:
                    mock_get_runtime.return_value.repository = {}
                    from mcp_hangar.server.bootstrap import _create_background_workers

                    workers = _create_background_workers()

        assert mock_worker_class.call_count == 2
        assert len(workers) == 3

    def test_workers_not_started(self):
        """Workers should be created but not started."""
        mock_worker_class = MagicMock()
        mock_worker = MagicMock()
        mock_worker_class.return_value = mock_worker

        with patch("mcp_hangar.server.bootstrap.workers.BackgroundWorker", mock_worker_class):
            with patch("mcp_hangar.server.bootstrap.workers.get_runtime") as mock_get_runtime:
                mock_get_runtime.return_value.repository = {}
                from mcp_hangar.server.bootstrap import _create_background_workers

                _workers = _create_background_workers()  # noqa: F841

        # Workers should not have start() called
        mock_worker.start.assert_not_called()

    def test_gc_worker_interval(self):
        """GC worker should use correct interval."""
        mock_worker_class = MagicMock()

        with patch("mcp_hangar.server.bootstrap.workers.BackgroundWorker", mock_worker_class):
            with patch("mcp_hangar.server.bootstrap.workers.get_runtime") as mock_get_runtime:
                mock_get_runtime.return_value.repository = {}
                from mcp_hangar.server.bootstrap import _create_background_workers

                _create_background_workers()

        # Find the GC worker call
        gc_call = None
        for call in mock_worker_class.call_args_list:
            if call.kwargs.get("task") == "gc":
                gc_call = call
                break

        assert gc_call is not None
        assert gc_call.kwargs["interval_s"] == GC_WORKER_INTERVAL_SECONDS

    def test_health_worker_interval(self):
        """Health worker should use correct interval."""
        mock_worker_class = MagicMock()

        with patch("mcp_hangar.server.bootstrap.workers.BackgroundWorker", mock_worker_class):
            with patch("mcp_hangar.server.bootstrap.workers.get_runtime") as mock_get_runtime:
                mock_get_runtime.return_value.repository = {}
                from mcp_hangar.server.bootstrap import _create_background_workers

                _create_background_workers()

        # Find the health worker call
        health_call = None
        for call in mock_worker_class.call_args_list:
            if call.kwargs.get("task") == "health_check":
                health_call = call
                break

        assert health_call is not None
        assert health_call.kwargs["interval_s"] == HEALTH_CHECK_INTERVAL_SECONDS


class TestAutoAddVolumes:
    """Tests for _auto_add_volumes function."""

    def test_memory_provider_gets_volume(self, tmp_path, monkeypatch):
        """Memory providers should get auto-added volume."""
        monkeypatch.chdir(tmp_path)

        volumes = _auto_add_volumes("memory-provider")

        assert len(volumes) == 1
        assert "memory" in volumes[0]
        assert "/app/data:rw" in volumes[0]

    def test_filesystem_provider_gets_volume(self, tmp_path, monkeypatch):
        """Filesystem providers should get auto-added volume."""
        monkeypatch.chdir(tmp_path)

        volumes = _auto_add_volumes("filesystem-provider")

        assert len(volumes) == 1
        assert "filesystem" in volumes[0]
        assert "/data:rw" in volumes[0]

    def test_other_provider_no_volume(self, tmp_path, monkeypatch):
        """Other providers should not get auto-added volumes."""
        monkeypatch.chdir(tmp_path)

        volumes = _auto_add_volumes("math-provider")

        assert len(volumes) == 0

    def test_case_insensitive_matching(self, tmp_path, monkeypatch):
        """Volume matching should be case-insensitive."""
        monkeypatch.chdir(tmp_path)

        volumes = _auto_add_volumes("MEMORY-PROVIDER")

        assert len(volumes) == 1


class TestCreateDiscoverySource:
    """Tests for discovery source construction (registry.create_source)."""

    def test_docker_source(self):
        """Should create Docker discovery source."""
        with patch("mcp_hangar.infrastructure.discovery.DockerDiscoverySource") as MockSource:
            source = create_source("docker", {"mode": "additive"})

        MockSource.assert_called_once()
        assert source == MockSource.return_value

    def test_filesystem_source(self, tmp_path):
        """Should create filesystem discovery source."""
        with patch("mcp_hangar.infrastructure.discovery.FilesystemDiscoverySource") as MockSource:
            config = {
                "mode": "additive",
                "path": str(tmp_path),
                "pattern": "*.yaml",
            }
            source = create_source("filesystem", config)

        MockSource.assert_called_once()
        assert source == MockSource.return_value

    def test_entrypoint_source(self):
        """Should create entrypoint discovery source."""
        with patch("mcp_hangar.infrastructure.discovery.EntrypointDiscoverySource") as MockSource:
            source = create_source("entrypoint", {"mode": "additive"})

        MockSource.assert_called_once()
        assert source == MockSource.return_value

    def test_unknown_source_raises(self):
        """Unknown source types are a configuration error, not a skip."""
        with pytest.raises(UnknownDiscoverySourceError):
            create_source("unknown", {"mode": "additive"})

    def test_authoritative_mode(self):
        """Should handle authoritative mode correctly."""
        with patch("mcp_hangar.infrastructure.discovery.DockerDiscoverySource") as MockSource:
            create_source("docker", {"mode": "authoritative"})

        # Check that mode was passed correctly
        call_kwargs = MockSource.call_args.kwargs
        from mcp_hangar.domain.discovery import DiscoveryMode

        assert call_kwargs["mode"] == DiscoveryMode.AUTHORITATIVE


class TestBootstrap:
    """Tests for bootstrap function."""

    @pytest.fixture
    def mock_dependencies(self):
        """Mock all dependencies for bootstrap using proper patch paths."""
        # Create mocks
        mock_data_dir = MagicMock()
        mock_get_runtime = MagicMock()
        mock_runtime = MagicMock()
        mock_runtime.rate_limit_config.requests_per_second = 10
        mock_runtime.rate_limit_config.burst_size = 100
        mock_get_runtime.return_value = mock_runtime
        mock_init_context = MagicMock()
        mock_init_eh = MagicMock()
        mock_init_cqrs = MagicMock()
        mock_init_saga = MagicMock()
        mock_load_config = MagicMock(return_value={"discovery": {"enabled": False}})
        mock_init_retry = MagicMock()
        mock_fastmcp = MagicMock()
        mock_reg_tools = MagicMock()
        mock_reg_modern = MagicMock()
        mock_create_workers = MagicMock(return_value=[])
        mock_runtime.repository.keys.return_value = []
        mock_init_event_store = MagicMock()
        mock_init_hot_loading = MagicMock(return_value=(None, None))
        mock_parse_auth = MagicMock(return_value={})
        mock_bootstrap_auth = MagicMock()
        mock_bootstrap_auth.return_value.enabled = False
        mock_get_context = MagicMock(return_value=MagicMock())

        # Patch paths - use the actual module paths where functions are called
        patches = [
            patch("mcp_hangar.server.bootstrap._ensure_data_dir", mock_data_dir),
            patch("mcp_hangar.server.bootstrap.get_runtime", mock_get_runtime),
            patch("mcp_hangar.server.bootstrap.init_context", mock_init_context),
            patch("mcp_hangar.server.bootstrap.init_event_handlers", mock_init_eh),
            patch("mcp_hangar.server.bootstrap.init_cqrs", mock_init_cqrs),
            patch("mcp_hangar.server.bootstrap.init_saga", mock_init_saga),
            patch("mcp_hangar.server.bootstrap.load_configuration", mock_load_config),
            patch("mcp_hangar.server.bootstrap.init_retry_config", mock_init_retry),
            patch("mcp_hangar.server.bootstrap.init_event_store", mock_init_event_store),
            patch("mcp_hangar.server.bootstrap.init_hot_loading", mock_init_hot_loading),
            patch("mcp_hangar.server.bootstrap.new_mcp_server", mock_fastmcp),
            patch("mcp_hangar.server.bootstrap.register_all_tools", mock_reg_tools),
            patch("mcp_hangar.server.bootstrap.register_modern_surface", mock_reg_modern),
            patch("mcp_hangar.server.bootstrap.create_background_workers", mock_create_workers),
            patch("mcp_hangar.server.bootstrap.GROUPS", {}),
            patch("mcp_hangar.server.bootstrap.parse_auth_config", mock_parse_auth),
            patch("mcp_hangar.server.bootstrap.bootstrap_auth", mock_bootstrap_auth),
            patch("mcp_hangar.server.bootstrap.get_context", mock_get_context),
        ]

        # Start all patches
        for p in patches:
            p.start()

        yield {
            "data_dir": mock_data_dir,
            "get_runtime": mock_get_runtime,
            "init_context": mock_init_context,
            "init_eh": mock_init_eh,
            "init_cqrs": mock_init_cqrs,
            "init_saga": mock_init_saga,
            "load_config": mock_load_config,
            "init_retry": mock_init_retry,
            "fastmcp": mock_fastmcp,
            "reg_tools": mock_reg_tools,
            "register_modern_surface": mock_reg_modern,
            "create_workers": mock_create_workers,
            "get_context": mock_get_context,
        }

        # Stop all patches
        for p in patches:
            p.stop()

    def test_bootstrap_returns_application_context(self, mock_dependencies):
        """Bootstrap should return ApplicationContext."""
        ctx = bootstrap()

        assert isinstance(ctx, ApplicationContext)

    def test_bootstrap_calls_init_sequence(self, mock_dependencies):
        """Bootstrap should call init functions in order."""
        bootstrap()

        mock_dependencies["data_dir"].assert_called_once()
        mock_dependencies["get_runtime"].assert_called_once()
        mock_dependencies["init_context"].assert_called_once()
        mock_dependencies["init_eh"].assert_called_once()
        mock_dependencies["init_cqrs"].assert_called_once()
        mock_dependencies["init_saga"].assert_called_once()

    def test_bootstrap_with_config_path(self, mock_dependencies):
        """Bootstrap should pass config path to load_configuration."""
        bootstrap(config_path="/path/to/config.yaml")

        # `load_servers=False` is load-bearing rather than cosmetic: building a
        # declared server reaches for the runtime singleton, and the runtime
        # takes the storage backend at construction because it is frozen
        # afterwards. Reading the file and building its servers had to become
        # two steps with the backend selected in between.
        mock_dependencies["load_config"].assert_called_once_with("/path/to/config.yaml", load_servers=False)

    def test_bootstrap_with_discovery_disabled(self, mock_dependencies):
        """Bootstrap without discovery should have None orchestrator."""
        ctx = bootstrap()

        assert ctx.discovery_orchestrator is None

    def test_bootstrap_wires_discovery_orchestrator_to_api_context(self, mock_dependencies):
        """The REST API reads the bootstrapped discovery orchestrator."""
        mock_dependencies["load_config"].return_value = {"discovery": {"enabled": True}}
        orchestrator = MagicMock()

        with patch("mcp_hangar.server.bootstrap.create_discovery_orchestrator", return_value=orchestrator):
            bootstrap()

        assert mock_dependencies["get_context"].return_value.discovery_orchestrator is orchestrator

    def test_bootstrap_creates_mcp_server(self, mock_dependencies):
        """Bootstrap should create the MCP server under the shared inbound identity."""
        from mcp_hangar import __version__
        from mcp_hangar.protocol import HANGAR_SERVER_NAME

        bootstrap()

        # One identity across surfaces: the factory path and this path must report
        # the same serverInfo (#560), so assert the constants, not literals. The
        # version is explicit because the SDK otherwise reports its own.
        mock_dependencies["fastmcp"].assert_called_once_with(HANGAR_SERVER_NAME, version=__version__)

    def test_bootstrap_registers_the_modern_surface(self, mock_dependencies):
        """Bootstrap must register SEP-2575 ``server/discover`` on the served server.

        The shipped ``serve --http`` surface 404'd this route for the whole 2.x
        pre-release because the wiring lived only in the never-called
        MCPServerFactory (#560). Pinned on the served server instance, so dropping
        the call fails here rather than in a live compat run.
        """
        bootstrap()

        mock_dependencies["register_modern_surface"].assert_called_once_with(mock_dependencies["fastmcp"].return_value)

    def test_bootstrap_registers_tools(self, mock_dependencies):
        """Bootstrap should register all MCP tools."""
        bootstrap()

        mock_dependencies["reg_tools"].assert_called_once()

    def test_bootstrap_creates_workers(self, mock_dependencies):
        """Bootstrap should create background workers."""
        bootstrap()

        mock_dependencies["create_workers"].assert_called_once()
