"""Tests for rbsp.runtime.server -- HTTP REST API server."""

import json
import os
import sys
import tempfile
import shutil
import threading
import time
import unittest
import urllib.parse
import urllib.request
import urllib.error

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.runtime.server import (
    RBSPServer,
    RBSPHandler,
    _match_route,
    _serialize_result,
    _serialize_index_stats,
    _ROUTES,
    _DEFAULT_HOST,
    _DEFAULT_PORT,
    _DEFAULT_WORKERS,
    _MAX_BODY_SIZE,
)
from rbsp.runtime.engine import RBSPEngine, Result, IndexStats
from rbsp.runtime.monitoring import Metrics


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

def _find_free_port() -> int:
    """Find an available TCP port on localhost."""
    import socket
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


def _request(
    url: str,
    method: str = "GET",
    data: object = None,
    headers: dict = None,
    timeout: float = 5.0,
) -> tuple:
    """Make an HTTP request and return (status_code, parsed_json_body, response_headers).

    Returns (status_code, body_dict, headers) on success or error.
    """
    body_bytes = None
    req_headers = {}
    if data is not None:
        body_bytes = json.dumps(data).encode("utf-8")
        req_headers["Content-Type"] = "application/json"
        req_headers["Content-Length"] = str(len(body_bytes))
    if headers:
        req_headers.update(headers)

    req = urllib.request.Request(
        url, data=body_bytes, headers=req_headers, method=method
    )
    try:
        resp = urllib.request.urlopen(req, timeout=timeout)
        raw = resp.read()
        return resp.status, json.loads(raw) if raw else {}, resp.headers
    except urllib.error.HTTPError as exc:
        raw = exc.read()
        return exc.code, json.loads(raw) if raw else {}, exc.headers


def _start_server_bg(server: RBSPServer) -> threading.Thread:
    """Start an RBSPServer.serve_forever() in a background daemon thread."""
    t = threading.Thread(target=server.serve_forever, daemon=True)
    t.start()
    time.sleep(0.3)  # give server time to bind
    return t


# ---------------------------------------------------------------------------
# Base test with initialized engine and running server
# ---------------------------------------------------------------------------

class ServerTestBase(unittest.TestCase):
    """Base class that sets up a temporary project, engine, and running server."""

    @classmethod
    def setUpClass(cls):
        cls.tmp_dir = tempfile.mkdtemp(prefix="rbsp_server_test_")
        # Create sample source files to index
        src_dir = os.path.join(cls.tmp_dir, "src")
        os.makedirs(src_dir)
        with open(os.path.join(src_dir, "hello.py"), "w", encoding="utf-8") as f:
            f.write("def hello():\n    print('hello world')\n")
        with open(os.path.join(src_dir, "auth.py"), "w", encoding="utf-8") as f:
            f.write(
                "class AuthManager:\n"
                "    def authenticate(self, user, password):\n"
                "        return user == 'admin'\n"
            )

        # Initialize and index with a fresh engine
        RBSPEngine.reset()
        cls.engine = RBSPEngine.get_instance()
        cls.engine.init(cls.tmp_dir)
        cls.engine.index(cls.tmp_dir)

        cls.port = _find_free_port()
        cls.server = RBSPServer(
            host="127.0.0.1", port=cls.port, workers=2
        )
        cls._server_thread = _start_server_bg(cls.server)

    @classmethod
    def tearDownClass(cls):
        cls.server.shutdown()
        cls._server_thread.join(timeout=5.0)
        RBSPEngine.reset()
        shutil.rmtree(cls.tmp_dir, ignore_errors=True)

    @property
    def base_url(self) -> str:
        return f"http://127.0.0.1:{self.port}"


# ---------------------------------------------------------------------------
# Health endpoint
# ---------------------------------------------------------------------------

class TestHealthEndpoint(ServerTestBase):
    """Tests for GET /health."""

    def test_health_returns_200(self):
        status, body, _ = _request(f"{self.base_url}/health")
        self.assertEqual(status, 200)
        self.assertEqual(body["status"], "ok")

    def test_health_contains_timestamp(self):
        status, body, _ = _request(f"{self.base_url}/health")
        self.assertIn("timestamp", body)
        self.assertIsInstance(body["timestamp"], float)

    def test_health_trailing_slash(self):
        """Trailing slash should still match /health."""
        status, body, _ = _request(f"{self.base_url}/health/")
        self.assertEqual(status, 200)
        self.assertEqual(body["status"], "ok")


