# -*- coding: utf-8 -*-
"""Location: ./tests/unit/mcpgateway/services/test_performance_service.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Tests for the Performance Monitoring Service.
"""

# Standard
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch, AsyncMock
import os

# Third-Party
import pytest

# First-Party
from mcpgateway.services.performance_service import (
    PerformanceService,
    get_performance_service,
    PSUTIL_AVAILABLE,
    REDIS_AVAILABLE,
    PROMETHEUS_AVAILABLE,
    APP_START_TIME,
    HOSTNAME,
)
from mcpgateway.schemas import (
    SystemMetricsSchema,
    WorkerMetrics,
    GunicornMetricsSchema,
    RequestMetricsSchema,
    DatabaseMetricsSchema,
    CacheMetricsSchema,
    PerformanceDashboard,
    PerformanceHistoryResponse,
)


class TestPerformanceServiceInit:
    """Tests for PerformanceService initialization."""

    def test_init_without_db(self):
        """Test initialization without database session."""
        service = PerformanceService()
        assert service.db is None
        assert isinstance(service._request_count_cache, dict)
        assert service._last_request_time > 0

    def test_init_with_db(self):
        """Test initialization with database session."""
        mock_db = MagicMock()
        service = PerformanceService(db=mock_db)
        assert service.db is mock_db


class TestSystemMetrics:
    """Tests for system metrics collection."""

    def test_get_system_metrics_without_psutil(self):
        """Test system metrics when psutil is not available."""
        service = PerformanceService()
        with patch("mcpgateway.services.performance_service.PSUTIL_AVAILABLE", False):
            with patch("mcpgateway.services.performance_service.psutil", None):
                result = service.get_system_metrics()
                assert isinstance(result, SystemMetricsSchema)
                assert result.cpu_percent == 0.0
                assert result.memory_total_mb == 0

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_system_metrics_with_psutil(self):
        """Test system metrics collection with psutil available."""
        service = PerformanceService()
        result = service.get_system_metrics()

        assert isinstance(result, SystemMetricsSchema)
        assert result.cpu_percent >= 0
        assert result.cpu_count > 0
        assert result.memory_total_mb > 0
        assert result.memory_used_mb >= 0
        assert result.memory_available_mb >= 0
        assert result.disk_total_gb > 0
        assert result.disk_used_gb >= 0

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_system_metrics_load_average(self):
        """Test load average is captured on Unix systems."""
        service = PerformanceService()
        result = service.get_system_metrics()

        # Load average is only available on Unix
        if os.name != "nt":
            # May be None if getloadavg fails
            if result.load_avg_1m is not None:
                assert result.load_avg_1m >= 0

    def test_get_system_metrics_load_average_exception_sets_none(self):
        """When os.getloadavg fails, load averages should be None."""
        service = PerformanceService()

        mock_psutil = MagicMock()
        mock_psutil.cpu_percent.return_value = 1.0
        mock_psutil.cpu_count.return_value = 1
        mock_psutil.cpu_freq.return_value = None
        mock_psutil.virtual_memory.return_value = MagicMock(total=1_048_576, used=0, available=1_048_576, percent=0.0)
        mock_psutil.swap_memory.return_value = MagicMock(total=0, used=0)
        mock_psutil.disk_usage.return_value = MagicMock(total=1_073_741_824, used=0, percent=0.0)
        mock_psutil.net_io_counters.return_value = MagicMock(bytes_sent=0, bytes_recv=0)
        mock_psutil.boot_time.return_value = 0

        with patch("mcpgateway.services.performance_service.PSUTIL_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.psutil", mock_psutil):
                with patch.object(service, "_get_net_connections_cached", return_value=0):
                    with patch("mcpgateway.services.performance_service.os.getloadavg", side_effect=OSError("nope")):
                        result = service.get_system_metrics()

        assert result.load_avg_1m is None
        assert result.load_avg_5m is None
        assert result.load_avg_15m is None


