"""Tests for ObservabilityMixin gRPC endpoints.

Tests cover:
- GetRecentLogs: retrieval with filtering and limits
- GetPerformanceMetrics: current and historical metrics
"""

from __future__ import annotations

from datetime import UTC, datetime
from unittest.mock import MagicMock, patch

import pytest

from noteflow.grpc._mixins._types import GrpcContext
from noteflow.grpc._mixins.observability import ObservabilityMixin
from noteflow.grpc.proto import noteflow_pb2
from noteflow.infrastructure.logging.log_buffer import LogBuffer, LogEntry
from noteflow.infrastructure.metrics.collector import MetricsCollector, PerformanceMetrics

# ============================================================================
# Mock Infrastructure
# ============================================================================


class MockServicerHost(ObservabilityMixin):
    """Mock servicer host implementing required protocol with explicit type stubs."""

    # Type stubs to fix inference when mixins use self: Protocol annotations
    async def GetRecentLogs(
        self,
        request: noteflow_pb2.GetRecentLogsRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.GetRecentLogsResponse:
        return await ObservabilityMixin.GetRecentLogs(self, request, context)

    async def GetPerformanceMetrics(
        self,
        request: noteflow_pb2.GetPerformanceMetricsRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.GetPerformanceMetricsResponse:
        return await ObservabilityMixin.GetPerformanceMetrics(self, request, context)


# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture
def observability_servicer() -> MockServicerHost:
    """Create servicer instance for observability mixin tests."""
    return MockServicerHost()


@pytest.fixture
def mock_log_buffer() -> LogBuffer:
    """Create real LogBuffer for testing."""
    return LogBuffer(capacity=100)


@pytest.fixture
def mock_metrics_collector() -> MagicMock:
    """Create mock MetricsCollector."""
    return MagicMock(spec=MetricsCollector)


@pytest.fixture
def sample_log_entries() -> list[LogEntry]:
    """Create sample log entries for testing."""
    base_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)
    return [
        LogEntry(
            timestamp=base_time,
            level="info",
            source="app",
            message="Application started",
            details={},
        ),
        LogEntry(
            timestamp=base_time,
            level="error",
            source="api",
            message="Request failed",
            details={"code": "500"},
        ),
        LogEntry(
            timestamp=base_time,
            level="warning",
            source="sync",
            message="Sync delayed",
            details={"delay_ms": "5000"},
        ),
        LogEntry(
            timestamp=base_time,
            level="info",
            source="api",
            message="Request completed",
            details={},
        ),
    ]


@pytest.fixture
def sample_metrics() -> PerformanceMetrics:
    """Create sample performance metrics."""
    return PerformanceMetrics(
        timestamp=1705320000.0,
        cpu_percent=45.5,
        memory_percent=62.3,
        memory_mb=8192.0,
        disk_percent=78.1,
        network_bytes_sent=1000000,
        network_bytes_recv=2000000,
        process_memory_mb=512.5,
        active_connections=25,
    )


# ============================================================================
# TestGetRecentLogs
# ============================================================================


class TestGetRecentLogs:
    """Tests for GetRecentLogs RPC."""

    async def test_returns_log_entries(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
        mock_log_buffer: LogBuffer,
        sample_log_entries: list[LogEntry],
    ) -> None:
        """GetRecentLogs returns log entries from buffer."""
        # Add entries explicitly (no loop)
        mock_log_buffer.append(sample_log_entries[0])
        mock_log_buffer.append(sample_log_entries[1])
        mock_log_buffer.append(sample_log_entries[2])
        mock_log_buffer.append(sample_log_entries[3])

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_log_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest()
            response = await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        assert len(response.logs) == 4, "Should return all 4 logs"

    async def test_default_limit_is_100(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs uses default limit of 100."""
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest()
            await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        mock_buffer.get_recent.assert_called_once_with(
            limit=100, level=None, source=None
        )

    async def test_logs_respects_custom_limit(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs respects custom limit parameter."""
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest(limit=50)
            await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        mock_buffer.get_recent.assert_called_once_with(
            limit=50, level=None, source=None
        )

    async def test_caps_limit_at_1000(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs caps limit at 1000."""
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest(limit=2000)
            await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        mock_buffer.get_recent.assert_called_once_with(
            limit=1000, level=None, source=None
        )

    async def test_filters_by_level(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs filters by level when specified."""
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest(level="error")
            await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        mock_buffer.get_recent.assert_called_once_with(
            limit=100, level="error", source=None
        )

    async def test_filters_by_source(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs filters by source when specified."""
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest(source="api")
            await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        mock_buffer.get_recent.assert_called_once_with(
            limit=100, level=None, source="api"
        )

    async def test_combines_level_and_source_filters(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs applies both level and source filters."""
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest(level="warning", source="sync")
            await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        mock_buffer.get_recent.assert_called_once_with(
            limit=100, level="warning", source="sync"
        )

    async def test_returns_empty_when_no_logs(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs returns empty response when buffer is empty."""
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest()
            response = await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        assert len(response.logs) == 0, "Should return empty list"

    async def test_log_entry_proto_format(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """GetRecentLogs returns correctly formatted proto entries."""
        entry = LogEntry(
            timestamp=datetime(2024, 1, 15, 10, 30, 0, tzinfo=UTC),
            level="error",
            source="api",
            message="Connection failed",
            details={"host": "example.com", "port": "443"},
        )
        mock_buffer = MagicMock(spec=LogBuffer)
        mock_buffer.get_recent.return_value = [entry]

        with patch(
            "noteflow.grpc._mixins.observability.get_log_buffer",
            return_value=mock_buffer,
        ):
            request = noteflow_pb2.GetRecentLogsRequest()
            response = await observability_servicer.GetRecentLogs(request, mock_grpc_context)

        proto = response.logs[0]
        assert proto.timestamp == "2024-01-15T10:30:00+00:00", "Timestamp should be ISO format"
        assert proto.level == "error", "Level should match"
        assert proto.source == "api", "Source should match"
        assert proto.message == "Connection failed", "Message should match"
        assert proto.details["host"] == "example.com", "Details should match"
        assert proto.details["port"] == "443", "Details should match"


# ============================================================================
# TestGetPerformanceMetrics
# ============================================================================


class TestGetPerformanceMetrics:
    """Tests for GetPerformanceMetrics RPC."""

    async def test_returns_current_metrics(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
        sample_metrics: PerformanceMetrics,
    ) -> None:
        """GetPerformanceMetrics returns current metrics snapshot."""
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = sample_metrics
        mock_collector.get_history.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest()
            response = await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        expected_cpu = sample_metrics.cpu_percent
        expected_memory = sample_metrics.memory_percent
        expected_memory_mb = sample_metrics.memory_mb
        expected_disk = sample_metrics.disk_percent
        assert response.current.cpu_percent == expected_cpu, "CPU percent should match"
        assert response.current.memory_percent == expected_memory, "Memory percent should match"
        assert response.current.memory_mb == expected_memory_mb, "Memory MB should match"
        assert response.current.disk_percent == expected_disk, "Disk percent should match"

    async def test_returns_historical_metrics(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
        sample_metrics: PerformanceMetrics,
    ) -> None:
        """GetPerformanceMetrics returns historical data."""
        history = [
            PerformanceMetrics(
                timestamp=1705320000.0 - 60.0,
                cpu_percent=40.0,
                memory_percent=60.0,
                memory_mb=8000.0,
                disk_percent=77.0,
                network_bytes_sent=900000,
                network_bytes_recv=1800000,
                process_memory_mb=500.0,
                active_connections=20,
            ),
            PerformanceMetrics(
                timestamp=1705320000.0 - 30.0,
                cpu_percent=42.0,
                memory_percent=61.0,
                memory_mb=8100.0,
                disk_percent=77.5,
                network_bytes_sent=950000,
                network_bytes_recv=1900000,
                process_memory_mb=505.0,
                active_connections=22,
            ),
        ]
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = sample_metrics
        mock_collector.get_history.return_value = history

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest()
            response = await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        expected_history_count = len(history)
        expected_first_cpu = history[0].cpu_percent
        expected_second_cpu = history[1].cpu_percent
        assert len(response.history) == expected_history_count, "Should return 2 history points"
        assert response.history[0].cpu_percent == expected_first_cpu, "First history CPU should match"
        assert response.history[1].cpu_percent == expected_second_cpu, "Second history CPU should match"

    async def test_default_history_limit_is_60(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
        sample_metrics: PerformanceMetrics,
    ) -> None:
        """GetPerformanceMetrics uses default history limit of 60."""
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = sample_metrics
        mock_collector.get_history.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest()
            await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        mock_collector.get_history.assert_called_once_with(limit=60)

    async def test_respects_custom_history_limit(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
        sample_metrics: PerformanceMetrics,
    ) -> None:
        """GetPerformanceMetrics respects custom history limit."""
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = sample_metrics
        mock_collector.get_history.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest(history_limit=30)
            await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        mock_collector.get_history.assert_called_once_with(limit=30)

    async def test_returns_empty_history(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
        sample_metrics: PerformanceMetrics,
    ) -> None:
        """GetPerformanceMetrics handles empty history gracefully."""
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = sample_metrics
        mock_collector.get_history.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest()
            response = await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        assert len(response.history) == 0, "History should be empty"
        assert response.current.timestamp > 0, "Current should still be present"


# ============================================================================
# TestMetricsProtoConversion
# ============================================================================


class TestMetricsProtoConversion:
    """Tests for metrics to proto conversion."""

    async def test_metrics_proto_includes_all_fields(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """Metrics proto includes all expected fields."""
        metrics = PerformanceMetrics(
            timestamp=1705320000.0,
            cpu_percent=55.5,
            memory_percent=70.0,
            memory_mb=10240.0,
            disk_percent=85.0,
            network_bytes_sent=5000000,
            network_bytes_recv=10000000,
            process_memory_mb=750.0,
            active_connections=50,
        )
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = metrics
        mock_collector.get_history.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest()
            response = await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        proto = response.current
        assert proto.timestamp == metrics.timestamp, "Timestamp should match"
        assert proto.cpu_percent == metrics.cpu_percent, "CPU percent should match"
        assert proto.memory_percent == metrics.memory_percent, "Memory percent should match"
        assert proto.memory_mb == metrics.memory_mb, "Memory MB should match"
        assert proto.disk_percent == metrics.disk_percent, "Disk percent should match"
        assert proto.network_bytes_sent == metrics.network_bytes_sent, "Network sent should match"
        assert proto.network_bytes_recv == metrics.network_bytes_recv, "Network recv should match"
        assert proto.process_memory_mb == metrics.process_memory_mb, "Process memory should match"
        assert proto.active_connections == metrics.active_connections, "Active connections should match"

    async def test_handles_zero_values(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """Metrics proto handles zero values correctly."""
        metrics = PerformanceMetrics(
            timestamp=0.0,
            cpu_percent=0.0,
            memory_percent=0.0,
            memory_mb=0.0,
            disk_percent=0.0,
            network_bytes_sent=0,
            network_bytes_recv=0,
            process_memory_mb=0.0,
            active_connections=0,
        )
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = metrics
        mock_collector.get_history.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest()
            response = await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        proto = response.current
        assert proto.cpu_percent == metrics.cpu_percent, "Zero CPU should be preserved"
        assert proto.memory_percent == metrics.memory_percent, "Zero memory should be preserved"
        assert proto.active_connections == metrics.active_connections, "Zero connections should be preserved"

    async def test_handles_max_values(
        self,
        observability_servicer: MockServicerHost,
        mock_grpc_context: MagicMock,
    ) -> None:
        """Metrics proto handles maximum values correctly."""
        metrics = PerformanceMetrics(
            timestamp=1705320000.0,
            cpu_percent=100.0,
            memory_percent=100.0,
            memory_mb=999999.0,
            disk_percent=100.0,
            network_bytes_sent=9999999999,
            network_bytes_recv=9999999999,
            process_memory_mb=99999.0,
            active_connections=10000,
        )
        mock_collector = MagicMock(spec=MetricsCollector)
        mock_collector.collect_now.return_value = metrics
        mock_collector.get_history.return_value = []

        with patch(
            "noteflow.grpc._mixins.observability.get_metrics_collector",
            return_value=mock_collector,
        ):
            request = noteflow_pb2.GetPerformanceMetricsRequest()
            response = await observability_servicer.GetPerformanceMetrics(request, mock_grpc_context)

        proto = response.current
        assert proto.cpu_percent == metrics.cpu_percent, "100% CPU should be preserved"
        assert proto.memory_percent == metrics.memory_percent, "100% memory should be preserved"
        assert proto.network_bytes_sent == metrics.network_bytes_sent, "Large network bytes preserved"
