"""
tests/test_http_server.py — Tests for cortex/api/http_server.py

Strategy:
  - Mock CortexPlugin via patch("cortex.api.plugin.CortexPlugin") kept active
    through the TestClient lifespan so our mock is injected into the app.
  - Test every endpoint for happy path and error cases
  - Test auth (with and without API key)
  - Test CORS headers
  - Test edge cases: cornerstone-protected delete, unavailable modules

Uses FastAPI TestClient (synchronous wrapper around httpx).
Patches must wrap TestClient.__enter__ to cover the lifespan startup.
"""
from __future__ import annotations

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

import pytest

# ---------------------------------------------------------------------------
# Skip gracefully if FastAPI / httpx not installed
# ---------------------------------------------------------------------------

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

from fastapi.testclient import TestClient  # noqa: E402


# ---------------------------------------------------------------------------
# Stub result types (mirror CortexPlugin dataclasses without real imports)
# ---------------------------------------------------------------------------

@dataclass
class _RememberResult:
    session_id: str = "sess-1"
    claims_extracted: int = 3
    claims_stored: int = 2
    entities_resolved: int = 1
    noise_filtered: int = 0
    errors: List[str] = field(default_factory=list)
    ok: bool = True


@dataclass
class _ForgetResult:
    memory_id: str = "mem-1"
    deleted: bool = True
    reason: str = "ok"
    errors: List[str] = field(default_factory=list)


@dataclass
class _HealthReport:
    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"


@dataclass
class _Cornerstone:
    cs_id: str = "cs-1"
    label: str = "Identity"
    content: str = "I am Eva"
    sealed: bool = True


@dataclass
class _FeedbackRecord:
    feedback_id: str = "fb-1"
    memory_id: str = "mem-1"
    helpful: bool = True


@dataclass
class _RetrievalResult:
    query: str = "test query"
    memories: List[str] = field(default_factory=lambda: ["memory A"])
    total_tokens: int = 42


@dataclass
class _Claim:
    id: str = "mem-123"
    subject: str = "test"
    predicate: str = "is"
    object_value: str = "value"
    owner_id: str = "owner-1"
    confidence: float = 0.8
    status: str = "active"


# ---------------------------------------------------------------------------
# Mock plugin factory
# ---------------------------------------------------------------------------

def _make_mock_plugin(**overrides) -> MagicMock:
    """Return a fully-mocked CortexPlugin instance."""
    plugin = MagicMock()
    plugin.config = MagicMock()
    plugin.config.owner_id = "owner-1"

    # Storage for stats
    plugin.storage = MagicMock()
    plugin.storage.initialize = AsyncMock()
    plugin.storage.count_claims = AsyncMock(return_value=overrides.get("count_claims", 10))
    plugin.storage.count_sessions = AsyncMock(return_value=overrides.get("count_sessions", 5))
    plugin.storage.close = AsyncMock()
    plugin.storage.get_claim = AsyncMock(return_value=overrides.get("get_claim", _Claim()))
    plugin.storage.get_changelog = AsyncMock(return_value=[])
    plugin.storage.log_changelog = AsyncMock(return_value=None)
    plugin.storage.delete_claim = AsyncMock(return_value=None)
    plugin.storage.get_cornerstones = AsyncMock(
        return_value=overrides.get("cornerstones", [_Cornerstone()])
    )

    # Plugin public methods
    plugin.remember = AsyncMock(return_value=overrides.get("remember_result", _RememberResult()))
    plugin.forget = AsyncMock(return_value=overrides.get("forget_result", _ForgetResult()))
    plugin.health = AsyncMock(return_value=overrides.get("health_report", _HealthReport()))
    plugin.get_cornerstones = AsyncMock(
        return_value=overrides.get("cornerstones", [_Cornerstone()])
    )
    plugin.seal_cornerstone = AsyncMock(
        return_value=overrides.get("seal_result", _Cornerstone())
    )
    plugin.unseal_cornerstone = AsyncMock(
        return_value=overrides.get("unseal_result", _Cornerstone(sealed=False))
    )
    plugin.feedback = AsyncMock(
        return_value=overrides.get("feedback_result", _FeedbackRecord())
    )
    plugin.retrieve = AsyncMock(
        return_value=overrides.get("retrieval_result", _RetrievalResult())
    )
    return plugin