class TestWorkerMetrics:
    """Tests for worker process metrics collection."""

    def test_get_worker_metrics_without_psutil(self):
        """Test worker metrics when psutil is not available."""
        service = PerformanceService()
        with patch("mcpgateway.services.performance_service.PSUTIL_AVAILABLE", False):
            result = service.get_worker_metrics()
            assert isinstance(result, list)
            assert len(result) == 0

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_worker_metrics_with_psutil(self):
        """Test worker metrics collection with psutil available."""
        service = PerformanceService()
        result = service.get_worker_metrics()

        assert isinstance(result, list)
        assert len(result) >= 1

        worker = result[0]
        assert isinstance(worker, WorkerMetrics)
        assert worker.pid > 0
        assert worker.memory_rss_mb >= 0
        assert worker.threads >= 1

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_worker_metrics_under_gunicorn_uses_parent_children(self):
        import psutil

        service = PerformanceService()

        mock_child1 = MagicMock(spec=psutil.Process)
        mock_child2 = MagicMock(spec=psutil.Process)
        mock_parent = MagicMock(spec=psutil.Process)
        mock_parent.name.return_value = "gunicorn: master"
        mock_parent.children.return_value = [mock_child1, mock_child2]

        mock_current = MagicMock(spec=psutil.Process)
        mock_current.parent.return_value = mock_parent

        with patch("mcpgateway.services.performance_service.psutil.Process", return_value=mock_current):
            with patch.object(
                service,
                "_get_process_metrics",
                side_effect=[
                    WorkerMetrics(pid=1, cpu_percent=0.0, memory_rss_mb=0.0, memory_vms_mb=0.0, threads=1),
                    WorkerMetrics(pid=2, cpu_percent=0.0, memory_rss_mb=0.0, memory_vms_mb=0.0, threads=1),
                ],
            ) as mock_pm:
                workers = service.get_worker_metrics()

        assert [w.pid for w in workers] == [1, 2]
        assert mock_pm.call_count == 2

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_worker_metrics_parent_access_denied_falls_back_to_current(self):
        import psutil

        service = PerformanceService()

        mock_current = MagicMock(spec=psutil.Process)
        mock_current.parent.side_effect = psutil.AccessDenied()

        with patch("mcpgateway.services.performance_service.psutil.Process", return_value=mock_current):
            with patch.object(
                service,
                "_get_process_metrics",
                return_value=WorkerMetrics(pid=123, cpu_percent=0.0, memory_rss_mb=0.0, memory_vms_mb=0.0, threads=1),
            ) as mock_pm:
                workers = service.get_worker_metrics()

        assert [w.pid for w in workers] == [123]
        mock_pm.assert_called_once_with(mock_current)

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_process_metrics(self):
        """Test metrics collection for a specific process."""
        import psutil

        service = PerformanceService()
        proc = psutil.Process()
        result = service._get_process_metrics(proc)

        assert isinstance(result, WorkerMetrics)
        assert result.pid == os.getpid()
        assert result.cpu_percent >= 0
        assert result.memory_rss_mb >= 0
        assert result.threads >= 1

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_process_metrics_open_fds_and_connections_fail(self):
        import psutil
        import time

        service = PerformanceService()

        mock_proc = MagicMock(spec=psutil.Process)
        mock_proc.pid = 12345

        mock_oneshot = MagicMock()
        mock_oneshot.__enter__.return_value = None
        mock_oneshot.__exit__.return_value = False
        mock_proc.oneshot.return_value = mock_oneshot

        mock_proc.memory_info.return_value = MagicMock(rss=0, vms=0)
        mock_proc.create_time.return_value = time.time()
        mock_proc.num_fds.side_effect = AttributeError("no fds")
        mock_proc.net_connections = None
        mock_proc.connections.side_effect = psutil.AccessDenied()
        mock_proc.cpu_percent.return_value = 0.0
        mock_proc.num_threads.return_value = 1
        mock_proc.status.return_value = "running"

        result = service._get_process_metrics(mock_proc)

        assert result.open_fds is None
        assert result.connections == 0

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_process_metrics_with_exception(self):
        """Test process metrics when process access fails."""
        import psutil

        service = PerformanceService()
        mock_proc = MagicMock(spec=psutil.Process)
        mock_proc.pid = 99999
        mock_proc.oneshot.side_effect = psutil.NoSuchProcess(99999)

        result = service._get_process_metrics(mock_proc)

        assert isinstance(result, WorkerMetrics)
        assert result.pid == 99999
        assert result.status == "unknown"


