"""
tests/test_monitoring_767.py — Tests for Issue #767 enhanced monitoring additions.

Coverage:
  1. Retrieval smoke test in ?detail=true health endpoint
     - smoke test present in detail block
     - smoke test "ok" when retrieve succeeds
     - smoke test "error" when retrieve raises
     - smoke test "unavailable" when plugin has no retrieve method
     - smoke test NOT present in non-detail health response (performance path)
  2. GET /api/v1/admin/metrics/growth endpoint
     - returns 200 with correct shape
     - data grouped by day and sorted ascending
     - handles storage unavailable (503)
     - custom ?days= parameter respected
     - empty result when no claims in range
"""
from __future__ import annotations

import asyncio
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

pytest.importorskip("fastapi", reason="fastapi not installed")
pytest.importorskip("httpx", reason="httpx not installed")

from fastapi import FastAPI
from fastapi.testclient import TestClient  # noqa: E402


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

@dataclass
class _FakeHealthReport:
    ok: bool = True
    storage: str = "ok"
    modules: Dict[str, str] = field(default_factory=lambda: {"storage": "ok"})
    errors: List[str] = field(default_factory=list)
    timestamp: str = "2025-01-01T00:00:00+00:00"


def _make_mock_plugin(
    *,
    storage_ok: bool = True,
    retrieve_raises: bool = False,
    no_retrieve: bool = False,
    growth_rows: Optional[List[tuple]] = None,
) -> MagicMock:
    """Construct a mock CortexPlugin for health/admin endpoint tests."""
    plugin = MagicMock()
    plugin._init_errors = []
    plugin.health = AsyncMock(return_value=_FakeHealthReport())

    cfg = MagicMock()
    cfg.owner_id = "default"
    cfg.quality_gates_enabled = False
    cfg.metrics_enabled = True
    plugin.config = cfg

    if storage_ok:
        storage = MagicMock()
        storage.ping = AsyncMock(return_value=None)
        storage.initialize = AsyncMock(return_value=None)
        storage.close = AsyncMock(return_value=None)
        storage.count_claims = AsyncMock(return_value=42)
        storage.count_sessions = AsyncMock(return_value=5)
        storage._conn = MagicMock()  # truthy connection

        # Growth query mock: storage._conn used directly via async context manager
        _rows = growth_rows if growth_rows is not None else [
            ("2026-03-10", 5),
            ("2026-03-11", 8),
            ("2026-03-12", 3),
        ]

        # Mock the async context manager for `conn.execute(...)` / cursor
        async def _mock_fetchall():
            return _rows

        cursor_mock = MagicMock()
        cursor_mock.fetchall = _mock_fetchall
        cursor_mock.__aenter__ = AsyncMock(return_value=cursor_mock)
        cursor_mock.__aexit__ = AsyncMock(return_value=False)

        storage._conn.execute = MagicMock(return_value=cursor_mock)

        plugin.storage = storage
    else:
        plugin.storage = None

    # Retrieve method
    if no_retrieve:
        # Explicitly remove retrieve so hasattr check fails
        del plugin.retrieve
    elif retrieve_raises:
        plugin.retrieve = AsyncMock(side_effect=RuntimeError("retrieval failure"))
    else:
        plugin.retrieve = AsyncMock(return_value=MagicMock(memories=[]))

    # Metrics mock
    metrics = MagicMock()

    async def _get_summary():
        return {
            "counters": {"memories_stored": 10},
            "histograms": {},
            "gauges": {},
            "uptime_seconds": 60.0,
        }

    metrics.get_summary = AsyncMock(side_effect=_get_summary)
    plugin._metrics = metrics

    return plugin


def _build_client(plugin: MagicMock) -> TestClient:
    """Construct a TestClient with the given plugin injected."""
    from cortex.api.routes.admin import router, _make_routes
    from cortex.api import deps as _deps
    from cortex.api.admin import require_admin

    app = FastAPI()

    def _noop_verify(x: Any = None):
        return None

    async def _noop_admin():
        return None

    _make_routes(verify_api_key=_noop_verify)
    app.include_router(router)
    app.dependency_overrides[require_admin] = _noop_admin

    _deps.plugin_holder["plugin"] = plugin
    return TestClient(app)


# ---------------------------------------------------------------------------
# 1. Retrieval smoke test in health?detail=true
# ---------------------------------------------------------------------------

