"""
Tests for NetworkRuntime.

Tests the Network IO features including:
- HTTP requests with retry and caching
- Security policy enforcement
- MCard synchronization
- Rate limiting
"""

import base64
import os
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from mcard import MCard
from mcard.model.card_collection import CardCollection

# Updated Imports direct from new package
from mcard.ptr.network import (
    MCardSerialization,
    NetworkRuntime,
    NetworkSecurity,
    RetryUtils,
    SecurityViolationError,
)

# Optional check for aiohttp availability
try:
    import aiohttp

    AIOHTTP_AVAILABLE = True
except ImportError:
    AIOHTTP_AVAILABLE = False


@pytest.fixture
def runtime():
    """Create a NetworkRuntime with in-memory collection."""
    collection = CardCollection(db_path=":memory:")
    return NetworkRuntime(collection)


@pytest.fixture
def runtime_no_collection():
    """Create a NetworkRuntime without a collection."""
    return NetworkRuntime()


class TestSecurityPolicy:
    """Tests for URL security validation."""

    def test_block_domains_exact_match(self):
        """Should block domains in CLM_BLOCKED_DOMAINS."""
        os.environ["CLM_BLOCKED_DOMAINS"] = "evil.com,malicious.org"
        security = NetworkSecurity()

        with pytest.raises(SecurityViolationError, match="blocked by security policy"):
            security.validate_url("https://evil.com/api")

        del os.environ["CLM_BLOCKED_DOMAINS"]

    def test_block_domains_wildcard(self):
        """Should block wildcard domain patterns."""
        os.environ["CLM_BLOCKED_DOMAINS"] = "*.malicious.org"
        security = NetworkSecurity()

        with pytest.raises(SecurityViolationError, match="blocked by security policy"):
            security.validate_url("https://sub.malicious.org/data")

        del os.environ["CLM_BLOCKED_DOMAINS"]

    def test_allowed_domains(self):
        """Should only allow domains in CLM_ALLOWED_DOMAINS when set."""
        os.environ["CLM_ALLOWED_DOMAINS"] = "api.trusted.com,*.internal.corp"
        security = NetworkSecurity()

        with pytest.raises(SecurityViolationError, match="not in the allowed list"):
            security.validate_url("https://external.com/api")

        del os.environ["CLM_ALLOWED_DOMAINS"]

    def test_block_localhost(self):
        """Should block localhost when CLM_BLOCK_LOCALHOST is true."""
        os.environ["CLM_BLOCK_LOCALHOST"] = "true"
        security = NetworkSecurity()

        with pytest.raises(SecurityViolationError, match="Localhost access is blocked"):
            security.validate_url("http://localhost:3000/api")

        with pytest.raises(SecurityViolationError, match="Localhost access is blocked"):
            security.validate_url("http://127.0.0.1:8080/data")

        del os.environ["CLM_BLOCK_LOCALHOST"]

    def test_block_private_ips(self):
        """Should block private IPs when CLM_BLOCK_PRIVATE_IPS is true."""
        os.environ["CLM_BLOCK_PRIVATE_IPS"] = "true"
        security = NetworkSecurity()

        with pytest.raises(SecurityViolationError, match="Private IP"):
            security.validate_url("http://10.0.0.1/internal")

        with pytest.raises(SecurityViolationError, match="Private IP"):
            security.validate_url("http://192.168.1.1/router")

        del os.environ["CLM_BLOCK_PRIVATE_IPS"]


class TestMCardSerialization:
    """Tests for MCard serialization/deserialization."""

    def test_serialize_mcard(self):
        """Should serialize MCard to JSON-safe payload."""
        card = MCard("Hello, World!")
        payload = MCardSerialization.serialize(card)

        assert "hash" in payload
        assert "content" in payload
        assert "g_time" in payload

        # Content should be base64 encoded
        decoded = base64.b64decode(payload["content"]).decode("utf-8")
        assert decoded == "Hello, World!"

    def test_deserialize_mcard(self):
        """Should deserialize JSON payload back to MCard."""
        original = MCard("Test content")
        payload = MCardSerialization.serialize(original)

        restored = MCardSerialization.deserialize(payload)

        assert restored.hash == original.hash
        assert restored.get_content() == original.get_content()