class TestGunicornMetrics:
    """Tests for Gunicorn-specific metrics."""

    def test_get_gunicorn_metrics_without_psutil(self):
        """Test Gunicorn metrics when psutil is not available."""
        service = PerformanceService()
        with patch("mcpgateway.services.performance_service.PSUTIL_AVAILABLE", False):
            result = service.get_gunicorn_metrics()
            assert isinstance(result, GunicornMetricsSchema)

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_gunicorn_metrics_not_under_gunicorn(self):
        """Test Gunicorn metrics when not running under Gunicorn."""
        service = PerformanceService()
        result = service.get_gunicorn_metrics()

        assert isinstance(result, GunicornMetricsSchema)
        # When not under gunicorn, master_pid should be None
        assert result.workers_total >= 1

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_gunicorn_metrics_mocked_gunicorn(self):
        """Test Gunicorn metrics with mocked Gunicorn environment."""
        import psutil

        service = PerformanceService()

        mock_parent = MagicMock(spec=psutil.Process)
        mock_parent.pid = 1000
        mock_parent.name.return_value = "gunicorn: master"
        mock_parent.children.return_value = [
            MagicMock(pid=1001, cpu_percent=MagicMock(return_value=10.0)),
            MagicMock(pid=1002, cpu_percent=MagicMock(return_value=0.0)),
        ]

        mock_current = MagicMock(spec=psutil.Process)
        mock_current.parent.return_value = mock_parent

        with patch("mcpgateway.services.performance_service.psutil.Process", return_value=mock_current):
            result = service.get_gunicorn_metrics()

        assert isinstance(result, GunicornMetricsSchema)
        assert result.master_pid == 1000
        assert result.workers_total == 2

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_gunicorn_metrics_ignores_child_cpu_percent_errors(self):
        import psutil

        service = PerformanceService()

        good_child = MagicMock(pid=1001, cpu_percent=MagicMock(return_value=0.0))
        bad_child = MagicMock(pid=1002)
        bad_child.cpu_percent.side_effect = psutil.AccessDenied()

        mock_parent = MagicMock(spec=psutil.Process)
        mock_parent.pid = 1000
        mock_parent.name.return_value = "gunicorn: master"
        mock_parent.children.return_value = [good_child, bad_child]

        mock_current = MagicMock(spec=psutil.Process)
        mock_current.parent.return_value = mock_parent

        with patch("mcpgateway.services.performance_service.psutil.Process", return_value=mock_current):
            result = service.get_gunicorn_metrics()

        assert result.master_pid == 1000
        assert result.workers_total == 2

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_get_gunicorn_metrics_parent_access_denied_falls_back(self):
        import psutil

        service = PerformanceService()

        mock_current = MagicMock(spec=psutil.Process)
        mock_current.parent.side_effect = psutil.AccessDenied()

        with patch("mcpgateway.services.performance_service.psutil.Process", return_value=mock_current):
            result = service.get_gunicorn_metrics()

        assert result.master_pid is None


class TestRequestMetrics:
    """Tests for HTTP request metrics."""

    def test_get_request_metrics_without_prometheus(self):
        """Test request metrics when Prometheus is not available."""
        service = PerformanceService()
        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", False):
            result = service.get_request_metrics()
            assert isinstance(result, RequestMetricsSchema)
            assert result.requests_total == 0

    def test_get_request_metrics_empty_registry(self):
        """Test request metrics with empty Prometheus registry."""
        service = PerformanceService()

        mock_registry = MagicMock()
        mock_registry.collect.return_value = []

        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.REGISTRY", mock_registry):
                result = service.get_request_metrics()

        assert isinstance(result, RequestMetricsSchema)
        assert result.requests_total == 0

    def test_get_request_metrics_with_data(self):
        """Test request metrics with Prometheus data."""
        service = PerformanceService()

        # Mock Prometheus metrics
        mock_sample = MagicMock()
        mock_sample.name = "http_requests_total"
        mock_sample.labels = {"status": "200"}
        mock_sample.value = 100

        mock_metric = MagicMock()
        mock_metric.name = "http_requests_total"
        mock_metric.samples = [mock_sample]

        mock_registry = MagicMock()
        mock_registry.collect.return_value = [mock_metric]

        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.REGISTRY", mock_registry):
                result = service.get_request_metrics()

        assert isinstance(result, RequestMetricsSchema)
        assert result.requests_total == 100
        assert result.requests_2xx == 100


class TestDatabaseMetrics:
    """Tests for database connection pool metrics."""

    def test_get_database_metrics(self):
        """Test database metrics collection."""
        service = PerformanceService()

        mock_pool = MagicMock()
        mock_pool.size.return_value = 10
        mock_pool.checkedout.return_value = 3
        mock_pool.checkedin.return_value = 7
        mock_pool.overflow.return_value = 0

        mock_engine = MagicMock()
        mock_engine.pool = mock_pool

        with patch("mcpgateway.services.performance_service.engine", mock_engine, create=True):
            # Import happens inside the function, so we need to patch it there
            with patch.dict("sys.modules", {"mcpgateway.db": MagicMock(engine=mock_engine)}):
                result = service.get_database_metrics()

        assert isinstance(result, DatabaseMetricsSchema)

    def test_get_database_metrics_with_exception(self):
        """Test database metrics when pool access fails."""
        service = PerformanceService()

        # Patch the import that happens inside the function
        mock_module = MagicMock()
        mock_module.engine.pool.size.side_effect = Exception("Connection error")

        with patch.dict("sys.modules", {"mcpgateway.db": mock_module}):
            # Force the function to re-import by clearing any cached reference
            result = service.get_database_metrics()

        # The function handles the exception and returns defaults
        assert isinstance(result, DatabaseMetricsSchema)