# ---------------------------------------------------------------------------
# Status endpoint
# ---------------------------------------------------------------------------

class TestStatusEndpoint(ServerTestBase):
    """Tests for GET /api/v1/status."""

    def test_status_returns_200(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/status")
        self.assertEqual(status, 200)

    def test_status_contains_index_entries(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/status")
        self.assertIn("index_entries", body)
        self.assertGreaterEqual(body["index_entries"], 0)

    def test_status_contains_initialized(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/status")
        self.assertTrue(body["initialized"])


# ---------------------------------------------------------------------------
# Resolve endpoint (POST /api/v1/resolve)
# ---------------------------------------------------------------------------

class TestResolveEndpoint(ServerTestBase):
    """Tests for POST /api/v1/resolve."""

    def test_resolve_returns_results(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "hello"},
        )
        self.assertEqual(status, 200)
        self.assertIn("results", body)
        self.assertIn("count", body)
        self.assertIn("query", body)
        self.assertEqual(body["query"], "hello")

    def test_resolve_missing_query(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={},
        )
        self.assertEqual(status, 400)
        self.assertIn("error", body)

    def test_resolve_empty_string_query(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": ""},
        )
        self.assertEqual(status, 400)

    def test_resolve_non_string_query(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": 12345},
        )
        self.assertEqual(status, 400)

    def test_resolve_with_max_results(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "hello", "max_results": 1},
        )
        self.assertEqual(status, 200)
        self.assertLessEqual(body["count"], 1)

    def test_resolve_invalid_max_results(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "hello", "max_results": "not_a_number"},
        )
        self.assertEqual(status, 400)

    def test_resolve_invalid_threshold(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "hello", "threshold": "bad"},
        )
        self.assertEqual(status, 400)

    def test_resolve_result_fields(self):
        """Each result should contain path, score, line, snippet, metadata."""
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "authenticate"},
        )
        self.assertEqual(status, 200)
        if body["count"] > 0:
            result = body["results"][0]
            for key in ("path", "score", "line", "snippet", "metadata"):
                self.assertIn(key, result)

    def test_resolve_with_scope(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "hello", "scope": self.tmp_dir},
        )
        self.assertEqual(status, 200)


# ---------------------------------------------------------------------------
# Search endpoint (GET /api/v1/search?q=...)
# ---------------------------------------------------------------------------