class TestRetryLogic:
    """Tests for HTTP retry with backoff."""

    def test_calculate_backoff_exponential(self):
        """Should calculate exponential backoff correctly."""
        # Using the utility method directly
        delay1 = RetryUtils.calculate_backoff_delay(1, "exponential", 1000)
        delay2 = RetryUtils.calculate_backoff_delay(2, "exponential", 1000)
        delay3 = RetryUtils.calculate_backoff_delay(3, "exponential", 1000)

        # Should approximately double each time (with jitter)
        assert 900 <= delay1 <= 1100  # ~1000
        assert 1800 <= delay2 <= 2200  # ~2000
        assert 3600 <= delay3 <= 4400  # ~4000

    def test_calculate_backoff_linear(self):
        """Should calculate linear backoff correctly."""
        delay1 = RetryUtils.calculate_backoff_delay(1, "linear", 1000)
        delay2 = RetryUtils.calculate_backoff_delay(2, "linear", 1000)
        delay3 = RetryUtils.calculate_backoff_delay(3, "linear", 1000)

        # Should increase linearly
        assert 900 <= delay1 <= 1100  # ~1000
        assert 1800 <= delay2 <= 2200  # ~2000
        assert 2700 <= delay3 <= 3300  # ~3000

    def test_calculate_backoff_constant(self):
        """Should calculate constant backoff correctly."""
        delay1 = RetryUtils.calculate_backoff_delay(1, "constant", 1000)
        delay2 = RetryUtils.calculate_backoff_delay(2, "constant", 1000)

        # Should stay the same
        assert 900 <= delay1 <= 1100
        assert 900 <= delay2 <= 1100

    def test_should_retry_status(self):
        """Should identify retryable status codes."""
        assert RetryUtils.should_retry_status(503) is True
        assert RetryUtils.should_retry_status(429) is True
        assert RetryUtils.should_retry_status(500) is True
        assert RetryUtils.should_retry_status(200) is False
        assert RetryUtils.should_retry_status(404) is False


class TestCaching:
    """Tests for response caching."""

    def test_generate_cache_key(self):
        """Should generate consistent cache keys."""
        from mcard.ptr.network import NetworkCache

        key1 = NetworkCache.generate_key("GET", "https://api.com/data", None)
        key2 = NetworkCache.generate_key("GET", "https://api.com/data", None)
        key3 = NetworkCache.generate_key("POST", "https://api.com/data", None)

        assert key1 == key2  # Same request = same key
        assert key1 != key3  # Different method = different key

    def test_cache_and_retrieve(self, runtime):
        """Should cache and retrieve responses."""
        response = {
            "status": 200,
            "headers": {"Content-Type": "application/json"},
            "body": {"data": "test"},
            "mcard_hash": "abc123",
            "timing": {"total": 100},
        }

        cache_key = "test_key"
        # Access cache via runtime
        runtime.cache.set(cache_key, response, ttl=60)

        cached = runtime.cache.get(cache_key)
        assert cached is not None
        assert cached["body"] == {"data": "test"}
        assert cached.get("cached") is True

    def test_cache_expiry(self, runtime):
        """Should expire cached entries after TTL."""
        import time

        response = {"status": 200, "body": "data"}

        cache_key = "expire_test"
        runtime.cache.set(cache_key, response, ttl=1)  # 1 second TTL

        # Should be cached
        assert runtime.cache.get(cache_key) is not None

        # Wait for expiry
        time.sleep(1.1)

        # Should be expired
        assert runtime.cache.get(cache_key) is None