# ---------------------------------------------------------------------------
# Context manager that gives a TestClient with a patched CortexPlugin.
# The patch MUST be active during TestClient.__enter__ so the lifespan
# startup uses our mock when it calls CortexPlugin(...).
# ---------------------------------------------------------------------------

@contextmanager
def _make_client(api_key: Optional[str] = None, **plugin_overrides):
    """
    Yield (TestClient, mock_plugin).

    CortexPlugin is patched throughout the client lifetime.
    """
    from cortex.api.http_server import create_app

    mock_plugin = _make_mock_plugin(**plugin_overrides)

    # Build app (this doesn't call CortexPlugin yet — just wires routes)
    app = create_app(api_key=api_key)

    # Build a fake HealthReport matching what HealthChecker.check() returns.
    # Default to healthy; callers can override via plugin_overrides["health_status"].
    from cortex.api.health import HealthReport as _RealHealthReport
    _health_status = plugin_overrides.get("health_status", "healthy")
    _fake_health_report = _RealHealthReport(
        status=_health_status,
        checks={"storage": {"status": "ok"}},
    )

    # Keep patch active while TestClient starts + runs so the lifespan
    # import of CortexPlugin returns our mock.
    with patch("cortex.api.plugin.CortexPlugin") as MockClass, \
         patch("cortex.api.health.HealthChecker") as MockHC:
        MockClass.return_value = mock_plugin
        MockHC.check = AsyncMock(return_value=_fake_health_report)
        with TestClient(app, raise_server_exceptions=True) as tc:
            tc._mock_plugin = mock_plugin
            yield tc, mock_plugin


# ---------------------------------------------------------------------------
# pytest fixtures (thin wrappers)
# ---------------------------------------------------------------------------

@pytest.fixture
def client():
    with _make_client() as (tc, _):
        yield tc


@pytest.fixture
def authed_client():
    with _make_client(api_key="test-key") as (tc, _):
        yield tc


# ---------------------------------------------------------------------------
# Tests — Health
# ---------------------------------------------------------------------------

class TestHealth:
    def test_health_ok(self, client):
        resp = client.get("/api/v1/health")
        assert resp.status_code == 200
        data = resp.json()
        assert data["ok"] is True
        assert data["storage"] == "ok"
        assert "modules" in data
        assert "timestamp" in data

    def test_health_degraded(self):
        """Degraded state: ok should still be True, degraded flag set."""
        bad_health = _HealthReport(
            ok=False, storage="ok", errors=["quality gate warning"]
        )
        with _make_client(health_report=bad_health, health_status="degraded") as (tc, _):
            resp = tc.get("/api/v1/health")
        assert resp.status_code == 200
        data = resp.json()
        assert data["ok"] is True
        assert data["degraded"] is True

    def test_health_unhealthy(self):
        """Unhealthy state: ok=False."""
        bad_health = _HealthReport(
            ok=False, storage="error: db locked", errors=["db locked"]
        )
        with _make_client(health_report=bad_health, health_status="unhealthy") as (tc, _):
            resp = tc.get("/api/v1/health")
        assert resp.status_code == 200
        data = resp.json()
        assert data["ok"] is False
        assert data["degraded"] is False

    def test_health_no_auth_required(self):
        """Health endpoints must be accessible without authentication."""
        with _make_client(api_key="secret-key") as (tc, _):
            resp = tc.get("/api/v1/health")
            assert resp.status_code == 200
            resp = tc.get("/api/v1/health/ready")
            assert resp.status_code == 200


# ---------------------------------------------------------------------------
# Tests — Stats
# ---------------------------------------------------------------------------