class TestSearchEndpoint(ServerTestBase):
    """Tests for GET /api/v1/search with query parameters."""

    def test_search_returns_results(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/search?q=hello"
        )
        self.assertEqual(status, 200)
        self.assertIn("results", body)
        self.assertIn("count", body)
        self.assertEqual(body["query"], "hello")

    def test_search_missing_q_param(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/search")
        self.assertEqual(status, 400)
        self.assertIn("error", body)

    def test_search_with_max_results(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/search?q=hello&max_results=1"
        )
        self.assertEqual(status, 200)
        self.assertLessEqual(body["count"], 1)

    def test_search_with_scope(self):
        encoded_scope = urllib.parse.quote(self.tmp_dir, safe="")
        status, body, _ = _request(
            f"{self.base_url}/api/v1/search?q=hello&scope={encoded_scope}"
        )
        self.assertEqual(status, 200)


# ---------------------------------------------------------------------------
# Index endpoint (POST /api/v1/index)
# ---------------------------------------------------------------------------

class TestIndexEndpoint(ServerTestBase):
    """Tests for POST /api/v1/index."""

    def test_index_returns_stats(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/index",
            method="POST",
            data={"path": self.tmp_dir},
        )
        self.assertEqual(status, 200)
        self.assertIn("files_indexed", body)
        self.assertIn("files_skipped", body)
        self.assertIn("total_size", body)
        self.assertIn("duration", body)
        self.assertIn("index_path", body)

    def test_index_with_force(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/index",
            method="POST",
            data={"path": self.tmp_dir, "force": True},
        )
        self.assertEqual(status, 200)
        self.assertGreater(body["files_indexed"], 0)

    def test_index_default_path(self):
        """Empty body should default to '.'."""
        status, body, _ = _request(
            f"{self.base_url}/api/v1/index",
            method="POST",
            data={"path": self.tmp_dir},
        )
        self.assertEqual(status, 200)


# ---------------------------------------------------------------------------
# Init endpoint (POST /api/v1/init)
# ---------------------------------------------------------------------------

class TestInitEndpoint(ServerTestBase):
    """Tests for POST /api/v1/init."""

    def test_init_returns_success(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/init",
            method="POST",
            data={"path": self.tmp_dir},
        )
        self.assertEqual(status, 200)
        self.assertTrue(body["initialized"])
        self.assertEqual(body["path"], self.tmp_dir)

    def test_init_with_overrides(self):
        status, body, _ = _request(
            f"{self.base_url}/api/v1/init",
            method="POST",
            data={"path": self.tmp_dir, "overrides": {"max_results": 10}},
        )
        self.assertEqual(status, 200)
        self.assertTrue(body["initialized"])


# ---------------------------------------------------------------------------
# Watcher endpoints
# ---------------------------------------------------------------------------

class TestWatcherEndpoints(ServerTestBase):
    """Tests for watcher start/stop/status endpoints."""

    def test_watcher_status(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/watcher")
        self.assertEqual(status, 200)
        self.assertIn("watching", body)

    def test_watcher_start_and_stop(self):
        # Start
        status, body, _ = _request(
            f"{self.base_url}/api/v1/watcher/start",
            method="POST",
            data={"path": self.tmp_dir},
        )
        self.assertEqual(status, 200)
        self.assertIn("started", body)
        self.assertIn("watching", body)

        # Stop
        status, body, _ = _request(
            f"{self.base_url}/api/v1/watcher/stop",
            method="POST",
            data={},
        )
        self.assertEqual(status, 200)
        self.assertTrue(body["stopped"])
        self.assertFalse(body["watching"])


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------

class TestErrorHandling(ServerTestBase):
    """Tests for error responses."""

    def test_unknown_get_endpoint(self):
        status, body, _ = _request(f"{self.base_url}/nonexistent")
        self.assertEqual(status, 404)
        self.assertIn("error", body)

    def test_unknown_post_endpoint(self):
        status, body, _ = _request(
            f"{self.base_url}/nonexistent",
            method="POST",
            data={},
        )
        self.assertEqual(status, 404)

    def test_resolve_invalid_json(self):
        """Sending malformed JSON should return 400."""
        req = urllib.request.Request(
            f"{self.base_url}/api/v1/resolve",
            data=b"not json",
            headers={
                "Content-Type": "application/json",
                "Content-Length": "8",
            },
            method="POST",
        )
        try:
            resp = urllib.request.urlopen(req, timeout=5.0)
            status = resp.status
            resp.read()
        except urllib.error.HTTPError as exc:
            status = exc.code
            exc.read()
        self.assertEqual(status, 400)

    def test_resolve_non_object_json(self):
        """Sending a JSON array instead of object should return 400."""
        raw_body = json.dumps(["not", "an", "object"]).encode("utf-8")
        req = urllib.request.Request(
            f"{self.base_url}/api/v1/resolve",
            data=raw_body,
            headers={
                "Content-Type": "application/json",
                "Content-Length": str(len(raw_body)),
            },
            method="POST",
        )
        try:
            resp = urllib.request.urlopen(req, timeout=5.0)
            status = resp.status
            resp.read()
        except urllib.error.HTTPError as exc:
            status = exc.code
            exc.read()
        self.assertEqual(status, 400)

    def test_error_response_has_status_field(self):
        status, body, _ = _request(f"{self.base_url}/nonexistent")
        self.assertEqual(body.get("status"), 404)


# ---------------------------------------------------------------------------
# CORS
# ---------------------------------------------------------------------------

class TestCORS(ServerTestBase):
    """Tests for CORS headers."""

    def test_cors_headers_present(self):
        _, _, hdrs = _request(f"{self.base_url}/health")
        self.assertEqual(hdrs.get("Access-Control-Allow-Origin"), "*")
        self.assertIn("GET", hdrs.get("Access-Control-Allow-Methods", ""))
        self.assertIn("X-API-Key", hdrs.get("Access-Control-Allow-Headers", ""))

    def test_options_preflight(self):
        """OPTIONS request should return 204 with CORS headers."""
        req = urllib.request.Request(
            f"{self.base_url}/api/v1/resolve", method="OPTIONS"
        )
        resp = urllib.request.urlopen(req, timeout=5.0)
        self.assertEqual(resp.status, 204)
        self.assertEqual(resp.headers.get("Access-Control-Allow-Origin"), "*")


# ---------------------------------------------------------------------------
# API key authentication
# ---------------------------------------------------------------------------

class TestAPIKeyAuth(unittest.TestCase):
    """Tests for X-API-Key authentication."""

    @classmethod
    def setUpClass(cls):
        cls.tmp_dir = tempfile.mkdtemp(prefix="rbsp_auth_test_")
        # Create a sample file so resolve has something to search
        with open(os.path.join(cls.tmp_dir, "sample.py"), "w", encoding="utf-8") as f:
            f.write("def test():\n    pass\n")

        RBSPEngine.reset()
        engine = RBSPEngine.get_instance()
        engine.init(cls.tmp_dir)
        engine.index(cls.tmp_dir)

        cls.port = _find_free_port()
        cls.api_key = "test-secret-key-12345"
        cls.server = RBSPServer(
            host="127.0.0.1",
            port=cls.port,
            workers=2,
            api_key=cls.api_key,
        )
        cls._server_thread = _start_server_bg(cls.server)

    @classmethod
    def tearDownClass(cls):
        cls.server.shutdown()
        cls._server_thread.join(timeout=5.0)
        RBSPEngine.reset()
        shutil.rmtree(cls.tmp_dir, ignore_errors=True)

    def test_missing_api_key_returns_401(self):
        status, body, _ = _request(f"http://127.0.0.1:{self.port}/health")
        self.assertEqual(status, 401)
        self.assertIn("error", body)

    def test_wrong_api_key_returns_401(self):
        status, body, _ = _request(
            f"http://127.0.0.1:{self.port}/health",
            headers={"X-API-Key": "wrong-key"},
        )
        self.assertEqual(status, 401)

    def test_correct_api_key_returns_200(self):
        status, body, _ = _request(
            f"http://127.0.0.1:{self.port}/health",
            headers={"X-API-Key": self.api_key},
        )
        self.assertEqual(status, 200)
        self.assertEqual(body["status"], "ok")

    def test_correct_key_on_resolve(self):
        status, body, _ = _request(
            f"http://127.0.0.1:{self.port}/api/v1/resolve",
            method="POST",
            data={"query": "test", "scope": self.tmp_dir},
            headers={"X-API-Key": self.api_key},
        )
        self.assertEqual(status, 200)


# ---------------------------------------------------------------------------
# Server lifecycle
# ---------------------------------------------------------------------------

class TestServerLifecycle(unittest.TestCase):
    """Tests for RBSPServer start/shutdown lifecycle."""

    def test_server_properties(self):
        server = RBSPServer(host="127.0.0.1", port=9999)
        self.assertEqual(server.address, ("127.0.0.1", 9999))
        self.assertFalse(server.is_running)

    def test_repr(self):
        server = RBSPServer(host="127.0.0.1", port=9999, workers=2)
        r = repr(server)
        self.assertIn("RBSPServer", r)
        self.assertIn("9999", r)
        self.assertIn("workers=2", r)
        self.assertIn("running=False", r)

    def test_invalid_workers_raises(self):
        with self.assertRaises(ValueError):
            RBSPServer(workers=0)

    def test_invalid_port_raises(self):
        with self.assertRaises(ValueError):
            RBSPServer(port=0)
        with self.assertRaises(ValueError):
            RBSPServer(port=70000)

    def test_start_and_shutdown(self):
        tmp = tempfile.mkdtemp(prefix="rbsp_lifecycle_")
        try:
            RBSPEngine.reset()
            engine = RBSPEngine.get_instance()
            engine.init(tmp)

            port = _find_free_port()
            server = RBSPServer(host="127.0.0.1", port=port, workers=1)
            t = _start_server_bg(server)
            self.assertTrue(server.is_running)

            # Verify server responds
            status, body, _ = _request(f"http://127.0.0.1:{port}/health")
            self.assertEqual(status, 200)

            server.shutdown()
            t.join(timeout=5.0)
            self.assertFalse(server.is_running)
        finally:
            RBSPEngine.reset()
            shutil.rmtree(tmp, ignore_errors=True)

    def test_shutdown_when_not_started(self):
        """Shutting down a server that was never started should not raise."""
        server = RBSPServer()
        server.shutdown()  # should be a no-op

    def test_auto_init(self):
        """auto_init should initialize the engine on serve_forever."""
        tmp = tempfile.mkdtemp(prefix="rbsp_autoinit_")
        try:
            RBSPEngine.reset()
            port = _find_free_port()
            server = RBSPServer(
                host="127.0.0.1", port=port, workers=1, auto_init=tmp
            )
            t = _start_server_bg(server)

            status, body, _ = _request(
                f"http://127.0.0.1:{port}/api/v1/status"
            )
            self.assertEqual(status, 200)
            self.assertTrue(body["initialized"])

            server.shutdown()
            t.join(timeout=5.0)
        finally:
            RBSPEngine.reset()
            shutil.rmtree(tmp, ignore_errors=True)


# ---------------------------------------------------------------------------
# Route matching
# ---------------------------------------------------------------------------

class TestRouteMatching(unittest.TestCase):
    """Tests for the _match_route function."""

    def test_exact_match(self):
        result = _match_route("GET", "/health")
        self.assertIsNotNone(result)

    def test_trailing_slash_match(self):
        result = _match_route("GET", "/health/")
        self.assertIsNotNone(result)

    def test_query_string_stripped(self):
        result = _match_route("GET", "/api/v1/search?q=hello")
        self.assertIsNotNone(result)

    def test_no_match(self):
        result = _match_route("GET", "/nonexistent")
        self.assertIsNone(result)

    def test_wrong_method(self):
        result = _match_route("DELETE", "/health")
        self.assertIsNone(result)

    def test_all_routes_defined(self):
        """Verify all expected routes exist in the table."""
        expected = [
            ("GET", "/health"),
            ("GET", "/api/v1/status"),
            ("POST", "/api/v1/resolve"),
            ("GET", "/api/v1/search"),
            ("POST", "/api/v1/index"),
            ("POST", "/api/v1/init"),
            ("POST", "/api/v1/watcher/start"),
            ("POST", "/api/v1/watcher/stop"),
            ("GET", "/api/v1/watcher"),
        ]
        for method, path in expected:
            result = _match_route(method, path)
            self.assertIsNotNone(result, f"Route not found: {method} {path}")


# ---------------------------------------------------------------------------
# Serialization helpers
# ---------------------------------------------------------------------------

class TestSerialization(unittest.TestCase):
    """Tests for result and stats serialization."""

    def test_serialize_result(self):
        r = Result(path="foo.py", score=0.123456789, snippet="x", line=5, metadata={"a": 1})
        d = _serialize_result(r)
        self.assertEqual(d["path"], "foo.py")
        self.assertEqual(d["score"], round(0.123456789, 6))
        self.assertEqual(d["snippet"], "x")
        self.assertEqual(d["line"], 5)
        self.assertEqual(d["metadata"], {"a": 1})

    def test_serialize_index_stats(self):
        s = IndexStats(
            files_indexed=10, files_skipped=2, total_size=4096,
            duration=1.23456, index_path="/tmp/idx"
        )
        d = _serialize_index_stats(s)
        self.assertEqual(d["files_indexed"], 10)
        self.assertEqual(d["files_skipped"], 2)
        self.assertEqual(d["total_size"], 4096)
        self.assertEqual(d["duration"], round(1.23456, 4))
        self.assertEqual(d["index_path"], "/tmp/idx")


# ---------------------------------------------------------------------------
# Content-Type and edge cases
# ---------------------------------------------------------------------------

class TestEdgeCases(ServerTestBase):
    """Edge case and boundary condition tests."""

    def test_json_content_type(self):
        """Response should have application/json content type."""
        _, _, hdrs = _request(f"{self.base_url}/health")
        ct = hdrs.get("Content-Type", "")
        self.assertIn("application/json", ct)

    def test_concurrent_requests(self):
        """Server should handle multiple concurrent requests."""
        results = []
        errors = []

        def make_request():
            try:
                s, b, _ = _request(f"{self.base_url}/health")
                results.append(s)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=make_request) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join(timeout=10.0)

        self.assertEqual(len(errors), 0, f"Errors during concurrent requests: {errors}")
        self.assertTrue(all(s == 200 for s in results))

    def test_body_size_limit(self):
        """Body exceeding _MAX_BODY_SIZE should return 413."""
        # We cannot actually send 4 MiB in a test, but we can send a request
        # with a Content-Length header claiming a huge size.
        req = urllib.request.Request(
            f"{self.base_url}/api/v1/resolve",
            data=b'{"query":"x"}',
            headers={
                "Content-Type": "application/json",
                # Claim body is larger than max
                "Content-Length": str(_MAX_BODY_SIZE + 1),
            },
            method="POST",
        )
        try:
            resp = urllib.request.urlopen(req, timeout=5.0)
            status = resp.status
            raw = resp.read()
            body = json.loads(raw)
        except urllib.error.HTTPError as exc:
            status = exc.code
            raw = exc.read()
            body = json.loads(raw) if raw else {}
        self.assertEqual(status, 413)
        self.assertIn("error", body)


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

class TestConstants(unittest.TestCase):
    """Verify default constants."""

    def test_default_host(self):
        self.assertEqual(_DEFAULT_HOST, "127.0.0.1")

    def test_default_port(self):
        self.assertEqual(_DEFAULT_PORT, 7342)

    def test_default_workers(self):
        self.assertEqual(_DEFAULT_WORKERS, 8)

    def test_max_body_size(self):
        self.assertEqual(_MAX_BODY_SIZE, 4 * 1024 * 1024)


# ---------------------------------------------------------------------------
# Metrics integration
# ---------------------------------------------------------------------------

class TestMetricsIntegration(ServerTestBase):
    """Tests for metrics counters and histograms."""

    def test_request_counter_increments(self):
        """Metrics counter http.requests.total should increment on requests."""
        before = self.server.metrics.get_counter("http.requests.total")
        _request(f"{self.base_url}/health")
        after = self.server.metrics.get_counter("http.requests.total")
        self.assertGreater(after, before)

    def test_query_counter_on_resolve(self):
        before = self.server.metrics.get_counter("http.requests.query")
        _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "hello", "scope": self.tmp_dir},
        )
        after = self.server.metrics.get_counter("http.requests.query")
        self.assertGreater(after, before)

    def test_query_latency_histogram(self):
        _request(
            f"{self.base_url}/api/v1/resolve",
            method="POST",
            data={"query": "hello", "scope": self.tmp_dir},
        )
        hist = self.server.metrics.get_histogram("http.query.latency_ms")
        self.assertGreater(hist["count"], 0)

    def test_index_counter_on_index(self):
        before = self.server.metrics.get_counter("http.requests.index")
        _request(
            f"{self.base_url}/api/v1/index",
            method="POST",
            data={"path": self.tmp_dir},
        )
        after = self.server.metrics.get_counter("http.requests.index")
        self.assertGreater(after, before)

    def test_error_counter_on_404(self):
        before = self.server.metrics.get_counter("http.requests.error")
        _request(f"{self.base_url}/nonexistent")
        after = self.server.metrics.get_counter("http.requests.error")
        self.assertGreater(after, before)