class TestCacheMetrics:
    """Tests for Redis cache metrics."""

    @pytest.mark.asyncio
    async def test_get_cache_metrics_without_redis(self):
        """Test cache metrics when Redis is not available."""
        service = PerformanceService()
        with patch("mcpgateway.services.performance_service.REDIS_AVAILABLE", False):
            result = await service.get_cache_metrics()
            assert isinstance(result, CacheMetricsSchema)
            assert result.connected is False

    @pytest.mark.asyncio
    async def test_get_cache_metrics_no_redis_url(self):
        """Test cache metrics when Redis URL is not configured."""
        service = PerformanceService()

        with patch("mcpgateway.services.performance_service.settings") as mock_settings:
            mock_settings.redis_url = None
            result = await service.get_cache_metrics()

        assert isinstance(result, CacheMetricsSchema)
        assert result.connected is False

    @pytest.mark.asyncio
    async def test_get_cache_metrics_with_redis(self):
        """Test cache metrics with Redis connection via shared factory."""
        service = PerformanceService()

        # Create a proper async mock for the Redis client
        mock_client = AsyncMock()
        mock_client.info = AsyncMock(
            return_value={
                "redis_version": "7.0.0",
                "used_memory": 1048576,
                "connected_clients": 5,
                "instantaneous_ops_per_sec": 100,
                "keyspace_hits": 1000,
                "keyspace_misses": 100,
            }
        )

        async def mock_get_redis_client():
            return mock_client

        with patch("mcpgateway.services.performance_service.REDIS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.get_redis_client", mock_get_redis_client):
                with patch("mcpgateway.services.performance_service.settings") as mock_settings:
                    mock_settings.redis_url = "redis://localhost:6379"
                    mock_settings.cache_type = "redis"
                    result = await service.get_cache_metrics()

        assert isinstance(result, CacheMetricsSchema)
        assert result.connected is True
        assert result.version == "7.0.0"
        assert result.used_memory_mb > 0
        assert result.hit_rate > 0

    @pytest.mark.asyncio
    async def test_get_cache_metrics_returns_default_when_no_client(self):
        """When Redis client factory returns None, metrics should stay default."""
        service = PerformanceService()

        async def mock_get_redis_client():
            return None

        with patch("mcpgateway.services.performance_service.REDIS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.get_redis_client", mock_get_redis_client):
                with patch("mcpgateway.services.performance_service.settings") as mock_settings:
                    mock_settings.redis_url = "redis://localhost:6379"
                    mock_settings.cache_type = "redis"
                    result = await service.get_cache_metrics()

        assert isinstance(result, CacheMetricsSchema)
        assert result.connected is False

    @pytest.mark.asyncio
    async def test_get_cache_metrics_exception_sets_connected_false(self):
        """Errors collecting Redis metrics should be swallowed and connected set to False."""
        service = PerformanceService()

        mock_client = AsyncMock()
        mock_client.info = AsyncMock(side_effect=RuntimeError("boom"))

        async def mock_get_redis_client():
            return mock_client

        with patch("mcpgateway.services.performance_service.REDIS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.get_redis_client", mock_get_redis_client):
                with patch("mcpgateway.services.performance_service.settings") as mock_settings:
                    mock_settings.redis_url = "redis://localhost:6379"
                    mock_settings.cache_type = "redis"
                    result = await service.get_cache_metrics()

        assert result.connected is False


class TestDashboard:
    """Tests for the complete performance dashboard."""

    @pytest.mark.asyncio
    async def test_get_dashboard(self):
        """Test complete dashboard generation."""
        service = PerformanceService()

        # Mock all the individual metric methods
        with patch.object(service, "get_system_metrics") as mock_sys:
            with patch.object(service, "get_request_metrics") as mock_req:
                with patch.object(service, "get_database_metrics") as mock_db:
                    with patch.object(service, "get_cache_metrics", new_callable=AsyncMock) as mock_cache:
                        with patch.object(service, "get_gunicorn_metrics") as mock_guni:
                            with patch.object(service, "get_worker_metrics") as mock_workers:
                                mock_sys.return_value = SystemMetricsSchema(
                                    cpu_percent=10.0,
                                    cpu_count=4,
                                    memory_total_mb=8000,
                                    memory_used_mb=4000,
                                    memory_available_mb=4000,
                                    memory_percent=50.0,
                                    disk_total_gb=100.0,
                                    disk_used_gb=50.0,
                                    disk_percent=50.0,
                                )
                                mock_req.return_value = RequestMetricsSchema()
                                mock_db.return_value = DatabaseMetricsSchema()
                                mock_cache.return_value = CacheMetricsSchema()
                                mock_guni.return_value = GunicornMetricsSchema()
                                mock_workers.return_value = []

                                result = await service.get_dashboard()

        assert isinstance(result, PerformanceDashboard)
        assert result.uptime_seconds >= 0
        assert result.host == HOSTNAME
        assert result.system.cpu_percent == 10.0