class TestStats:
    def test_stats_ok(self, client):
        resp = client.get("/api/v1/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["storage_ok"] is True
        assert data["total_claims"] == 10
        assert data["total_sessions"] == 5
        assert isinstance(data["total_cornerstones"], int)

    def test_stats_no_storage(self):
        with _make_client() as (tc, mock_plugin):
            mock_plugin.storage = None
            resp = tc.get("/api/v1/stats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["storage_ok"] is False
        assert len(data["errors"]) > 0


# ---------------------------------------------------------------------------
# Tests — Remember
# ---------------------------------------------------------------------------

class TestRemember:
    def test_remember_with_messages(self, client):
        payload = {
            "conversation": [
                {"role": "user", "content": "My name is Andrew"},
                {"role": "assistant", "content": "Got it, Andrew"},
            ],
            "session_id": "sess-abc",
            "scope": "user",
        }
        resp = client.post("/api/v1/remember", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["ok"] is True
        assert data["claims_extracted"] == 3
        assert data["claims_stored"] == 2

    def test_remember_with_raw_text(self, client):
        payload = {"conversation": "Andrew lives in Bangkok"}
        resp = client.post("/api/v1/remember", json=payload)
        assert resp.status_code == 200
        assert resp.json()["ok"] is True

    def test_remember_rejects_very_long_single_message_422(self, client):
        payload = {"conversation": "x" * 12001}
        resp = client.post("/api/v1/remember", json=payload)
        assert resp.status_code == 422
        assert "too long" in resp.json()["detail"].lower()

    def test_remember_delegates_to_plugin(self):
        with _make_client() as (tc, mock_plugin):
            payload = {
                "conversation": [{"role": "user", "content": "test"}],
                "session_id": "sess-xyz",
                "owner_id": "owner-a",
            }
            tc.post("/api/v1/remember", json=payload)
        mock_plugin.remember.assert_called_once()
        call_kwargs = mock_plugin.remember.call_args.kwargs
        assert call_kwargs["session_id"] == "sess-xyz"
        assert call_kwargs["owner_id"] == "owner-a"

    def test_remember_missing_conversation(self, client):
        resp = client.post("/api/v1/remember", json={})
        assert resp.status_code == 422

    def test_remember_with_errors(self):
        result_with_errors = _RememberResult(
            ok=False,
            errors=["ExtractionPipeline not available"],
            claims_stored=0,
        )
        with _make_client(remember_result=result_with_errors) as (tc, _):
            resp = tc.post(
                "/api/v1/remember",
                json={"conversation": "test"},
            )
        assert resp.status_code == 200
        data = resp.json()
        assert data["ok"] is False
        assert len(data["errors"]) > 0


# ---------------------------------------------------------------------------
# Tests — Retrieve
# ---------------------------------------------------------------------------

class TestRetrieve:
    def test_retrieve_ok(self, client):
        payload = {"query": "What does Andrew prefer?"}
        resp = client.post("/api/v1/retrieve", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        # RetrievalResult fields are present (serialised from dataclass)
        assert isinstance(data, dict)

    def test_retrieve_with_budget(self):
        with _make_client() as (tc, mock_plugin):
            payload = {"query": "test", "token_budget": 2000, "mode": "fast"}
            resp = tc.post("/api/v1/retrieve", json=payload)
        assert resp.status_code == 200
        call_kwargs = mock_plugin.retrieve.call_args.kwargs
        assert call_kwargs["token_budget"] == 2000
        assert call_kwargs["mode"] == "lightweight"  # "fast" aliased to "lightweight"

    def test_retrieve_unavailable(self):
        with _make_client() as (tc, mock_plugin):
            mock_plugin.retrieve = AsyncMock(return_value=None)
            resp = tc.post("/api/v1/retrieve", json={"query": "test"})
        assert resp.status_code == 503

    def test_retrieve_missing_query(self, client):
        resp = client.post("/api/v1/retrieve", json={})
        assert resp.status_code == 422

    def test_retrieve_default_mode(self):
        with _make_client() as (tc, mock_plugin):
            tc.post("/api/v1/retrieve", json={"query": "anything"})
        call_kwargs = mock_plugin.retrieve.call_args.kwargs
        assert call_kwargs["mode"] == "auto"

    def test_retrieve_invalid_strategy_returns_422(self, client):
        resp = client.post("/api/v1/retrieve", json={"query": "anything", "mode": "invalid"})
        assert resp.status_code == 422


# ---------------------------------------------------------------------------
# Tests — Forget
# ---------------------------------------------------------------------------

class TestForget:
    def test_forget_ok(self, client):
        resp = client.delete("/api/v1/memories/mem-123")
        assert resp.status_code == 200
        data = resp.json()
        assert data["deleted"] is True

    def test_forget_hard_delete(self):
        with _make_client() as (tc, mock_plugin):
            resp = tc.delete("/api/v1/memories/mem-123?hard=true")
        assert resp.status_code == 200
        call_kwargs = mock_plugin.storage.delete_claim.call_args
        assert call_kwargs.kwargs.get("hard") is True

    def test_forget_soft_delete_default(self):
        with _make_client() as (tc, mock_plugin):
            tc.delete("/api/v1/memories/mem-123")
        call_kwargs = mock_plugin.storage.delete_claim.call_args
        assert call_kwargs.kwargs.get("hard") is False

    def test_forget_cornerstone_protected(self):
        """When delete_claim raises, endpoint returns 500."""
        def _raise(*a, **kw):
            raise Exception("cornerstone protected")
        with _make_client() as (tc, mock_plugin):
            mock_plugin.storage.delete_claim = AsyncMock(side_effect=_raise)
            resp = tc.delete("/api/v1/memories/cs-1")
        assert resp.status_code == 500

    def test_forget_storage_unavailable(self):
        """When storage is None, endpoint returns 503."""
        with _make_client() as (tc, mock_plugin):
            mock_plugin.storage = None
            resp = tc.delete("/api/v1/memories/mem-x")
        assert resp.status_code == 503

    def test_forget_delegates_memory_id(self):
        with _make_client() as (tc, mock_plugin):
            tc.delete("/api/v1/memories/target-mem-id")
        call_args = mock_plugin.storage.delete_claim.call_args
        assert call_args.args[0] == "target-mem-id"


# ---------------------------------------------------------------------------
# Tests — Cornerstones
# ---------------------------------------------------------------------------

class TestCornerstones:
    def test_list_cornerstones(self, client):
        resp = client.get("/api/v1/cornerstones")
        assert resp.status_code == 200
        data = resp.json()
        assert "cornerstones" in data
        assert "count" in data
        assert data["count"] == 1

    def test_list_cornerstones_empty(self):
        with _make_client(cornerstones=[]) as (tc, _):
            resp = tc.get("/api/v1/cornerstones")
        assert resp.status_code == 200
        assert resp.json()["count"] == 0

    def test_list_cornerstones_multiple(self):
        cs_list = [_Cornerstone(cs_id=f"cs-{i}") for i in range(3)]
        with _make_client(cornerstones=cs_list) as (tc, _):
            resp = tc.get("/api/v1/cornerstones")
        assert resp.json()["count"] == 3

    def test_seal_cornerstone(self):
        payload = {"label": "Core Identity", "content": "I am Eva"}
        with _make_client() as (tc, mock_plugin):
            resp = tc.post("/api/v1/cornerstones/mem-1/seal", json=payload)
        assert resp.status_code == 200
        assert "cornerstone" in resp.json()
        mock_plugin.seal_cornerstone.assert_called_once()
        call_kwargs = mock_plugin.seal_cornerstone.call_args.kwargs
        assert call_kwargs["memory_id"] == "mem-1"
        assert call_kwargs["label"] == "Core Identity"

    def test_seal_cornerstone_no_body(self, client):
        resp = client.post("/api/v1/cornerstones/mem-1/seal")
        assert resp.status_code == 200

    def test_seal_cornerstone_unavailable(self):
        with _make_client() as (tc, mock_plugin):
            mock_plugin.seal_cornerstone = AsyncMock(return_value=None)
            resp = tc.post("/api/v1/cornerstones/mem-1/seal")
        assert resp.status_code == 503

    def test_unseal_cornerstone(self):
        with _make_client() as (tc, mock_plugin):
            resp = tc.post("/api/v1/cornerstones/cs-1/unseal")
        assert resp.status_code == 200
        assert "cornerstone" in resp.json()
        mock_plugin.unseal_cornerstone.assert_called_once_with(memory_id="cs-1")

    def test_unseal_cornerstone_unavailable(self):
        with _make_client() as (tc, mock_plugin):
            mock_plugin.unseal_cornerstone = AsyncMock(return_value=None)
            resp = tc.post("/api/v1/cornerstones/cs-1/unseal")
        assert resp.status_code == 503


# ---------------------------------------------------------------------------
# Tests — Feedback
# ---------------------------------------------------------------------------

class TestFeedback:
    def test_feedback_helpful(self, client):
        payload = {
            "memory_id": "mem-1",
            "helpful": True,
            "context": "very relevant",
            "target_type": "claim",
        }
        resp = client.post("/api/v1/feedback", json=payload)
        assert resp.status_code == 200
        assert "feedback" in resp.json()

    def test_feedback_not_helpful(self, client):
        payload = {"memory_id": "mem-1", "helpful": False}
        resp = client.post("/api/v1/feedback", json=payload)
        assert resp.status_code == 200

    def test_feedback_delegates_correctly(self):
        payload = {
            "memory_id": "mem-42",
            "helpful": True,
            "context": "great",
            "target_type": "cornerstone",
        }
        with _make_client() as (tc, mock_plugin):
            tc.post("/api/v1/feedback", json=payload)
        call_kwargs = mock_plugin.feedback.call_args.kwargs
        assert call_kwargs["memory_id"] == "mem-42"
        assert call_kwargs["helpful"] is True
        assert call_kwargs["context"] == "great"
        assert call_kwargs["target_type"] == "cornerstone"

    def test_feedback_unavailable(self):
        with _make_client() as (tc, mock_plugin):
            mock_plugin.feedback = AsyncMock(return_value=None)
            resp = tc.post(
                "/api/v1/feedback",
                json={"memory_id": "m1", "helpful": True},
            )
        assert resp.status_code == 503

    def test_feedback_missing_fields(self, client):
        # missing `helpful` field
        resp = client.post("/api/v1/feedback", json={"memory_id": "m1"})
        assert resp.status_code == 422

    def test_feedback_default_target_type(self):
        with _make_client() as (tc, mock_plugin):
            tc.post("/api/v1/feedback", json={"memory_id": "m1", "helpful": True})
        call_kwargs = mock_plugin.feedback.call_args.kwargs
        assert call_kwargs["target_type"] == "claim"


# ---------------------------------------------------------------------------
# Tests — Authentication
# ---------------------------------------------------------------------------

class TestAuth:
    def test_no_auth_required_by_default(self, client):
        resp = client.get("/api/v1/health")
        assert resp.status_code == 200

    def test_valid_key_passes(self, authed_client):
        resp = authed_client.get("/api/v1/health", headers={"X-API-Key": "test-key"})
        assert resp.status_code == 200

    def test_wrong_key_rejected(self, authed_client):
        resp = authed_client.get("/api/v1/health", headers={"X-API-Key": "wrong-key"})
        assert resp.status_code == 401

    def test_missing_key_rejected(self, authed_client):
        resp = authed_client.get("/api/v1/health")
        assert resp.status_code == 401

    def test_auth_required_on_get_endpoints(self, authed_client):
        get_endpoints = [
            "/api/v1/health",
            "/api/v1/stats",
            "/api/v1/cornerstones",
        ]
        for path in get_endpoints:
            resp = authed_client.get(path)  # no key
            assert resp.status_code == 401, (
                f"Expected 401 for GET {path}, got {resp.status_code}"
            )

    def test_auth_required_on_post_endpoints(self, authed_client):
        post_endpoints = [
            ("/api/v1/remember", {"conversation": "test"}),
            ("/api/v1/retrieve", {"query": "test"}),
            ("/api/v1/feedback", {"memory_id": "m1", "helpful": True}),
        ]
        for path, body in post_endpoints:
            resp = authed_client.post(path, json=body)  # no key
            assert resp.status_code == 401, (
                f"Expected 401 for POST {path}, got {resp.status_code}"
            )

    def test_auth_unlocks_with_valid_key(self):
        with _make_client(api_key="secret-123") as (tc, _):
            # Without key
            resp = tc.get("/api/v1/health")
            assert resp.status_code == 401
            # With key
            resp = tc.get("/api/v1/health", headers={"X-API-Key": "secret-123"})
            assert resp.status_code == 200


# ---------------------------------------------------------------------------
# Tests — CORS
# ---------------------------------------------------------------------------

class TestCORS:
    def test_cors_preflight(self):
        # Security: default CORS is restrictive (empty allow_origins).
        # Use an explicit origin list to test CORS preflight behavior.
        from cortex.api.http_server import create_app
        mock_plugin = _make_mock_plugin()
        app = create_app(cors_origins=["http://localhost:3000"])
        with patch("cortex.api.plugin.CortexPlugin") as MP:
            MP.return_value = mock_plugin
            with TestClient(app) as tc:
                resp = tc.options(
                    "/api/v1/health",
                    headers={
                        "Origin": "http://localhost:3000",
                        "Access-Control-Request-Method": "GET",
                    },
                )
        assert resp.status_code in (200, 204)

    def test_cors_allow_origin_wildcard(self):
        # Security: default allow_origins is [] (restrictive).
        # Wildcard must be explicitly configured; verify it works when set.
        from cortex.api.http_server import create_app
        mock_plugin = _make_mock_plugin()
        app = create_app(cors_origins=["*"])
        with patch("cortex.api.plugin.CortexPlugin") as MP:
            MP.return_value = mock_plugin
            with TestClient(app) as tc:
                resp = tc.get("/api/v1/health", headers={"Origin": "http://any-origin.com"})
        assert resp.status_code == 200
        assert "access-control-allow-origin" in resp.headers

    def test_custom_cors_origins(self):
        from cortex.api.http_server import create_app
        mock_plugin = _make_mock_plugin()
        app = create_app(cors_origins=["https://app.100yen.ai"])
        with patch("cortex.api.plugin.CortexPlugin") as MP:
            MP.return_value = mock_plugin
            with TestClient(app) as tc:
                resp = tc.get(
                    "/api/v1/health",
                    headers={"Origin": "https://app.100yen.ai"},
                )
        assert resp.status_code == 200


# ---------------------------------------------------------------------------
# Tests — OpenAPI metadata
# ---------------------------------------------------------------------------

class TestOpenAPI:
    def test_openapi_schema_available(self, client):
        resp = client.get("/openapi.json")
        assert resp.status_code == 200
        schema = resp.json()
        assert schema["info"]["title"] == "Cortex HTTP API"

    def test_all_endpoints_registered(self, client):
        resp = client.get("/openapi.json")
        paths = resp.json()["paths"]
        required_paths = [
            "/api/v1/health",
            "/api/v1/stats",
            "/api/v1/remember",
            "/api/v1/retrieve",
            "/api/v1/cornerstones",
            "/api/v1/feedback",
        ]
        for path in required_paths:
            assert path in paths, f"Expected {path} in OpenAPI paths"

    def test_forget_endpoint_registered(self, client):
        resp = client.get("/openapi.json")
        paths = resp.json()["paths"]
        # DELETE /api/v1/memories/{memory_id}
        memory_paths = [p for p in paths if "memories" in p]
        assert len(memory_paths) >= 1

    def test_cornerstone_seal_unseal_registered(self, client):
        resp = client.get("/openapi.json")
        paths = resp.json()["paths"]
        seal_paths = [p for p in paths if "seal" in p or "unseal" in p]
        assert len(seal_paths) >= 2, f"Expected seal+unseal paths, found: {seal_paths}"

    def test_docs_available(self, client):
        resp = client.get("/docs")
        assert resp.status_code == 200


# ---------------------------------------------------------------------------
# Tests — Serialisation helper
# ---------------------------------------------------------------------------

class TestSerialise:
    def test_serialise_primitives(self):
        from cortex.api.http_server import _serialise
        assert _serialise(None) is None
        assert _serialise(42) == 42
        assert _serialise(3.14) == 3.14
        assert _serialise("hello") == "hello"
        assert _serialise(True) is True
        assert _serialise(False) is False

    def test_serialise_dict_strips_private(self):
        from cortex.api.http_server import _serialise
        result = _serialise({"a": 1, "_hidden": "x", "b": [1, 2]})
        assert result == {"a": 1, "b": [1, 2]}

    def test_serialise_list(self):
        from cortex.api.http_server import _serialise
        result = _serialise([1, "two", None])
        assert result == [1, "two", None]

    def test_serialise_dataclass_via_dict(self):
        from cortex.api.http_server import _serialise
        cs = _Cornerstone(cs_id="x", label="Test")
        result = _serialise(cs.__dict__)
        assert result["cs_id"] == "x"
        assert result["label"] == "Test"

    def test_serialise_nested(self):
        from cortex.api.http_server import _serialise
        obj = {"memories": [{"id": "m1", "text": "test"}]}
        result = _serialise(obj)
        assert result["memories"][0]["id"] == "m1"

    def test_serialise_object_with_dict(self):
        from cortex.api.http_server import _serialise

        class _FakeObj:
            def __init__(self):
                self.name = "foo"
                self._private = "bar"

        result = _serialise(_FakeObj())
        assert result == {"name": "foo"}


# ---------------------------------------------------------------------------
# Tests — create_app import guard
# ---------------------------------------------------------------------------

class TestCreateApp:
    def test_create_app_returns_fastapi(self):
        from fastapi import FastAPI
        from cortex.api.http_server import create_app

        app = create_app()
        assert isinstance(app, FastAPI)

    def test_create_app_with_api_key(self):
        from fastapi import FastAPI
        from cortex.api.http_server import create_app

        app = create_app(api_key="my-secret")
        assert isinstance(app, FastAPI)

    def test_create_app_with_custom_cors(self):
        from fastapi import FastAPI
        from cortex.api.http_server import create_app

        app = create_app(cors_origins=["https://example.com"])
        assert isinstance(app, FastAPI)
