from __future__ import annotations

"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""Unit tests for VhostValidator middleware (Story 3.11)."""

import asyncio
import time
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest

from rabbitmq_mcp_connection.connection.exceptions import VHostNotFoundError
from rabbitmq_mcp_connection.middleware.vhost_validator import (
    CACHE_TTL,
    VhostValidator,
    validate_vhost,
)


class TestVhostValidator:
    """Test VhostValidator class functionality (AC #1-6)."""

    @pytest.fixture
    def mock_http_client(self):
        """Create mock HTTP client for testing."""
        client = AsyncMock()
        client.request = AsyncMock()
        return client

    @pytest.fixture
    def validator(self, mock_http_client):
        """Create VhostValidator instance with mock HTTP client."""
        return VhostValidator(mock_http_client)

    @pytest.mark.asyncio
    async def test_validate_vhost_success(self, validator, mock_http_client):
        """Test successful vhost validation (AC #1)."""
        # Mock API response with vhosts
        mock_http_client.request.return_value = [
            {"name": "/"},
            {"name": "/staging"},
            {"name": "/prod"},
        ]

        # Validate existing vhost
        await validator.validate_vhost("/staging")

        # Should call API once
        mock_http_client.request.assert_called_once_with(
            method="GET",
            endpoint="/vhosts",
            correlation_id=None,
        )

    @pytest.mark.asyncio
    async def test_validate_vhost_not_found(self, validator, mock_http_client):
        """Test vhost not found raises error with available vhosts and correlation_id (AC #2)."""
        # Mock API response with vhosts
        mock_http_client.request.return_value = [
            {"name": "/"},
            {"name": "/staging"},
            {"name": "/prod"},
        ]

        # Validate non-existent vhost with correlation_id
        correlation_id = "test-123"
        with pytest.raises(VHostNotFoundError) as exc_info:
            await validator.validate_vhost("/invalid", correlation_id=correlation_id)

        # Check error message includes available vhosts (AC #2)
        error_msg = str(exc_info.value)
        assert "/invalid" in error_msg
        assert "does not exist" in error_msg
        assert "/" in error_msg
        assert "/staging" in error_msg
        assert "/prod" in error_msg
        assert correlation_id in error_msg  # correlation_id included in error

    @pytest.mark.asyncio
    async def test_cache_hit_no_api_call(self, validator, mock_http_client):
        """Test cache hit avoids API call (AC #4)."""
        # Mock API response
        mock_http_client.request.return_value = [
            {"name": "/"},
            {"name": "/staging"},
        ]

        # First validation - should call API
        await validator.validate_vhost("/staging")
        assert mock_http_client.request.call_count == 1

        # Second validation - should use cache
        await validator.validate_vhost("/staging")
        assert mock_http_client.request.call_count == 1  # No additional call

    @pytest.mark.asyncio
    async def test_cache_ttl_expiration(self, validator, mock_http_client):
        """Test cache expires after TTL (AC #4)."""
        # Mock API response
        mock_http_client.request.return_value = [{"name": "/staging"}]

        # First validation
        with patch(
            "rabbitmq_mcp_connection.middleware.vhost_validator.time"
        ) as mock_time:
            mock_time.time.return_value = 1000.0
            await validator.validate_vhost("/staging")
            first_call_count = mock_http_client.request.call_count

            # Advance time by TTL + 1 second
            mock_time.time.return_value = 1000.0 + CACHE_TTL + 1

            # Should trigger new API call
            await validator.validate_vhost("/staging")

            # Should have made a second API call
            assert mock_http_client.request.call_count == first_call_count + 1

    @pytest.mark.asyncio
    async def test_cache_invalidation(self, validator, mock_http_client):
        """Test cache invalidation forces refresh and logs stats (AC #6)."""
        # Mock API response
        mock_http_client.request.return_value = [{"name": "/staging"}]

        # First validation
        await validator.validate_vhost("/staging")
        assert mock_http_client.request.call_count == 1

        # Invalidate cache - should log statistics
        validator.invalidate_cache()

        # Next validation should call API again
        await validator.validate_vhost("/staging")
        assert mock_http_client.request.call_count == 2

    @pytest.mark.asyncio
    async def test_correlation_id_propagation(self, validator, mock_http_client):
        """Test correlation_id is passed to HTTP client."""
        mock_http_client.request.return_value = [{"name": "/"}]
        correlation_id = "test-correlation-123"

        await validator.validate_vhost("/", correlation_id=correlation_id)

        # Verify correlation_id passed to request
        mock_http_client.request.assert_called_once_with(
            method="GET",
            endpoint="/vhosts",
            correlation_id=correlation_id,
        )

    @pytest.mark.asyncio
    async def test_concurrent_validations(self, validator, mock_http_client):
        """Test concurrent validations use lock to avoid race conditions (AC #4)."""

        # Mock API response with delay to simulate slow request
        async def slow_request(*args, **kwargs):
            await asyncio.sleep(0.05)
            return [{"name": "/staging"}]

        mock_http_client.request.side_effect = slow_request

        # Launch concurrent validations
        tasks = [validator.validate_vhost("/staging") for _ in range(5)]
        await asyncio.gather(*tasks)

        # Should only call API once despite concurrent requests
        assert mock_http_client.request.call_count == 1

    @pytest.mark.asyncio
    async def test_performance_under_50ms(self, validator, mock_http_client):
        """Test validation completes in <50ms (AC #3)."""
        # Mock fast API response
        mock_http_client.request.return_value = [{"name": "/staging"}]

        # First call to populate cache
        await validator.validate_vhost("/staging")

        # Measure cached validation performance
        start = time.perf_counter()
        await validator.validate_vhost("/staging")
        duration = (time.perf_counter() - start) * 1000  # Convert to ms

        # Cached validation should be very fast (<1ms typically)
        assert duration < 50, f"Validation took {duration}ms, expected <50ms"


