"""
Tests for MeshNode — the async context manager client.

All NATS interactions are mocked via pytest-mock so no live NATS server is required.
"""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, MagicMock, patch


class TestMeshNodeContextManager:
    """Tests for MeshNode async context manager lifecycle."""

    @pytest.mark.asyncio
    async def test_connects_to_nats_on_enter(self) -> None:
        """__aenter__ should call nats.connect with the configured URL."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc) as mock_connect:
            from meshpocalypse_semantic.client import MeshNode
            async with MeshNode("nats://test:4222"):
                mock_connect.assert_called_once_with("nats://test:4222")

    @pytest.mark.asyncio
    async def test_drains_nats_on_exit(self) -> None:
        """__aexit__ should drain the NATS connection."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc):
            from meshpocalypse_semantic.client import MeshNode
            async with MeshNode():
                pass
            mock_nc.drain.assert_called_once()

    @pytest.mark.asyncio
    async def test_drains_on_exception_exit(self) -> None:
        """__aexit__ should drain even when the block raises."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc):
            from meshpocalypse_semantic.client import MeshNode
            with pytest.raises(ValueError, match="intentional"):
                async with MeshNode():
                    raise ValueError("intentional")
            mock_nc.drain.assert_called_once()

    @pytest.mark.asyncio
    async def test_publish_sends_bytes_to_nats(self) -> None:
        """publish() should forward subject and payload to nats.publish."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc):
            from meshpocalypse_semantic.client import MeshNode
            async with MeshNode() as mesh:
                await mesh.publish("mesh.test.subject", b"hello world")
                mock_nc.publish.assert_called_once_with("mesh.test.subject", b"hello world")

    @pytest.mark.asyncio
    async def test_publish_raises_runtime_error_when_disconnected(self) -> None:
        """publish() outside context should raise RuntimeError."""
        from meshpocalypse_semantic.client import MeshNode
        mesh = MeshNode()
        with pytest.raises(RuntimeError, match="not connected"):
            await mesh.publish("subject", b"data")

    @pytest.mark.asyncio
    async def test_publish_raises_value_error_for_empty_subject(self) -> None:
        """publish() with empty subject should raise ValueError."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc):
            from meshpocalypse_semantic.client import MeshNode
            async with MeshNode() as mesh:
                with pytest.raises(ValueError, match="subject"):
                    await mesh.publish("", b"data")

    @pytest.mark.asyncio
    async def test_sparql_property_raises_when_disconnected(self) -> None:
        """Accessing .sparql outside context should raise RuntimeError."""
        from meshpocalypse_semantic.client import MeshNode
        mesh = MeshNode()
        with pytest.raises(RuntimeError, match="not connected"):
            _ = mesh.sparql

    @pytest.mark.asyncio
    async def test_atf_property_raises_when_disconnected(self) -> None:
        """Accessing .atf outside context should raise RuntimeError."""
        from meshpocalypse_semantic.client import MeshNode
        mesh = MeshNode()
        with pytest.raises(RuntimeError, match="not connected"):
            _ = mesh.atf

    @pytest.mark.asyncio
    async def test_sparql_property_available_in_context(self) -> None:
        """SparqlClient should be accessible within the async with block."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc):
            from meshpocalypse_semantic.client import MeshNode
            from meshpocalypse_semantic.sparql import SparqlClient
            async with MeshNode() as mesh:
                assert isinstance(mesh.sparql, SparqlClient)

    @pytest.mark.asyncio
    async def test_atf_property_available_in_context(self) -> None:
        """ATFClient should be accessible within the async with block."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc):
            from meshpocalypse_semantic.client import MeshNode
            from meshpocalypse_semantic.atf import ATFClient
            async with MeshNode() as mesh:
                assert isinstance(mesh.atf, ATFClient)

    @pytest.mark.asyncio
    async def test_defaults_to_standard_nats_url(self) -> None:
        """Default URL should be nats://localhost:4222."""
        mock_nc = AsyncMock()
        with patch("meshpocalypse_semantic.client.nats.connect", return_value=mock_nc) as mock_connect:
            from meshpocalypse_semantic.client import MeshNode
            async with MeshNode():
                mock_connect.assert_called_once_with("nats://localhost:4222")