class TestSnapshotOperations:
    """Tests for snapshot save and cleanup operations."""

    def test_save_snapshot(self, test_db):
        """Test saving a performance snapshot."""
        service = PerformanceService(db=test_db)

        # Mock the metrics methods to return predictable data
        with patch.object(service, "get_system_metrics") as mock_sys:
            with patch.object(service, "get_request_metrics") as mock_req:
                with patch.object(service, "get_database_metrics") as mock_db:
                    with patch.object(service, "get_gunicorn_metrics") as mock_guni:
                        with patch.object(service, "get_worker_metrics") as mock_workers:
                            mock_sys.return_value = SystemMetricsSchema(
                                cpu_percent=10.0,
                                cpu_count=4,
                                memory_total_mb=8000,
                                memory_used_mb=4000,
                                memory_available_mb=4000,
                                memory_percent=50.0,
                                disk_total_gb=100.0,
                                disk_used_gb=50.0,
                                disk_percent=50.0,
                            )
                            mock_req.return_value = RequestMetricsSchema()
                            mock_db.return_value = DatabaseMetricsSchema()
                            mock_guni.return_value = GunicornMetricsSchema()
                            mock_workers.return_value = []

                            result = service.save_snapshot(test_db)

        assert result is not None
        assert result.host == HOSTNAME
        assert "system" in result.metrics_json
        assert "requests" in result.metrics_json

    def test_save_snapshot_converts_datetimes_to_isoformat(self):
        """save_snapshot should JSON-serialize datetimes as ISO 8601 strings."""
        service = PerformanceService()
        mock_db = MagicMock()

        boot_time = datetime(2026, 1, 1, tzinfo=timezone.utc)
        worker_create_time = datetime(2026, 1, 2, tzinfo=timezone.utc)

        system = SystemMetricsSchema(
            cpu_percent=0.0,
            cpu_count=1,
            memory_total_mb=0,
            memory_used_mb=0,
            memory_available_mb=0,
            memory_percent=0.0,
            disk_total_gb=0.0,
            disk_used_gb=0.0,
            disk_percent=0.0,
            boot_time=boot_time,
        )

        with patch.object(service, "get_system_metrics", return_value=system):
            with patch.object(service, "get_request_metrics", return_value=RequestMetricsSchema()):
                with patch.object(service, "get_database_metrics", return_value=DatabaseMetricsSchema()):
                    with patch.object(service, "get_gunicorn_metrics", return_value=GunicornMetricsSchema()):
                        with patch.object(
                            service,
                            "get_worker_metrics",
                            return_value=[WorkerMetrics(pid=1, cpu_percent=0.0, memory_rss_mb=0.0, memory_vms_mb=0.0, threads=1, create_time=worker_create_time)],
                        ):
                            service.save_snapshot(mock_db)

        snapshot = mock_db.add.call_args[0][0]
        assert snapshot.metrics_json["system"]["boot_time"] == boot_time.isoformat()
        assert snapshot.metrics_json["workers"][0]["create_time"] == worker_create_time.isoformat()

    def test_save_snapshot_with_exception(self, test_db):
        """Test snapshot save with database error."""
        service = PerformanceService()

        # Create a mock db that raises on commit
        mock_db = MagicMock()
        mock_db.commit.side_effect = Exception("Database error")

        with patch.object(service, "get_system_metrics") as mock_sys:
            with patch.object(service, "get_request_metrics") as mock_req:
                with patch.object(service, "get_database_metrics") as mock_dbm:
                    with patch.object(service, "get_gunicorn_metrics") as mock_guni:
                        with patch.object(service, "get_worker_metrics") as mock_workers:
                            mock_sys.return_value = SystemMetricsSchema(
                                cpu_percent=10.0,
                                cpu_count=4,
                                memory_total_mb=8000,
                                memory_used_mb=4000,
                                memory_available_mb=4000,
                                memory_percent=50.0,
                                disk_total_gb=100.0,
                                disk_used_gb=50.0,
                                disk_percent=50.0,
                            )
                            mock_req.return_value = RequestMetricsSchema()
                            mock_dbm.return_value = DatabaseMetricsSchema()
                            mock_guni.return_value = GunicornMetricsSchema()
                            mock_workers.return_value = []

                            result = service.save_snapshot(mock_db)

        assert result is None
        mock_db.rollback.assert_called_once()

    def test_cleanup_old_snapshots(self, test_db):
        """Test cleanup of old snapshots."""
        from mcpgateway.db import PerformanceSnapshot

        service = PerformanceService()

        # Create some old snapshots
        old_time = datetime.now(timezone.utc) - timedelta(hours=48)
        snapshot = PerformanceSnapshot(
            host="test-host",
            worker_id="1234",
            metrics_json={"test": "data"},
            timestamp=old_time,
        )
        test_db.add(snapshot)
        test_db.commit()

        # Run cleanup
        deleted = service.cleanup_old_snapshots(test_db)

        # Should have deleted the old snapshot
        assert deleted >= 0

    def test_cleanup_old_snapshots_with_exception(self):
        """Test cleanup with database error."""
        service = PerformanceService()

        mock_db = MagicMock()
        mock_db.execute.side_effect = Exception("Database error")

        deleted = service.cleanup_old_snapshots(mock_db)

        assert deleted == 0
        mock_db.rollback.assert_called_once()


