"""
Performance and load tests for MCP Weather Server.
"""

import pytest
import asyncio
import time
from unittest.mock import AsyncMock, Mock, patch
from src.mcp_weather_server.server import (
    register_all_tools,
    call_tool,
    tool_handlers
)


class TestPerformance:
    """Performance tests for the weather server."""

    def setup_method(self):
        """Setup for each test."""
        global tool_handlers
        tool_handlers.clear()
        register_all_tools()

    @pytest.mark.asyncio
    async def test_weather_request_performance(self):
        """Test that weather requests complete within reasonable time."""
        mock_geo_response = {
            "results": [{"latitude": 40.7128, "longitude": -74.0060}]
        }
        mock_weather_response = {
            "hourly": {
                "time": ["2024-01-01T12:00"],
                "temperature_2m": [20.0],
                "relative_humidity_2m": [60],
                "dew_point_2m": [12.0],
                "weather_code": [0]
            }
        }

        with patch('httpx.AsyncClient') as mock_client_class:
            mock_client = AsyncMock()

            def mock_get(url):
                response = Mock()
                response.status_code = 200
                if "geocoding-api" in url:
                    response.json.return_value = mock_geo_response
                else:
                    response.json.return_value = mock_weather_response
                return response

            mock_client.get.side_effect = mock_get
            mock_client_class.return_value.__aenter__.return_value = mock_client

            with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0):
                start_time = time.time()
                result = await call_tool("get_current_weather", {"city": "New York"})
                end_time = time.time()

                # Request should complete in less than 1 second (mocked)
                assert end_time - start_time < 1.0
                assert len(result) == 1
                assert not result[0].text.startswith("Error:")

    @pytest.mark.asyncio
    async def test_concurrent_request_performance(self):
        """Test performance with multiple concurrent requests."""
        mock_geo_response = {
            "results": [{"latitude": 40.7128, "longitude": -74.0060}]
        }
        mock_weather_response = {
            "hourly": {
                "time": ["2024-01-01T12:00"],
                "temperature_2m": [20.0],
                "relative_humidity_2m": [60],
                "dew_point_2m": [12.0],
                "weather_code": [0]
            }
        }

        with patch('httpx.AsyncClient') as mock_client_class:
            mock_client = AsyncMock()

            def mock_get(url):
                response = Mock()
                response.status_code = 200
                if "geocoding-api" in url:
                    response.json.return_value = mock_geo_response
                else:
                    response.json.return_value = mock_weather_response
                return response

            mock_client.get.side_effect = mock_get
            mock_client_class.return_value.__aenter__.return_value = mock_client

            with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0):
                # Create 10 concurrent requests
                tasks = [
                    call_tool("get_current_weather", {"city": f"City{i}"})
                    for i in range(10)
                ]

                start_time = time.time()
                results = await asyncio.gather(*tasks)
                end_time = time.time()

                # All requests should complete in reasonable time
                assert end_time - start_time < 2.0
                assert len(results) == 10

                # All requests should succeed
                for result in results:
                    assert len(result) == 1
                    assert not result[0].text.startswith("Error:")

    @pytest.mark.asyncio
    async def test_time_tool_performance(self):
        """Test performance of time-related tools."""
        from datetime import datetime
        from zoneinfo import ZoneInfo

        fixed_time = datetime(2024, 1, 1, 12, 0, 0, tzinfo=ZoneInfo("UTC"))

        with patch('src.mcp_weather_server.utils.get_zoneinfo') as mock_get_tz:
            mock_get_tz.return_value = ZoneInfo("UTC")
            with patch('src.mcp_weather_server.tools.tools_time.datetime') as mock_datetime:
                mock_datetime.now.return_value = fixed_time

                start_time = time.time()
                result = await call_tool("get_current_datetime", {"timezone_name": "UTC"})
                end_time = time.time()

                # Time tools should be very fast
                assert end_time - start_time < 0.1
                assert len(result) == 1
                assert not result[0].text.startswith("Error:")

    @pytest.mark.asyncio
    async def test_memory_usage_with_many_requests(self):
        """Test that memory usage doesn't grow excessively with many requests."""
        import gc
        import sys

        # Get initial memory usage (approximation)
        gc.collect()
        initial_objects = len(gc.get_objects())

        mock_geo_response = {
            "results": [{"latitude": 40.7128, "longitude": -74.0060}]
        }
        mock_weather_response = {
            "hourly": {
                "time": ["2024-01-01T12:00"],
                "temperature_2m": [20.0],
                "relative_humidity_2m": [60],
                "dew_point_2m": [12.0],
                "weather_code": [0]
            }
        }

        with patch('httpx.AsyncClient') as mock_client_class:
            mock_client = AsyncMock()

            def mock_get(url):
                response = Mock()
                response.status_code = 200
                if "geocoding-api" in url:
                    response.json.return_value = mock_geo_response
                else:
                    response.json.return_value = mock_weather_response
                return response

            mock_client.get.side_effect = mock_get
            mock_client_class.return_value.__aenter__.return_value = mock_client

            with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0):
                # Make many sequential requests
                for i in range(50):
                    result = await call_tool("get_current_weather", {"city": f"City{i}"})
                    assert len(result) == 1
                    assert not result[0].text.startswith("Error:")

                # Force garbage collection and check memory
                gc.collect()
                final_objects = len(gc.get_objects())

                # Object count shouldn't grow dramatically
                # Allow some growth but not excessive (factor of 2)
                assert final_objects < initial_objects * 2


