"""
tests/test_debug_log.py — Tests for the debug-log ring buffer and
                           request-tracing middleware (#453).

Coverage:
  - Request ID generation (12-char hex) and presence in response header
  - Ring buffer stores entries correctly
  - Ring buffer rotates at max capacity (old entries evicted)
  - Debug-log endpoint returns entries in JSON
  - owner_id filter
  - min_latency_ms filter
  - Combined filters
  - Limit parameter capping
  - Buffer clear utility
  - CORTEX_DEBUG_LOG_SIZE configures buffer size
  - Non-HTTP scopes are passed through unchanged
"""
from __future__ import annotations

import importlib
import os
import sys
import time
from typing import Any
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

# ---------------------------------------------------------------------------
# Helpers — build a minimal test app with our middleware + endpoint
# ---------------------------------------------------------------------------

def _make_tracer_app() -> FastAPI:
    """
    Create a tiny FastAPI app that exercises the full tracing pipeline:
        RequestTracerMiddleware → /ping → populates ring buffer → /debug-log reads it
    """
    from cortex.api.debug_log import _make_routes, clear_debug_buffer, router as _router
    from cortex.api.middleware.request_tracer import RequestTracerMiddleware
    from cortex.api.deps import PREFIX
    from fastapi import APIRouter

    app = FastAPI()

    # Mount tracer (last so it's outermost)
    app.add_middleware(RequestTracerMiddleware)

    # Minimal test routes
    @app.get("/ping")
    async def ping():
        return {"pong": True}

    @app.get("/slow")
    async def slow():
        return {"slow": True}

    @app.get("/authed")
    async def authed_route(request: Any = None):
        return {"authed": True}

    # Attach debug-log endpoint (no auth in test — open access)
    async def _no_auth():
        return None

    import cortex.api.debug_log as _dblog
    _dblog.router = APIRouter()
    _dblog._make_routes(_no_auth)
    app.include_router(_dblog.router)

    return app


@pytest.fixture(autouse=True)
def _reset_buffer():
    """Clear the ring buffer before every test."""
    import cortex.api.debug_log as _dblog
    _dblog.clear_debug_buffer()
    yield
    _dblog.clear_debug_buffer()


@pytest.fixture
def tracer_client():
    app = _make_tracer_app()
    with TestClient(app, raise_server_exceptions=True) as tc:
        yield tc


# ---------------------------------------------------------------------------
# 1. Request ID — generation and header presence
# ---------------------------------------------------------------------------

class TestRequestIdGeneration:
    def test_response_has_x_request_id_header(self, tracer_client):
        resp = tracer_client.get("/ping")
        assert resp.status_code == 200
        assert "x-request-id" in resp.headers, "X-Request-ID must be in response headers"

    def test_request_id_is_12_char_hex(self, tracer_client):
        resp = tracer_client.get("/ping")
        rid = resp.headers["x-request-id"]
        assert len(rid) == 12, f"Expected 12 chars, got {len(rid)}: {rid!r}"
        assert all(c in "0123456789abcdef" for c in rid), f"Not hex: {rid!r}"

    def test_each_request_gets_unique_id(self, tracer_client):
        ids = {tracer_client.get("/ping").headers["x-request-id"] for _ in range(5)}
        assert len(ids) == 5, "Request IDs should be unique per request"


# ---------------------------------------------------------------------------
# 2. Ring buffer — storage and rotation
# ---------------------------------------------------------------------------

class TestRingBuffer:
    def test_entry_added_after_request(self, tracer_client):
        import cortex.api.debug_log as _dblog
        tracer_client.get("/ping")
        assert len(_dblog.get_debug_buffer()) == 1

    def test_entry_fields_populated(self, tracer_client):
        import cortex.api.debug_log as _dblog
        tracer_client.get("/ping")
        buf = list(_dblog.get_debug_buffer())
        entry = buf[0]
        assert entry.method == "GET"
        assert entry.path == "/ping"
        assert entry.status == 200
        assert entry.latency_ms >= 0
        assert len(entry.request_id) == 12

    def test_multiple_requests_stored(self, tracer_client):
        import cortex.api.debug_log as _dblog
        for _ in range(3):
            tracer_client.get("/ping")
        assert len(_dblog.get_debug_buffer()) == 3

    def test_ring_buffer_rotates_at_max_capacity(self):
        """Oldest entries should be evicted when buffer is full."""
        import cortex.api.debug_log as _dblog
        from cortex.api.debug_log import DebugLogEntry, add_entry, get_debug_buffer

        buf = get_debug_buffer()
        maxlen = buf.maxlen
        assert maxlen is not None and maxlen > 0

        # Fill buffer to capacity + 5
        for i in range(maxlen + 5):
            add_entry(DebugLogEntry(
                request_id=f"aabbcc{i:06d}"[:12],
                method="GET",
                path=f"/path/{i}",
                status=200,
                latency_ms=1.0,
                owner_id="",
                timestamp=time.time(),
            ))

        assert len(buf) == maxlen, "Buffer must not exceed maxlen"
        # Oldest entries (0..4) should be gone; most recent are present
        paths = {e.path for e in buf}
        # The last entry should always be present
        assert f"/path/{maxlen + 4}" in paths

    def test_clear_debug_buffer(self):
        import cortex.api.debug_log as _dblog
        from cortex.api.debug_log import DebugLogEntry, add_entry
        add_entry(DebugLogEntry("aabbccddeeff", "GET", "/x", 200, 1.0, "", time.time()))
        assert len(_dblog.get_debug_buffer()) == 1
        _dblog.clear_debug_buffer()
        assert len(_dblog.get_debug_buffer()) == 0