class TestHistoryOperations:
    """Tests for historical data retrieval."""

    @pytest.mark.asyncio
    async def test_get_history_empty(self, test_db):
        """Test getting history when no data exists."""
        service = PerformanceService()
        result = await service.get_history(test_db)

        assert isinstance(result, PerformanceHistoryResponse)
        assert result.aggregates == []
        assert result.period_type == "hourly"

    @pytest.mark.asyncio
    async def test_get_history_with_filters(self, test_db):
        """Test getting history with filters."""
        service = PerformanceService()

        start_time = datetime.now(timezone.utc) - timedelta(hours=24)
        end_time = datetime.now(timezone.utc)

        result = await service.get_history(
            test_db,
            period_type="hourly",
            start_time=start_time,
            end_time=end_time,
            host="test-host",
            limit=100,
        )

        assert isinstance(result, PerformanceHistoryResponse)
        assert result.period_type == "hourly"

    @pytest.mark.asyncio
    async def test_get_history_cache_hit_returns_cached_value(self):
        service = PerformanceService()

        cached = PerformanceHistoryResponse(aggregates=[], period_type="hourly", total_count=0).model_dump()

        mock_cache = MagicMock()
        mock_cache.get_performance_history = AsyncMock(return_value=cached)

        with patch("mcpgateway.services.performance_service._get_admin_stats_cache", return_value=mock_cache):
            result = await service.get_history(MagicMock())

        assert result.period_type == "hourly"


class TestHourlyAggregate:
    """Tests for hourly aggregate creation."""

    def test_create_hourly_aggregate_no_snapshots(self, test_db):
        """Test creating aggregate when no snapshots exist."""
        from mcpgateway.db import PerformanceAggregate

        service = PerformanceService()

        # Use a very old hour that definitely has no snapshots
        hour_start = datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc)

        # Clean up any existing aggregate for this hour
        test_db.query(PerformanceAggregate).filter(PerformanceAggregate.period_start == hour_start, PerformanceAggregate.period_type == "hourly").delete()
        test_db.commit()

        result = service.create_hourly_aggregate(test_db, hour_start)

        assert result is None

    def test_create_hourly_aggregate_with_snapshots(self, test_db):
        """Test creating aggregate from snapshots."""
        from mcpgateway.db import PerformanceSnapshot, PerformanceAggregate

        service = PerformanceService()

        # Use a unique hour for this test to avoid conflicts
        hour_start = datetime(2021, 6, 15, 10, 0, 0, tzinfo=timezone.utc)
        snapshot_time = hour_start + timedelta(minutes=30)

        # Clean up any existing data for this hour
        test_db.query(PerformanceAggregate).filter(PerformanceAggregate.period_start == hour_start, PerformanceAggregate.period_type == "hourly").delete()
        test_db.query(PerformanceSnapshot).filter(PerformanceSnapshot.timestamp >= hour_start, PerformanceSnapshot.timestamp < hour_start + timedelta(hours=1)).delete()
        test_db.commit()

        snapshot = PerformanceSnapshot(
            host=HOSTNAME,
            worker_id="1234",
            timestamp=snapshot_time,
            metrics_json={
                "system": {"cpu_percent": 50.0, "memory_percent": 60.0},
                "requests": {
                    "requests_total": 1000,
                    "requests_2xx": 950,
                    "requests_4xx": 30,
                    "requests_5xx": 20,
                    "response_time_avg_ms": 100.0,
                    "requests_per_second": 10.0,
                },
            },
        )
        test_db.add(snapshot)
        test_db.commit()

        result = service.create_hourly_aggregate(test_db, hour_start)

        assert result is not None
        assert result.period_type == "hourly"
        assert result.requests_total == 1000
        assert result.avg_cpu_percent == 50.0

    def test_create_hourly_aggregate_with_exception(self):
        """Test aggregate creation with database error."""
        service = PerformanceService()

        mock_db = MagicMock()
        mock_db.query.side_effect = Exception("Database error")

        hour_start = datetime.now(timezone.utc)
        result = service.create_hourly_aggregate(mock_db, hour_start)

        assert result is None


class TestSingleton:
    """Tests for the singleton service getter."""

    def test_get_performance_service_singleton(self):
        """Test that get_performance_service returns a singleton."""
        # Reset the singleton
        import mcpgateway.services.performance_service as ps

        ps._performance_service = None

        service1 = get_performance_service()
        service2 = get_performance_service()

        assert service1 is service2

    def test_get_performance_service_with_db(self):
        """Test that get_performance_service updates db session."""
        import mcpgateway.services.performance_service as ps

        ps._performance_service = None

        mock_db = MagicMock()
        service = get_performance_service(db=mock_db)

        assert service.db is mock_db

        # Update with new db
        mock_db2 = MagicMock()
        service2 = get_performance_service(db=mock_db2)

        assert service is service2
        assert service.db is mock_db2


