"""
Integration tests for server lifecycle and process management.

Tests the ServerLauncher, health checks, and graceful shutdown.
"""

from unittest.mock import MagicMock, patch

import httpx
import pytest

from qwenvert.launcher import ProcessHandle, ServerLauncher
from qwenvert.models import Backend


class TestServerLauncher:
    """Test server launching and lifecycle management."""

    @pytest.mark.asyncio
    async def test_ollama_backend_launch(self, sample_model_7b_q4, temp_config_dir):
        """Test launching Ollama backend."""
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
            adapter_host="127.0.0.1",
            adapter_port=8088,
        )

        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/local/bin/ollama"

            launcher = ServerLauncher(config=config)

            with patch("subprocess.Popen") as mock_popen:
                # Mock process
                mock_process = MagicMock()
                mock_process.pid = 12345
                mock_process.returncode = None
                mock_process.poll.return_value = None
                mock_popen.return_value = mock_process

                # Mock health checks: first check (is already running) = False, then wait succeeds
                with patch.object(launcher, "_check_health", return_value=False):
                    with patch.object(launcher, "_wait_for_health", return_value=True):
                        with patch.object(
                            launcher, "_ensure_ollama_model", return_value=None
                        ):
                            handle = await launcher.start_backend()

                            assert handle is not None
                            assert handle.pid == 12345
                            assert handle.process == mock_process

    @pytest.mark.asyncio
    async def test_llamacpp_backend_launch(self, sample_model_14b_q5, temp_config_dir):
        """Test launching llama.cpp backend."""
        from qwenvert.config import QwenvertConfig

        # Update model to llama.cpp
        from qwenvert.models import Model

        llamacpp_model = Model(
            id=sample_model_14b_q5.id,
            display_name=sample_model_14b_q5.display_name,
            family=sample_model_14b_q5.family,
            size_b=sample_model_14b_q5.size_b,
            quantization=sample_model_14b_q5.quantization,
            backend=Backend.LLAMACPP,
            backend_model_id=sample_model_14b_q5.backend_model_id,
            context_length=sample_model_14b_q5.context_length,
            min_ram_gb=sample_model_14b_q5.min_ram_gb,
            recommended_ram_gb=sample_model_14b_q5.recommended_ram_gb,
        )

        config = QwenvertConfig(
            model_id=llamacpp_model.id,
            backend=llamacpp_model.backend.value,
            backend_url="http://localhost:8080",
            backend_model_id=llamacpp_model.backend_model_id,
            adapter_host="127.0.0.1",
            adapter_port=8088,
        )

        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/local/bin/llama-server"

            # Mock Path.exists to return True for llama-server
            with patch("pathlib.Path.exists", return_value=True):
                launcher = ServerLauncher(config=config)

                with patch("subprocess.Popen") as mock_popen:
                    mock_process = MagicMock()
                    mock_process.pid = 12346
                    mock_process.returncode = None
                    mock_process.poll.return_value = None
                    mock_popen.return_value = mock_process

                    with patch.object(launcher, "_check_health", return_value=False):
                        with patch.object(
                            launcher, "_wait_for_health", return_value=True
                        ):
                            handle = await launcher.start_backend()

                            assert handle is not None
                            assert handle.pid == 12346

                            # Verify llama.cpp command line
                            call_args = mock_popen.call_args
                            cmd = call_args[0][0] if call_args and call_args[0] else []
                            assert any("llama" in str(arg).lower() for arg in cmd)

    @pytest.mark.asyncio
    async def test_backend_health_check_retry(self, sample_model_7b_q4):
        """Test that launcher retries health checks on startup."""
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
            adapter_host="127.0.0.1",
            adapter_port=8088,
        )

        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/local/bin/ollama"

            launcher = ServerLauncher(config=config)

            with patch("subprocess.Popen") as mock_popen:
                mock_process = MagicMock()
                mock_process.pid = 12347
                mock_process.returncode = None
                mock_process.poll.return_value = None
                mock_popen.return_value = mock_process

                # Mock the checks - simpler approach without custom retry logic
                with patch.object(launcher, "_check_health", return_value=False):
                    with patch.object(launcher, "_wait_for_health", return_value=True):
                        with patch.object(
                            launcher, "_ensure_ollama_model", return_value=None
                        ):
                            handle = await launcher.start_backend()

                            assert handle is not None
                            assert handle.pid == 12347

    @pytest.mark.asyncio
    async def test_backend_health_check_timeout(self, sample_model_7b_q4):
        """Test that launcher times out if backend never becomes healthy."""
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
            adapter_host="127.0.0.1",
            adapter_port=8088,
        )

        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/local/bin/ollama"

            with patch("subprocess.Popen") as mock_popen:
                mock_process = MagicMock()
                mock_process.pid = 12348
                mock_process.returncode = None
                mock_process.poll.return_value = None
                mock_popen.return_value = mock_process

                launcher = ServerLauncher(config=config)

                # Mock health check always failing and wait returning False
                with patch.object(launcher, "_check_health", return_value=False):
                    with patch.object(launcher, "_wait_for_health", return_value=False):
                        with pytest.raises(RuntimeError, match="failed to start"):
                            # Should raise RuntimeError when health check times out
                            await launcher.start_backend()

    @pytest.mark.asyncio
    async def test_graceful_shutdown(self, sample_model_7b_q4):
        """Test graceful shutdown of backend process."""
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
            adapter_host="127.0.0.1",
            adapter_port=8088,
        )

        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/local/bin/ollama"

            launcher = ServerLauncher(config=config)

            with patch("subprocess.Popen") as mock_popen:
                mock_process = MagicMock()
                mock_process.pid = 12349
                mock_process.returncode = None
                mock_process.poll.return_value = None  # Process is running
                mock_popen.return_value = mock_process

                # Mock health checks: not already running, then wait succeeds
                with patch.object(launcher, "_check_health", return_value=False):
                    with patch.object(launcher, "_wait_for_health", return_value=True):
                        with patch.object(
                            launcher, "_ensure_ollama_model", return_value=None
                        ):
                            handle = await launcher.start_backend()

                            # Verify we got the mocked process
                            assert handle.process == mock_process
                            launcher.backend_process = handle

                            # Stop the backend
                            await launcher.stop_all()

                            # Verify terminate was called on the process
                            mock_process.terminate.assert_called()