class TestLoadTesting:
    """Load testing scenarios."""

    def setup_method(self):
        """Setup for each test."""
        global tool_handlers
        tool_handlers.clear()
        register_all_tools()

    @pytest.mark.asyncio
    async def test_burst_load_handling(self):
        """Test handling of burst load (many requests at once)."""
        mock_geo_response = {
            "results": [{"latitude": 40.7128, "longitude": -74.0060}]
        }
        mock_weather_response = {
            "hourly": {
                "time": ["2024-01-01T12:00"],
                "temperature_2m": [20.0],
                "relative_humidity_2m": [60],
                "dew_point_2m": [12.0],
                "weather_code": [0]
            }
        }

        with patch('httpx.AsyncClient') as mock_client_class:
            mock_client = AsyncMock()

            def mock_get(url):
                response = Mock()
                response.status_code = 200
                if "geocoding-api" in url:
                    response.json.return_value = mock_geo_response
                else:
                    response.json.return_value = mock_weather_response
                return response

            mock_client.get.side_effect = mock_get
            mock_client_class.return_value.__aenter__.return_value = mock_client

            with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0):
                # Create a large burst of requests (simulate high load)
                burst_size = 25
                tasks = [
                    call_tool("get_current_weather", {"city": f"BurstCity{i}"})
                    for i in range(burst_size)
                ]

                start_time = time.time()
                results = await asyncio.gather(*tasks, return_exceptions=True)
                end_time = time.time()

                # Check that most requests succeeded
                successful_results = [r for r in results if not isinstance(r, Exception)]
                error_results = [r for r in results if isinstance(r, Exception)]

                # At least 80% should succeed under load
                success_rate = len(successful_results) / len(results)
                assert success_rate >= 0.8, f"Success rate {success_rate} too low"

                # Total time should be reasonable even under load
                assert end_time - start_time < 5.0

    @pytest.mark.asyncio
    async def test_mixed_tool_load(self):
        """Test load with mixed tool types."""
        from datetime import datetime
        from zoneinfo import ZoneInfo

        # Setup weather mocks
        mock_geo_response = {
            "results": [{"latitude": 40.7128, "longitude": -74.0060}]
        }
        mock_weather_response = {
            "hourly": {
                "time": ["2024-01-01T12:00"],
                "temperature_2m": [20.0],
                "relative_humidity_2m": [60],
                "dew_point_2m": [12.0],
                "weather_code": [0]
            }
        }

        # Setup time mocks
        fixed_time = datetime(2024, 1, 1, 12, 0, 0, tzinfo=ZoneInfo("UTC"))

        with patch('httpx.AsyncClient') as mock_client_class:
            mock_client = AsyncMock()

            def mock_get(url):
                response = Mock()
                response.status_code = 200
                if "geocoding-api" in url:
                    response.json.return_value = mock_geo_response
                else:
                    response.json.return_value = mock_weather_response
                return response

            mock_client.get.side_effect = mock_get
            mock_client_class.return_value.__aenter__.return_value = mock_client

            with patch('src.mcp_weather_server.utils.get_zoneinfo') as mock_get_tz:
                mock_get_tz.return_value = ZoneInfo("UTC")
                with patch('src.mcp_weather_server.tools.tools_time.datetime') as mock_datetime:
                    mock_datetime.now.return_value = fixed_time
                    with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0):

                        # Create mixed requests
                        tasks = []
                        for i in range(20):
                            if i % 3 == 0:
                                tasks.append(call_tool("get_current_weather", {"city": f"City{i}"}))
                            elif i % 3 == 1:
                                tasks.append(call_tool("get_current_datetime", {"timezone_name": "UTC"}))
                            else:
                                tasks.append(call_tool("get_timezone_info", {"timezone_name": "UTC"}))

                        start_time = time.time()
                        results = await asyncio.gather(*tasks, return_exceptions=True)
                        end_time = time.time()

                        # Check results
                        successful_results = [r for r in results if not isinstance(r, Exception)]
                        success_rate = len(successful_results) / len(results)

                        assert success_rate >= 0.9, f"Mixed load success rate {success_rate} too low"
                        assert end_time - start_time < 3.0
                        assert len(results) == 20