class TestNetConnectionsCache:
    """Tests for net_connections caching functionality."""

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_net_connections_disabled_returns_zero(self):
        """Test that disabled net_connections returns 0."""
        import mcpgateway.services.performance_service as ps

        service = PerformanceService()

        with patch.object(ps.settings, "mcpgateway_performance_net_connections_enabled", False):
            result = service._get_net_connections_cached()

        assert result == 0

    def test_net_connections_without_psutil_returns_zero(self):
        """Test that without psutil, net_connections returns 0."""
        import mcpgateway.services.performance_service as ps

        service = PerformanceService()

        with patch.object(ps, "PSUTIL_AVAILABLE", False):
            with patch.object(ps, "psutil", None):
                result = service._get_net_connections_cached()

        assert result == 0

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_net_connections_cache_returns_cached_value(self):
        """Test that cache returns cached value within TTL."""
        import mcpgateway.services.performance_service as ps
        import time

        service = PerformanceService()

        # Set up cache with a known value
        ps._net_connections_cache = 42
        ps._net_connections_cache_time = time.time()  # Just set

        with patch.object(ps.settings, "mcpgateway_performance_net_connections_enabled", True):
            with patch.object(ps.settings, "mcpgateway_performance_net_connections_cache_ttl", 15):
                result = service._get_net_connections_cached()

        assert result == 42

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_net_connections_cache_double_check_returns_cached_after_lock(self):
        """Covers the double-check locking fast-return path inside the lock."""
        import mcpgateway.services.performance_service as ps
        import psutil

        service = PerformanceService()

        ps._net_connections_cache = 42
        ps._net_connections_cache_time = 0.0

        class FakeLock:
            def __enter__(self):
                # Simulate another thread refreshing the cache while we waited for the lock.
                ps._net_connections_cache_time = 1000.0
                return self

            def __exit__(self, exc_type, exc, tb):
                return False

        with patch.object(ps.settings, "mcpgateway_performance_net_connections_enabled", True):
            with patch.object(ps.settings, "mcpgateway_performance_net_connections_cache_ttl", 15):
                with patch.object(ps, "_net_connections_lock", FakeLock()):
                    with patch.object(ps.time, "time", return_value=1000.0):
                        with patch.object(psutil, "net_connections") as mock_nc:
                            result = service._get_net_connections_cached()

        assert result == 42
        mock_nc.assert_not_called()

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_net_connections_cache_refreshes_when_expired(self):
        """Test that cache refreshes when TTL expires."""
        import mcpgateway.services.performance_service as ps
        import time
        import psutil

        service = PerformanceService()

        # Set up cache as expired (20 seconds ago with 15 second TTL)
        ps._net_connections_cache = 42
        ps._net_connections_cache_time = time.time() - 20

        mock_connections = [MagicMock() for _ in range(10)]

        with patch.object(ps.settings, "mcpgateway_performance_net_connections_enabled", True):
            with patch.object(ps.settings, "mcpgateway_performance_net_connections_cache_ttl", 15):
                with patch.object(psutil, "net_connections", return_value=mock_connections):
                    result = service._get_net_connections_cached()

        assert result == 10
        assert ps._net_connections_cache == 10

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_net_connections_cache_handles_access_denied(self):
        """Test that cache handles AccessDenied error gracefully."""
        import mcpgateway.services.performance_service as ps
        import time
        import psutil

        service = PerformanceService()

        # Set up cache as expired
        ps._net_connections_cache = 42
        ps._net_connections_cache_time = time.time() - 20

        with patch.object(ps.settings, "mcpgateway_performance_net_connections_enabled", True):
            with patch.object(ps.settings, "mcpgateway_performance_net_connections_cache_ttl", 15):
                with patch.object(psutil, "net_connections", side_effect=psutil.AccessDenied()):
                    result = service._get_net_connections_cached()

        # Should return stale cache value on error
        assert result == 42

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_net_connections_cache_handles_oserror(self):
        """Test that cache handles OSError gracefully."""
        import mcpgateway.services.performance_service as ps
        import time
        import psutil

        service = PerformanceService()

        # Set up cache as expired with no previous value
        ps._net_connections_cache = 0
        ps._net_connections_cache_time = 0

        with patch.object(ps.settings, "mcpgateway_performance_net_connections_enabled", True):
            with patch.object(ps.settings, "mcpgateway_performance_net_connections_cache_ttl", 15):
                with patch.object(psutil, "net_connections", side_effect=OSError("Permission denied")):
                    result = service._get_net_connections_cached()

        # Should return 0 when no stale cache available
        assert result == 0

    @pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
    def test_net_connections_error_throttled(self):
        """Test that errors are throttled - cache time updated on error."""
        import mcpgateway.services.performance_service as ps
        import time
        import psutil

        service = PerformanceService()

        # Set up cache as expired
        ps._net_connections_cache = 42
        ps._net_connections_cache_time = time.time() - 20

        mock_net_connections = MagicMock(side_effect=psutil.AccessDenied())

        with patch.object(ps.settings, "mcpgateway_performance_net_connections_enabled", True):
            with patch.object(ps.settings, "mcpgateway_performance_net_connections_cache_ttl", 15):
                with patch.object(psutil, "net_connections", mock_net_connections):
                    # First call - error occurs, cache time updated
                    result1 = service._get_net_connections_cached()
                    # Second call - should use cache (not call psutil again)
                    result2 = service._get_net_connections_cached()

        # Both should return stale value
        assert result1 == 42
        assert result2 == 42
        # psutil.net_connections should only be called once (throttled on error)
        assert mock_net_connections.call_count == 1


