"""Lifespan composition smoke tests for Plan 02 (MCP-04)."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, cast
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastmcp import FastMCP
from fastmcp.server.lifespan import Lifespan

if TYPE_CHECKING:
    from pathlib import Path


class TestLifespanComposition:
    """Verify that four @lifespan components compose without error."""

    def test_lifespan_composition_smoke(self) -> None:
        """Verify mcp is a FastMCP instance with a composed Lifespan set.

        Import is deferred to inside the test body to prevent FileSystemProvider
        from reloading mcp_vm_blackbox.models during xdist worker collection.
        A module-level import of server.py triggers FileSystemProvider at import
        time, which reloads models.py and invalidates GoalState class identity
        in other tests sharing the same worker process.
        """
        from mcp_vm_blackbox.server import mcp

        assert isinstance(mcp, FastMCP)
        # When lifespan=config_lifespan | winrm_lifespan | ssh_tunnel_lifespan | vboxmanage_lifespan
        # is passed to FastMCP, _lifespan becomes a Lifespan instance (not the
        # default_lifespan function).
        assert isinstance(mcp._lifespan, Lifespan), (
            f"Expected mcp._lifespan to be a Lifespan instance (composed with |), got {type(mcp._lifespan)}"
        )


class TestConfigLifespan:
    """Verify config_lifespan warning behaviour."""

    @pytest.mark.asyncio
    async def test_missing_config_emits_warning(self, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
        """config_lifespan logs WARNING when infra_targets.toml is absent."""
        from mcp_vm_blackbox.server import config_lifespan

        missing = tmp_path / "infra_targets.toml"
        fake_server = cast("FastMCP", MagicMock())
        with (
            patch("mcp_vm_blackbox.server._TOML_PATH", missing),
            caplog.at_level(logging.WARNING, logger="mcp_vm_blackbox.server"),
        ):
            gen = config_lifespan._fn(fake_server)
            await gen.__anext__()
            with pytest.raises(StopAsyncIteration):
                await gen.__anext__()

        assert any("configuration not found" in r.message for r in caplog.records)
        assert any(str(missing) in r.message for r in caplog.records)

    @pytest.mark.asyncio
    async def test_present_config_no_warning(self, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
        """config_lifespan does not emit WARNING when infra_targets.toml exists."""
        from mcp_vm_blackbox.server import config_lifespan

        present = tmp_path / "infra_targets.toml"
        present.write_text('[targets.local]\nssh_host = ""\n')
        fake_server = cast("FastMCP", MagicMock())
        with (
            patch("mcp_vm_blackbox.server._TOML_PATH", present),
            caplog.at_level(logging.WARNING, logger="mcp_vm_blackbox.server"),
        ):
            gen = config_lifespan._fn(fake_server)
            await gen.__anext__()
            with pytest.raises(StopAsyncIteration):
                await gen.__anext__()

        assert not any("configuration not found" in r.message for r in caplog.records)


class TestPackerLifespan:
    """Verify packer_lifespan warning behaviour."""

    @pytest.mark.asyncio
    async def test_missing_packer_api_emits_warning(self, caplog: pytest.LogCaptureFixture) -> None:
        """packer_lifespan logs WARNING when asyncio.open_connection raises OSError."""
        from mcp_vm_blackbox.server import packer_lifespan

        fake_server = cast("FastMCP", MagicMock())
        with (
            patch("mcp_vm_blackbox.server._PACKER_API_HOST", "testhost"),
            patch("mcp_vm_blackbox.server._PACKER_API_PORT", 19999),
            patch("asyncio.open_connection", side_effect=OSError("Connection refused")),
            caplog.at_level(logging.WARNING, logger="mcp_vm_blackbox.server"),
        ):
            gen = packer_lifespan._fn(fake_server)
            await gen.__anext__()
            with pytest.raises(StopAsyncIteration):
                await gen.__anext__()

        assert any(
            "Packer" in r.message and "testhost" in r.message and "19999" in r.message
            for r in caplog.records
            if r.levelno == logging.WARNING
        )

    @pytest.mark.asyncio
    async def test_present_packer_api_no_warning(self, caplog: pytest.LogCaptureFixture) -> None:
        """packer_lifespan does not emit WARNING when connection succeeds."""
        from mcp_vm_blackbox.server import packer_lifespan

        fake_server = cast("FastMCP", MagicMock())
        writer = MagicMock()
        writer.close = MagicMock()
        writer.wait_closed = AsyncMock()
        mock_open = AsyncMock(return_value=(MagicMock(), writer))
        with (
            patch("mcp_vm_blackbox.server._PACKER_API_HOST", "testhost"),
            patch("mcp_vm_blackbox.server._PACKER_API_PORT", 19999),
            patch("asyncio.open_connection", mock_open),
            caplog.at_level(logging.WARNING, logger="mcp_vm_blackbox.server"),
        ):
            gen = packer_lifespan._fn(fake_server)
            await gen.__anext__()
            with pytest.raises(StopAsyncIteration):
                await gen.__anext__()

        assert not any(r.levelno == logging.WARNING for r in caplog.records)