class TestStressConditions:
    """Stress testing under adverse conditions."""

    def setup_method(self):
        """Setup for each test."""
        global tool_handlers
        tool_handlers.clear()
        register_all_tools()

    @pytest.mark.asyncio
    async def test_high_error_rate_resilience(self):
        """Test resilience when external APIs have high error rates."""
        error_count = 0
        success_count = 0

        def mock_get_with_errors(url):
            nonlocal error_count, success_count
            response = Mock()

            # Simulate 30% error rate
            if (error_count + success_count) % 10 < 3:
                error_count += 1
                response.status_code = 500
            else:
                success_count += 1
                response.status_code = 200
                if "geocoding-api" in url:
                    response.json.return_value = {
                        "results": [{"latitude": 40.7128, "longitude": -74.0060}]
                    }
                else:
                    response.json.return_value = {
                        "hourly": {
                            "time": ["2024-01-01T12:00"],
                            "temperature_2m": [20.0],
                            "relative_humidity_2m": [60],
                            "dew_point_2m": [12.0],
                            "weather_code": [0]
                        }
                    }
            return response

        with patch('httpx.AsyncClient') as mock_client_class:
            mock_client = AsyncMock()
            mock_client.get.side_effect = mock_get_with_errors
            mock_client_class.return_value.__aenter__.return_value = mock_client

            with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0):
                # Make requests despite high error rate
                tasks = [
                    call_tool("get_current_weather", {"city": f"ErrorTestCity{i}"})
                    for i in range(20)
                ]

                results = await asyncio.gather(*tasks, return_exceptions=True)

                # Count successes and errors
                successful_results = []
                error_results = []

                for result in results:
                    if isinstance(result, Exception):
                        error_results.append(result)
                    else:
                        if result[0].text.startswith("Error:"):
                            error_results.append(result)
                        else:
                            successful_results.append(result)

                # Should handle errors gracefully without crashing
                assert len(results) == 20
                # Some should succeed despite errors
                assert len(successful_results) > 0
                # Error handling should be proper
                for error_result in error_results:
                    if not isinstance(error_result, Exception):
                        assert "Error:" in error_result[0].text