class TestModuleConstants:
    """Tests for module-level constants."""

    def test_app_start_time(self):
        """Test that APP_START_TIME is set."""
        import time

        assert APP_START_TIME > 0
        assert APP_START_TIME <= time.time()

    def test_hostname(self):
        """Test that HOSTNAME is set."""
        assert HOSTNAME is not None
        assert len(HOSTNAME) > 0


class TestRequestMetricsEdgeCases:
    """Edge case tests for request metrics."""

    def test_get_request_metrics_with_error_responses(self):
        """Test request metrics calculating error rate."""
        service = PerformanceService()

        # Create mock samples for different status codes
        samples = [
            MagicMock(name="http_requests_total", labels={"status": "200"}, value=800),
            MagicMock(name="http_requests_total", labels={"status": "404"}, value=100),
            MagicMock(name="http_requests_total", labels={"status": "500"}, value=100),
        ]
        for s in samples:
            s.name = "http_requests_total"

        mock_metric = MagicMock()
        mock_metric.name = "http_requests_total"
        mock_metric.samples = samples

        mock_registry = MagicMock()
        mock_registry.collect.return_value = [mock_metric]

        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.REGISTRY", mock_registry):
                result = service.get_request_metrics()

        assert result.requests_total == 1000
        assert result.requests_2xx == 800
        assert result.requests_4xx == 100
        assert result.requests_5xx == 100
        assert result.error_rate == 20.0  # 200/1000 * 100

    def test_get_request_metrics_with_duration_histogram(self):
        """Test request metrics with response time histogram."""
        service = PerformanceService()

        # Mock http_request_duration_seconds histogram
        duration_sum = MagicMock(name="http_request_duration_seconds_sum", value=50.0)
        duration_sum.name = "http_request_duration_seconds_sum"
        duration_count = MagicMock(name="http_request_duration_seconds_count", value=100)
        duration_count.name = "http_request_duration_seconds_count"

        mock_metric = MagicMock()
        mock_metric.name = "http_request_duration_seconds"
        mock_metric.samples = [duration_sum, duration_count]

        mock_registry = MagicMock()
        mock_registry.collect.return_value = [mock_metric]

        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.REGISTRY", mock_registry):
                result = service.get_request_metrics()

        assert result.response_time_avg_ms == 500.0  # 50/100 * 1000

    def test_get_request_metrics_exception_handling(self):
        """Test request metrics handles exceptions gracefully."""
        service = PerformanceService()

        mock_registry = MagicMock()
        mock_registry.collect.side_effect = Exception("Registry error")

        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.REGISTRY", mock_registry):
                result = service.get_request_metrics()

        assert isinstance(result, RequestMetricsSchema)
        assert result.requests_total == 0

    def test_get_request_metrics_counts_1xx_and_3xx(self):
        service = PerformanceService()

        samples = [
            MagicMock(name="http_requests_total", labels={"status": "100"}, value=5),
            MagicMock(name="http_requests_total", labels={"status": "302"}, value=7),
        ]
        for s in samples:
            s.name = "http_requests_total"

        mock_metric = MagicMock()
        mock_metric.name = "http_requests_total"
        mock_metric.samples = samples

        mock_registry = MagicMock()
        mock_registry.collect.return_value = [mock_metric]

        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.REGISTRY", mock_registry):
                result = service.get_request_metrics()

        assert result.requests_total == 12
        assert result.requests_1xx == 5
        assert result.requests_3xx == 7

    def test_get_request_metrics_calculates_requests_per_second(self):
        service = PerformanceService()
        service._request_count_cache["total"] = 100
        service._last_request_time = 990.0

        sample = MagicMock(name="http_requests_total", labels={"status": "200"}, value=200)
        sample.name = "http_requests_total"
        mock_metric = MagicMock()
        mock_metric.name = "http_requests_total"
        mock_metric.samples = [sample]

        mock_registry = MagicMock()
        mock_registry.collect.return_value = [mock_metric]

        with patch("mcpgateway.services.performance_service.PROMETHEUS_AVAILABLE", True):
            with patch("mcpgateway.services.performance_service.REGISTRY", mock_registry):
                with patch("mcpgateway.services.performance_service.time.time", return_value=1000.0):
                    result = service.get_request_metrics()

        assert result.requests_per_second == 10.0