class TestValidateVhostDecorator:
    """Test @validate_vhost decorator functionality."""

    @pytest.fixture
    def mock_http_client(self):
        """Create mock HTTP client."""
        client = AsyncMock()
        client.request = AsyncMock(return_value=[{"name": "/"}, {"name": "/test"}])
        return client

    @pytest.fixture
    def mock_operation(self, mock_http_client):
        """Create mock operation with http_client."""
        operation = Mock()
        operation.http_client = mock_http_client
        return operation

    @pytest.mark.asyncio
    async def test_decorator_with_vhost_kwarg(self, mock_operation):
        """Test decorator extracts vhost from kwargs."""

        @validate_vhost
        async def test_func(self, vhost: str, correlation_id: str | None = None):
            return f"executed with {vhost}"

        result = await test_func(mock_operation, vhost="/test")
        assert result == "executed with /test"

    @pytest.mark.asyncio
    async def test_decorator_with_request_object(self, mock_operation):
        """Test decorator extracts vhost from request object."""

        # Mock request object
        mock_request = Mock()
        mock_request.vhost = "/test"

        @validate_vhost
        async def test_func(self, request, correlation_id: str | None = None):
            return f"executed with {request.vhost}"

        result = await test_func(
            mock_operation, request=mock_request, correlation_id=None
        )
        assert result == "executed with /test"

    @pytest.mark.asyncio
    async def test_decorator_raises_on_invalid_vhost(self):
        """Test decorator raises VHostNotFoundError for invalid vhost."""
        # Note: This is already tested in test_validate_vhost_not_found
        # and in integration tests. The decorator just wraps the validator.
        # Testing via real operation introduces mock complexity.
        pytest.skip("Covered by validator unit tests and integration tests")

    @pytest.mark.asyncio
    async def test_decorator_missing_vhost(self, mock_operation):
        """Test decorator raises ValueError if vhost missing with improved message."""

        @validate_vhost
        async def test_func(self, correlation_id: str | None = None):
            return "executed"

        with pytest.raises(
            ValueError, match="vhost parameter is required for test_func"
        ):
            await test_func(mock_operation)

    @pytest.mark.asyncio
    async def test_decorator_caches_validator(self, mock_operation):
        """Test decorator creates and reuses validator instance."""

        @validate_vhost
        async def test_func(self, vhost: str, correlation_id: str | None = None):
            return "executed"

        # First call should create validator
        await test_func(mock_operation, vhost="/test")
        assert hasattr(mock_operation.http_client, "_vhost_validator")

        validator = mock_operation.http_client._vhost_validator

        # Second call should reuse validator
        await test_func(mock_operation, vhost="/test")
        assert mock_operation.http_client._vhost_validator is validator


class TestVhostValidatorEdgeCases:
    """Test edge cases and error handling."""

    @pytest.fixture
    def mock_http_client(self):
        """Create mock HTTP client."""
        client = AsyncMock()
        client.request = AsyncMock()
        return client

    @pytest.fixture
    def validator(self, mock_http_client):
        """Create VhostValidator instance."""
        return VhostValidator(mock_http_client)

    @pytest.mark.asyncio
    async def test_api_error_propagation(self, validator, mock_http_client):
        """Test API errors are propagated."""
        # Mock API error
        mock_http_client.request.side_effect = Exception("API Error")

        with pytest.raises(Exception, match="API Error"):
            await validator.validate_vhost("/test")

    @pytest.mark.asyncio
    async def test_empty_vhost_list(self, validator, mock_http_client):
        """Test handling of empty vhost list."""
        mock_http_client.request.return_value = []

        with pytest.raises(VHostNotFoundError) as exc_info:
            await validator.validate_vhost("/test")

        assert "does not exist" in str(exc_info.value)
        assert "Available vhosts: []" in str(exc_info.value)

    @pytest.mark.asyncio
    async def test_default_vhost(self, validator, mock_http_client):
        """Test validation of default vhost '/'."""
        mock_http_client.request.return_value = [{"name": "/"}]

        # Should validate successfully
        await validator.validate_vhost("/")

    @pytest.mark.asyncio
    async def test_vhost_with_special_characters(self, validator, mock_http_client):
        """Test vhosts with special characters."""
        mock_http_client.request.return_value = [
            {"name": "/"},
            {"name": "/test-vhost"},
            {"name": "/prod_env"},
        ]

        await validator.validate_vhost("/test-vhost")
        await validator.validate_vhost("/prod_env")

    @pytest.mark.asyncio
    async def test_validation_timeout(self, validator, mock_http_client):
        """Test validation timeout protection (AC #3)."""

        # Mock slow API response that exceeds timeout
        async def slow_request(*args, **kwargs):
            await asyncio.sleep(15)  # Exceeds VALIDATION_TIMEOUT
            return [{"name": "/"}]

        mock_http_client.request.side_effect = slow_request

        # Should raise TimeoutError
        with pytest.raises(asyncio.TimeoutError):
            await validator.validate_vhost("/test")