# ---------------------------------------------------------------------------
# Status enrichment
# ---------------------------------------------------------------------------

class TestStatusEnrichment(ServerTestBase):
    """Tests for GET /api/v1/status enriched with metrics and throttle."""

    def test_status_contains_metrics(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/status")
        self.assertEqual(status, 200)
        self.assertIn("metrics", body)
        self.assertIn("counters", body["metrics"])

    def test_status_contains_throttle(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/status")
        self.assertEqual(status, 200)
        self.assertIn("throttle", body)
        self.assertIn("query_tokens_available", body["throttle"])

    def test_status_contains_http_connections(self):
        status, body, _ = _request(f"{self.base_url}/api/v1/status")
        self.assertEqual(status, 200)
        self.assertIn("http_connections_active", body)


# ---------------------------------------------------------------------------
# Server properties (throttle, metrics, slow_log)
# ---------------------------------------------------------------------------

class TestServerProperties(unittest.TestCase):
    """Tests for RBSPServer property accessors."""

    def test_metrics_property(self):
        server = RBSPServer()
        self.assertIsInstance(server.metrics, Metrics)

    def test_throttle_property(self):
        from rbsp.runtime.throttle import Throttle
        server = RBSPServer()
        self.assertIsInstance(server.throttle, Throttle)

    def test_slow_log_property(self):
        from rbsp.runtime.monitoring import SlowQueryLog
        server = RBSPServer()
        self.assertIsInstance(server.slow_log, SlowQueryLog)


if __name__ == "__main__":
    unittest.main()