class TestAdapterLauncher:
    """Test adapter server launching."""

    @pytest.mark.skip(
        reason="ServerLauncher no longer has _create_adapter_config method"
    )
    @pytest.mark.asyncio
    async def test_adapter_start(self, sample_model_7b_q4):
        """Test starting the adapter server."""


class TestHealthChecks:
    """Test health check functionality."""

    @pytest.mark.asyncio
    async def test_backend_health_check_success(self, sample_model_7b_q4):
        """Test successful backend health check."""
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
        )

        launcher = ServerLauncher(config=config)

        with patch("httpx.AsyncClient.get") as mock_get:
            mock_response = MagicMock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"status": "ok"}
            mock_get.return_value = mock_response

            is_healthy = await launcher._check_health("http://localhost:11434")
            assert is_healthy is True

    @pytest.mark.asyncio
    async def test_backend_health_check_failure(self, sample_model_7b_q4):
        """Test failed backend health check."""
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
        )

        launcher = ServerLauncher(config=config)

        with patch(
            "httpx.AsyncClient.get",
            side_effect=httpx.ConnectError("Connection refused"),
        ):
            is_healthy = await launcher._check_health("http://localhost:11434")
            assert is_healthy is False

    @pytest.mark.asyncio
    async def test_backend_health_check_http_error(self, sample_model_7b_q4):
        """Test backend health check with HTTP error."""
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
        )

        launcher = ServerLauncher(config=config)

        with patch("httpx.AsyncClient.get") as mock_get:
            mock_response = MagicMock()
            mock_response.status_code = 500
            mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
                "500 Server Error", request=MagicMock(), response=mock_response
            )
            mock_get.return_value = mock_response

            is_healthy = await launcher._check_health("http://localhost:11434")
            assert is_healthy is False