class TestRetrievalSmokeTest:
    """Verify retrieval_smoke appears only in ?detail=true health responses."""

    def test_detail_true_has_retrieval_smoke(self):
        """?detail=true response includes retrieval_smoke block."""
        plugin = _make_mock_plugin(storage_ok=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/health?detail=true")
        assert resp.status_code == 200
        data = resp.json()

        assert "detail" in data, "detail block missing from ?detail=true response"
        detail = data["detail"]
        assert "retrieval_smoke" in detail, "retrieval_smoke missing from detail block"

    def test_retrieval_smoke_ok_when_retrieve_succeeds(self):
        """retrieval_smoke.status == 'ok' when retrieve returns successfully."""
        plugin = _make_mock_plugin(storage_ok=True, retrieve_raises=False)
        client = _build_client(plugin)

        resp = client.get("/api/v1/health?detail=true")
        assert resp.status_code == 200
        smoke = resp.json()["detail"]["retrieval_smoke"]

        assert smoke["status"] == "ok"
        assert isinstance(smoke["latency_ms"], (int, float))
        assert smoke["latency_ms"] >= 0

    def test_retrieval_smoke_error_when_retrieve_fails(self):
        """retrieval_smoke.status == 'error' when retrieve raises an exception."""
        plugin = _make_mock_plugin(storage_ok=True, retrieve_raises=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/health?detail=true")
        assert resp.status_code == 200
        smoke = resp.json()["detail"]["retrieval_smoke"]

        assert smoke["status"] == "error"
        assert isinstance(smoke["latency_ms"], (int, float))
        assert "error" in smoke

    def test_retrieval_smoke_unavailable_when_no_retrieve(self):
        """retrieval_smoke.status == 'unavailable' when plugin lacks retrieve method."""
        plugin = _make_mock_plugin(storage_ok=True, no_retrieve=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/health?detail=true")
        assert resp.status_code == 200
        smoke = resp.json()["detail"]["retrieval_smoke"]

        assert smoke["status"] == "unavailable"
        assert smoke["latency_ms"] is None

    def test_retrieval_smoke_not_in_compact_health(self):
        """Standard /health (no ?detail) must NOT include retrieval_smoke in top-level."""
        plugin = _make_mock_plugin(storage_ok=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/health")
        assert resp.status_code == 200
        data = resp.json()

        # Top-level response should not contain retrieval_smoke
        assert "retrieval_smoke" not in data
        # detail block absent or None
        assert data.get("detail") is None

    @pytest.mark.asyncio
    async def test_retrieval_smoke_unit_ok(self):
        """Unit: _retrieval_smoke_test returns ok dict when retrieve works."""
        from cortex.api.health import HealthChecker

        plugin = MagicMock()
        plugin.retrieve = AsyncMock(return_value=MagicMock())
        plugin.config = MagicMock()
        plugin.config.owner_id = "default"

        result = await HealthChecker._retrieval_smoke_test(plugin)

        assert result["status"] == "ok"
        assert isinstance(result["latency_ms"], float)

    @pytest.mark.asyncio
    async def test_retrieval_smoke_unit_error(self):
        """Unit: _retrieval_smoke_test returns error dict when retrieve raises."""
        from cortex.api.health import HealthChecker

        plugin = MagicMock()
        plugin.retrieve = AsyncMock(side_effect=ValueError("db gone"))
        plugin.config = MagicMock()
        plugin.config.owner_id = "default"

        result = await HealthChecker._retrieval_smoke_test(plugin)

        assert result["status"] == "error"
        assert "db gone" in result["error"]

    @pytest.mark.asyncio
    async def test_retrieval_smoke_unit_unavailable(self):
        """Unit: _retrieval_smoke_test returns unavailable when no retrieve."""
        from cortex.api.health import HealthChecker

        plugin = MagicMock(spec=[])  # no attributes by default
        result = await HealthChecker._retrieval_smoke_test(plugin)

        assert result["status"] == "unavailable"
        assert result["latency_ms"] is None


# ---------------------------------------------------------------------------
# 2. GET /api/v1/admin/metrics/growth
# ---------------------------------------------------------------------------

class TestMetricsGrowth:
    """Tests for the /admin/metrics/growth endpoint."""

    def test_growth_returns_200(self):
        """Growth endpoint returns HTTP 200 with valid JSON."""
        plugin = _make_mock_plugin(storage_ok=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth")
        assert resp.status_code == 200

    def test_growth_response_shape(self):
        """Response has keys: days, data (list), total (int)."""
        plugin = _make_mock_plugin(storage_ok=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth")
        data = resp.json()

        assert "days" in data
        assert "data" in data
        assert "total" in data
        assert isinstance(data["data"], list)
        assert isinstance(data["total"], int)

    def test_growth_data_items_have_date_and_count(self):
        """Each item in data[] has 'date' (str) and 'count' (int) keys."""
        plugin = _make_mock_plugin(storage_ok=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth")
        items = resp.json()["data"]

        assert len(items) > 0, "Expected at least one data point from mock"
        for item in items:
            assert "date" in item
            assert "count" in item
            assert isinstance(item["date"], str)
            assert isinstance(item["count"], int)

    def test_growth_total_matches_sum(self):
        """total field equals sum of all count values in data."""
        rows = [("2026-03-10", 4), ("2026-03-11", 6), ("2026-03-12", 2)]
        plugin = _make_mock_plugin(storage_ok=True, growth_rows=rows)
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth")
        body = resp.json()

        expected_total = sum(r[1] for r in rows)
        assert body["total"] == expected_total

    def test_growth_default_days_is_30(self):
        """Default response uses days=30."""
        plugin = _make_mock_plugin(storage_ok=True)
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth")
        assert resp.json()["days"] == 30

    def test_growth_custom_days_param(self):
        """?days=7 is reflected in the response."""
        plugin = _make_mock_plugin(storage_ok=True, growth_rows=[("2026-03-12", 3)])
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth?days=7")
        assert resp.json()["days"] == 7

    def test_growth_empty_when_no_data(self):
        """Empty data list returned when no claims exist in the period."""
        plugin = _make_mock_plugin(storage_ok=True, growth_rows=[])
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth")
        body = resp.json()

        assert body["data"] == []
        assert body["total"] == 0

    def test_growth_503_when_storage_unavailable(self):
        """Returns 503 when plugin has no storage."""
        plugin = _make_mock_plugin(storage_ok=False)
        client = _build_client(plugin)

        resp = client.get("/api/v1/admin/metrics/growth")
        assert resp.status_code == 503