class TestRateLimiting:
    """Tests for rate limiting."""

    def test_rate_limit_allows_burst(self, runtime):
        """Should allow burst of requests."""
        domain = "test.example.com"

        # Should allow max_burst requests
        for _ in range(runtime.rate_limiter.DEFAULT_RATE_LIMIT["max_burst"]):
            assert runtime.rate_limiter.check(domain) is True

    def test_rate_limit_blocks_after_burst(self, runtime):
        """Should block after burst exhausted."""
        domain = "burst.example.com"

        # Exhaust burst
        for _ in range(runtime.rate_limiter.DEFAULT_RATE_LIMIT["max_burst"]):
            runtime.rate_limiter.check(domain)

        # Next request should be blocked
        assert runtime.rate_limiter.check(domain) is False


class TestInterpolation:
    """Tests for variable interpolation."""

    def test_simple_interpolation(self, runtime):
        """Should interpolate simple variables."""
        context = {"name": "Alice", "id": 123}

        result = runtime._interpolate("Hello, ${name}!", context)
        assert result == "Hello, Alice!"

        result = runtime._interpolate("User ${id}", context)
        assert result == "User 123"

    def test_nested_interpolation(self, runtime):
        """Should interpolate nested paths."""
        context = {"input": {"user_id": 456}, "secrets": {"API_KEY": "secret123"}}

        result = runtime._interpolate("${input.user_id}", context)
        assert result == "456"

        result = runtime._interpolate("Bearer ${secrets.API_KEY}", context)
        assert result == "Bearer secret123"

    def test_missing_interpolation(self, runtime):
        """Should return empty string for missing variables."""
        context = {"name": "Test"}

        result = runtime._interpolate("${missing}", context)
        assert result == ""


@pytest.mark.skipif(not AIOHTTP_AVAILABLE, reason="aiohttp not installed")
class TestHttpRequest:
    """Tests for HTTP request handling (requires aiohttp)."""

    @pytest.mark.asyncio
    async def test_http_get_success(self, runtime):
        """Should handle successful HTTP GET."""
        # Create mock response
        mock_response = AsyncMock()
        mock_response.ok = True
        mock_response.status = 200
        mock_response.headers = {"Content-Type": "application/json"}
        mock_response.json = AsyncMock(return_value={"id": 123})
        mock_response.text = AsyncMock(return_value='{"id": 123}')

        # Create mock for the request context manager (inner async with)
        mock_request_ctx = AsyncMock()
        mock_request_ctx.__aenter__ = AsyncMock(return_value=mock_response)
        mock_request_ctx.__aexit__ = AsyncMock(return_value=None)

        # Create mock session
        mock_session = AsyncMock()
        mock_session.request = MagicMock(return_value=mock_request_ctx)

        # Create mock for the session context manager (outer async with)
        mock_session_ctx = AsyncMock()
        mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session_ctx.__aexit__ = AsyncMock(return_value=None)

        # Patch aiohttp in http_client.py
        with patch("mcard.ptr.network.http_client.aiohttp") as mock_aiohttp:
            mock_aiohttp.ClientSession = MagicMock(return_value=mock_session_ctx)
            mock_aiohttp.ClientTimeout = MagicMock()

            result = await runtime._handle_http_get(
                {"url": "https://api.example.com/users/1"}, {}
            )

            assert result["success"] is True
            assert result["status"] == 200
            assert result["body"] == {"id": 123}


class TestRuntimeInterface:
    """Tests for RuntimeExecutor interface."""

    def test_validate_environment(self, runtime):
        """Network runtime availability."""
        # Now inherited correctly
        assert (
            runtime.validate_environment() is True
            or runtime.validate_environment() is False
        )

    def test_get_runtime_status(self, runtime):
        """Should return runtime status."""
        status = runtime.get_runtime_status()

        # NetworkRuntime inherits from RuntimeExecutor
        assert status["command"] == "network"
        assert "available" in status


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