class TestProcessManagement:
    """Test process management utilities."""

    @pytest.mark.asyncio
    async def test_process_is_running(self):
        """Test checking if process is running."""

        mock_process = MagicMock()
        mock_process.pid = 12351
        mock_process.returncode = None  # Still running
        mock_process.poll.return_value = None

        handle = ProcessHandle(
            process=mock_process,
            name="test_process",
        )

        assert handle.is_running() is True

    @pytest.mark.asyncio
    async def test_process_has_exited(self):
        """Test detecting process exit."""

        mock_process = MagicMock()
        mock_process.pid = 12352
        mock_process.returncode = 0  # Exited
        mock_process.poll.return_value = 0

        handle = ProcessHandle(
            process=mock_process,
            name="test_process",
        )

        assert handle.is_running() is False

    @pytest.mark.skip(reason="ServerLauncher no longer has stop_backend method")
    @pytest.mark.asyncio
    async def test_force_kill_on_timeout(self, sample_model_7b_q4):
        """Test force killing process if graceful shutdown times out."""


class TestRealServerIntegration:
    """Real integration tests (require actual servers)."""

    @pytest.mark.integration
    @pytest.mark.skipif(True, reason="Requires actual Ollama installation")
    @pytest.mark.asyncio
    async def test_full_lifecycle_ollama(self, sample_model_7b_q4, temp_config_dir):
        """Test full server lifecycle with real Ollama.

        Prerequisites:
        - Ollama installed
        - qwen2.5-coder:7b model downloaded
        """
        from qwenvert.config import QwenvertConfig

        config = QwenvertConfig(
            model_id=sample_model_7b_q4.id,
            backend=sample_model_7b_q4.backend.value,
            backend_url="http://localhost:11434",
            backend_model_id=sample_model_7b_q4.backend_model_id,
            adapter_host="127.0.0.1",
            adapter_port=8088,
        )

        launcher = ServerLauncher(config=config)

        # Start backend
        backend_handle = await launcher.start_backend()
        assert backend_handle.is_running()

        # Verify health
        async with httpx.AsyncClient() as client:
            response = await client.get("http://localhost:11434/api/tags")
            assert response.status_code == 200

        # Stop backend
        await launcher.stop_backend(backend_handle)
        assert not backend_handle.is_running()

    @pytest.mark.integration
    @pytest.mark.skipif(True, reason="Requires llama.cpp installation")
    @pytest.mark.asyncio
    async def test_full_lifecycle_llamacpp(self, sample_model_14b_q5, temp_config_dir):
        """Test full server lifecycle with real llama.cpp.

        Prerequisites:
        - llama.cpp built
        - Model file downloaded
        """
        from qwenvert.config import QwenvertConfig
        from qwenvert.models import Model

        llamacpp_model = Model(
            id=sample_model_14b_q5.id,
            display_name=sample_model_14b_q5.display_name,
            family=sample_model_14b_q5.family,
            size_b=sample_model_14b_q5.size_b,
            quantization=sample_model_14b_q5.quantization,
            backend=Backend.LLAMACPP,
            backend_model_id="/path/to/model.gguf",
            context_length=sample_model_14b_q5.context_length,
            min_ram_gb=sample_model_14b_q5.min_ram_gb,
            recommended_ram_gb=sample_model_14b_q5.recommended_ram_gb,
        )

        config = QwenvertConfig(
            model_id=llamacpp_model.id,
            backend=llamacpp_model.backend.value,
            backend_url="http://localhost:8080",
            backend_model_id=llamacpp_model.backend_model_id,
            adapter_host="127.0.0.1",
            adapter_port=8088,
        )

        launcher = ServerLauncher(config=config)

        # Start backend
        backend_handle = await launcher.start_backend()
        assert backend_handle.is_running()

        # Verify health
        async with httpx.AsyncClient() as client:
            response = await client.get("http://localhost:8080/health")
            assert response.status_code == 200

        # Stop backend
        await launcher.stop_backend(backend_handle)
        assert not backend_handle.is_running()