# ---------------------------------------------------------------------------
# 3. Debug-log endpoint
# ---------------------------------------------------------------------------

class TestDebugLogEndpoint:
    def test_endpoint_returns_200(self, tracer_client):
        from cortex.api.deps import PREFIX
        tracer_client.get("/ping")
        resp = tracer_client.get(f"{PREFIX}/admin/debug-log")
        assert resp.status_code == 200

    def test_endpoint_returns_entries(self, tracer_client):
        from cortex.api.deps import PREFIX
        tracer_client.get("/ping")
        resp = tracer_client.get(f"{PREFIX}/admin/debug-log")
        data = resp.json()
        assert "entries" in data
        assert data["total"] >= 1  # at least /ping + this request
        assert data["buffer_size"] > 0

    def test_entry_schema(self, tracer_client):
        from cortex.api.deps import PREFIX
        tracer_client.get("/ping")
        resp = tracer_client.get(f"{PREFIX}/admin/debug-log")
        data = resp.json()
        entry = next((e for e in data["entries"] if e["path"] == "/ping"), None)
        assert entry is not None, "/ping entry not found"
        for field in ("request_id", "method", "path", "status", "latency_ms", "owner_id", "timestamp"):
            assert field in entry, f"Missing field: {field}"

    def test_limit_parameter(self, tracer_client):
        from cortex.api.deps import PREFIX
        for _ in range(10):
            tracer_client.get("/ping")
        resp = tracer_client.get(f"{PREFIX}/admin/debug-log?limit=3")
        data = resp.json()
        assert data["total"] <= 3

    def test_limit_minimum_is_1(self, tracer_client):
        from cortex.api.deps import PREFIX
        for _ in range(5):
            tracer_client.get("/ping")
        resp = tracer_client.get(f"{PREFIX}/admin/debug-log?limit=0")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] >= 1  # limit=0 → clamped to 1


# ---------------------------------------------------------------------------
# 4. Filters — owner_id and min_latency_ms
# ---------------------------------------------------------------------------

class TestDebugLogFilters:
    def test_owner_id_filter(self):
        """Entries with non-matching owner_id are excluded."""
        import cortex.api.debug_log as _dblog
        from cortex.api.debug_log import DebugLogEntry, add_entry, get_entries

        add_entry(DebugLogEntry("aabbcc000001", "GET", "/x", 200, 5.0, "alice", time.time()))
        add_entry(DebugLogEntry("aabbcc000002", "GET", "/y", 200, 5.0, "bob", time.time()))
        add_entry(DebugLogEntry("aabbcc000003", "GET", "/z", 200, 5.0, "alice", time.time()))

        alice = get_entries(owner_id="alice")
        assert all(e.owner_id == "alice" for e in alice), "Filter must match only alice"
        assert len(alice) == 2

        bob = get_entries(owner_id="bob")
        assert len(bob) == 1

    def test_min_latency_ms_filter(self):
        """Entries below the latency threshold are excluded."""
        import cortex.api.debug_log as _dblog
        from cortex.api.debug_log import DebugLogEntry, add_entry, get_entries

        add_entry(DebugLogEntry("aabbcc000001", "GET", "/fast", 200, 5.0, "", time.time()))
        add_entry(DebugLogEntry("aabbcc000002", "GET", "/slow", 200, 250.0, "", time.time()))
        add_entry(DebugLogEntry("aabbcc000003", "GET", "/med", 200, 100.0, "", time.time()))

        slow = get_entries(min_latency_ms=100.0)
        assert all(e.latency_ms >= 100.0 for e in slow), "Should only include slow requests"
        assert len(slow) == 2  # /slow and /med

    def test_owner_id_filter_via_endpoint(self, tracer_client):
        """owner_id filter works end-to-end via the HTTP endpoint."""
        import cortex.api.debug_log as _dblog
        from cortex.api.debug_log import DebugLogEntry, add_entry
        from cortex.api.deps import PREFIX

        add_entry(DebugLogEntry("aabbcc000010", "POST", "/api/v1/remember", 200, 10.0, "tenant-x", time.time()))
        add_entry(DebugLogEntry("aabbcc000011", "POST", "/api/v1/remember", 200, 10.0, "tenant-y", time.time()))

        resp = tracer_client.get(f"{PREFIX}/admin/debug-log?owner_id=tenant-x")
        data = resp.json()
        assert all(e["owner_id"] == "tenant-x" for e in data["entries"])

    def test_min_latency_ms_filter_via_endpoint(self, tracer_client):
        """min_latency_ms filter works end-to-end via the HTTP endpoint."""
        import cortex.api.debug_log as _dblog
        from cortex.api.debug_log import DebugLogEntry, add_entry
        from cortex.api.deps import PREFIX

        add_entry(DebugLogEntry("aabbcc000020", "GET", "/fast", 200, 2.0, "", time.time()))
        add_entry(DebugLogEntry("aabbcc000021", "GET", "/slow", 200, 500.0, "", time.time()))

        resp = tracer_client.get(f"{PREFIX}/admin/debug-log?min_latency_ms=200")
        data = resp.json()
        assert all(e["latency_ms"] >= 200 for e in data["entries"])
        assert any(e["path"] == "/slow" for e in data["entries"])
        assert not any(e["path"] == "/fast" for e in data["entries"])
