"""
tests/test_server_integration.py — Integration tests for server wiring.

Covers:
- Hosted mode (CORTEX_DATA_DIR set) creates DBManager + StatsDB
- Self-host mode (db_path set, no CORTEX_DATA_DIR) skips DBManager + StatsDB
- UsageTrackingMiddleware appears in middleware chain for hosted mode only
- get_user_storage returns correct adapter for both modes
- Version bump to 1.2.1
- Lifespan teardown clears holders
"""
from __future__ import annotations

import os
import sys
from contextlib import asynccontextmanager
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio

# Skip entire module if FastAPI / httpx are not available
pytest.importorskip("fastapi", reason="fastapi not installed")
pytest.importorskip("httpx", reason="httpx not installed")

from fastapi.testclient import TestClient  # noqa: E402

import cortex.api.deps as _deps  # noqa: E402


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

def _make_mock_plugin(db_path=None):
    """Return a mock CortexPlugin with a mock storage."""
    storage = AsyncMock()
    storage.initialize = AsyncMock()
    storage.close = AsyncMock()

    plugin = MagicMock()
    plugin.storage = storage
    plugin.config = MagicMock()
    plugin.config.db_pool_max_open = 100
    plugin.config.db_pool_idle_timeout_seconds = 300.0

    cfg_issues = []
    return plugin, cfg_issues


def _make_mock_db_manager():
    mgr = AsyncMock()
    mgr.initialize = AsyncMock()
    mgr.close_all = AsyncMock()
    mgr.get_adapter = AsyncMock(return_value=AsyncMock())
    return mgr


def _make_mock_stats_db():
    sdb = AsyncMock()
    sdb.initialize = AsyncMock()
    sdb.close = AsyncMock()
    return sdb


# ---------------------------------------------------------------------------
# Fixtures: reset holders between tests
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def reset_holders():
    """Clear all shared holders before and after each test."""
    for holder in (_deps.plugin_holder, _deps.auth_holder,
                   _deps.db_manager_holder, _deps.stats_db_holder):
        holder.clear()
    yield
    for holder in (_deps.plugin_holder, _deps.auth_holder,
                   _deps.db_manager_holder, _deps.stats_db_holder):
        holder.clear()


# ---------------------------------------------------------------------------
# Version test
# ---------------------------------------------------------------------------

class TestVersion:
    def test_version_is_1_2_1(self):
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch.dict(os.environ, {}, clear=False):
            # Remove CORTEX_DATA_DIR if set
            env = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}
            with patch.dict(os.environ, env, clear=True):
                app = create_app(db_path="/tmp/test.db")
        assert app.version == "1.2.1"


# ---------------------------------------------------------------------------
# Hosted mode: DBManager created and stored
# ---------------------------------------------------------------------------

class TestHostedModeDBManager:
    def test_hosted_mode_creates_db_manager(self, tmp_path):
        """CORTEX_DATA_DIR set → DBManager initialized and stored in holder."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        env = {"CORTEX_DATA_DIR": str(tmp_path)}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr), \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb), \
             patch.dict(os.environ, env):
            app = create_app()
            with TestClient(app):
                assert _deps.db_manager_holder.get("manager") is mock_mgr
                mock_mgr.initialize.assert_awaited_once()

    def test_hosted_mode_db_manager_uses_data_dir(self, tmp_path):
        """DBManager is created with data_dir from CORTEX_DATA_DIR."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        data_dir = str(tmp_path / "hosted_data")
        env = {"CORTEX_DATA_DIR": data_dir}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr) as MockDBMgr, \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb), \
             patch.dict(os.environ, env):
            app = create_app()
            with TestClient(app):
                call_kwargs = MockDBMgr.call_args
                assert call_kwargs is not None
                assert str(call_kwargs.kwargs.get("data_dir") or call_kwargs.args[0]) == data_dir

    def test_hosted_mode_reads_pool_config(self, tmp_path):
        """DBManager respects db_pool_max_open and db_pool_idle_timeout_seconds from plugin config."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_plugin.config.db_pool_max_open = 50
        mock_plugin.config.db_pool_idle_timeout_seconds = 120.0
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        env = {"CORTEX_DATA_DIR": str(tmp_path)}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr) as MockDBMgr, \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb), \
             patch.dict(os.environ, env):
            app = create_app()
            with TestClient(app):
                call_kwargs = MockDBMgr.call_args
                assert int(call_kwargs.kwargs.get("max_open", call_kwargs.args[1] if len(call_kwargs.args) > 1 else 100)) == 50
                assert float(call_kwargs.kwargs.get("idle_timeout", call_kwargs.args[2] if len(call_kwargs.args) > 2 else 300)) == 120.0

    def test_hosted_mode_db_manager_closed_on_shutdown(self, tmp_path):
        """DBManager.close_all() is called on shutdown."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        env = {"CORTEX_DATA_DIR": str(tmp_path)}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr), \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb), \
             patch.dict(os.environ, env):
            app = create_app()
            with TestClient(app):
                pass  # startup + shutdown

        mock_mgr.close_all.assert_awaited_once()


# ---------------------------------------------------------------------------
# Hosted mode: StatsDB created and stored
# ---------------------------------------------------------------------------

class TestHostedModeStatsDB:
    def test_hosted_mode_creates_stats_db(self, tmp_path):
        """CORTEX_DATA_DIR set → StatsDB initialized and stored in holder."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        env = {"CORTEX_DATA_DIR": str(tmp_path)}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr), \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb), \
             patch.dict(os.environ, env):
            app = create_app()
            with TestClient(app):
                assert _deps.stats_db_holder.get("db") is mock_sdb
                mock_sdb.initialize.assert_awaited_once()

    def test_stats_db_default_path_uses_data_dir(self, tmp_path):
        """StatsDB path defaults to {data_dir}/stats.db."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        data_dir = str(tmp_path)
        env = {"CORTEX_DATA_DIR": data_dir}
        # Ensure CORTEX_STATS_DB is not set
        env_clean = {k: v for k, v in {**os.environ, **env}.items() if k != "CORTEX_STATS_DB"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr), \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb) as MockStatsDB, \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app()
            with TestClient(app):
                call_kwargs = MockStatsDB.call_args
                db_path_arg = call_kwargs.kwargs.get("db_path") or call_kwargs.args[0]
                assert db_path_arg == f"{data_dir}/stats.db"

    def test_stats_db_respects_cortex_stats_db_env(self, tmp_path):
        """StatsDB uses CORTEX_STATS_DB env var when set."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        custom_stats = str(tmp_path / "custom_stats.db")
        env = {"CORTEX_DATA_DIR": str(tmp_path), "CORTEX_STATS_DB": custom_stats}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr), \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb) as MockStatsDB, \
             patch.dict(os.environ, env):
            app = create_app()
            with TestClient(app):
                call_kwargs = MockStatsDB.call_args
                db_path_arg = call_kwargs.kwargs.get("db_path") or call_kwargs.args[0]
                assert db_path_arg == custom_stats

    def test_stats_db_closed_on_shutdown(self, tmp_path):
        """StatsDB.close() is called on shutdown."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        env = {"CORTEX_DATA_DIR": str(tmp_path)}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr), \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb), \
             patch.dict(os.environ, env):
            app = create_app()
            with TestClient(app):
                pass

        mock_sdb.close.assert_awaited_once()


# ---------------------------------------------------------------------------
# Self-host mode: DBManager and StatsDB skipped
# ---------------------------------------------------------------------------

class TestSelfHostMode:
    def test_selfhost_skips_db_manager(self, tmp_path):
        """No CORTEX_DATA_DIR → DBManager is NOT created."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()

        env_clean = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager") as MockDBMgr, \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app(db_path=str(tmp_path / "single.db"))
            with TestClient(app):
                MockDBMgr.assert_not_called()
                assert _deps.db_manager_holder.get("manager") is None

    def test_selfhost_skips_stats_db(self, tmp_path):
        """No CORTEX_DATA_DIR → StatsDB is NOT created."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()

        env_clean = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.api.stats_db.StatsDB") as MockStatsDB, \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app(db_path=str(tmp_path / "single.db"))
            with TestClient(app):
                MockStatsDB.assert_not_called()
                assert _deps.stats_db_holder.get("db") is None

    def test_selfhost_holders_remain_empty(self, tmp_path):
        """Both holders stay empty in self-host mode throughout lifespan."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()

        env_clean = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app(db_path=str(tmp_path / "single.db"))
            with TestClient(app):
                assert _deps.db_manager_holder.get("manager") is None
                assert _deps.stats_db_holder.get("db") is None


# ---------------------------------------------------------------------------
# UsageTrackingMiddleware in middleware chain
# ---------------------------------------------------------------------------

class TestUsageTrackingMiddleware:
    def test_middleware_present_in_hosted_mode(self, tmp_path):
        """UsageTrackingMiddleware is in app.user_middleware for hosted mode."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()
        mock_mgr = _make_mock_db_manager()
        mock_sdb = _make_mock_stats_db()

        env = {"CORTEX_DATA_DIR": str(tmp_path)}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch("cortex.storage.db_manager.DBManager", return_value=mock_mgr), \
             patch("cortex.api.stats_db.StatsDB", return_value=mock_sdb), \
             patch.dict(os.environ, env):
            app = create_app()

        from cortex.api.middleware.usage_tracker import UsageTrackingMiddleware
        middleware_classes = [m.cls for m in app.user_middleware]
        # _LazyUsageTrackingMiddleware is a subclass of UsageTrackingMiddleware
        assert any(
            issubclass(cls, UsageTrackingMiddleware)
            for cls in middleware_classes
            if isinstance(cls, type)
        )

    def test_middleware_absent_in_selfhost_mode(self, tmp_path):
        """UsageTrackingMiddleware is NOT added in self-host mode."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()

        env_clean = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app(db_path=str(tmp_path / "single.db"))

        from cortex.api.middleware.usage_tracker import UsageTrackingMiddleware
        middleware_classes = [m.cls for m in app.user_middleware]
        has_utm = any(
            isinstance(cls, type) and issubclass(cls, UsageTrackingMiddleware)
            for cls in middleware_classes
        )
        assert not has_utm


# ---------------------------------------------------------------------------
# get_user_storage dependency
# ---------------------------------------------------------------------------

class TestGetUserStorage:
    @pytest.mark.asyncio
    async def test_hosted_mode_uses_db_manager(self):
        """get_user_storage uses DBManager when active."""
        from cortex.api.deps import get_user_storage

        mock_adapter = MagicMock()
        mock_mgr = AsyncMock()
        mock_mgr.get_adapter = AsyncMock(return_value=mock_adapter)

        _deps.db_manager_holder["manager"] = mock_mgr

        result = await get_user_storage("user-123")

        mock_mgr.get_adapter.assert_awaited_once_with("user-123")
        assert result is mock_adapter

    @pytest.mark.asyncio
    async def test_selfhost_mode_uses_plugin_storage(self):
        """get_user_storage returns plugin.storage when no DBManager."""
        from cortex.api.deps import get_user_storage

        mock_storage = MagicMock()
        mock_plugin = MagicMock()
        mock_plugin.storage = mock_storage

        _deps.plugin_holder["plugin"] = mock_plugin
        # db_manager_holder is empty (reset by fixture)

        result = await get_user_storage("default")
        assert result is mock_storage

    @pytest.mark.asyncio
    async def test_hosted_mode_different_owner_ids(self):
        """get_user_storage calls get_adapter with the correct owner_id."""
        from cortex.api.deps import get_user_storage

        returned_adapters = {}

        async def _fake_get_adapter(owner_id):
            adapter = MagicMock(name=f"adapter-{owner_id}")
            returned_adapters[owner_id] = adapter
            return adapter

        mock_mgr = AsyncMock()
        mock_mgr.get_adapter = _fake_get_adapter
        _deps.db_manager_holder["manager"] = mock_mgr

        a1 = await get_user_storage("alice")
        a2 = await get_user_storage("bob")

        assert a1 is returned_adapters["alice"]
        assert a2 is returned_adapters["bob"]
        assert a1 is not a2

    @pytest.mark.asyncio
    async def test_db_manager_takes_priority_over_plugin_storage(self):
        """If both DBManager and plugin.storage exist, DBManager wins."""
        from cortex.api.deps import get_user_storage

        mock_adapter = MagicMock()
        mock_mgr = AsyncMock()
        mock_mgr.get_adapter = AsyncMock(return_value=mock_adapter)

        mock_storage = MagicMock()
        mock_plugin = MagicMock()
        mock_plugin.storage = mock_storage

        _deps.db_manager_holder["manager"] = mock_mgr
        _deps.plugin_holder["plugin"] = mock_plugin

        result = await get_user_storage("user-xyz")
        assert result is mock_adapter  # DBManager wins
        assert result is not mock_storage


# ---------------------------------------------------------------------------
# Backward compatibility: create_app signature unchanged
# ---------------------------------------------------------------------------

class TestBackwardCompatibility:
    def test_create_app_with_db_path(self, tmp_path):
        """create_app(db_path=...) still works (self-host mode)."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()

        env_clean = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app(db_path=str(tmp_path / "cortex.db"))

        assert app is not None

    def test_create_app_no_args(self):
        """create_app() with no args still works."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()

        env_clean = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app()

        assert app is not None

    def test_plugin_holder_populated_selfhost(self, tmp_path):
        """plugin_holder is populated in self-host mode (backward compat)."""
        from cortex.api.http_server import create_app
        mock_plugin, _ = _make_mock_plugin()

        env_clean = {k: v for k, v in os.environ.items() if k != "CORTEX_DATA_DIR"}

        with patch("cortex.api.plugin.CortexPlugin", return_value=mock_plugin), \
             patch("cortex.config.validate_config", return_value=[]), \
             patch.dict(os.environ, env_clean, clear=True):
            app = create_app(db_path=str(tmp_path / "cortex.db"))
            with TestClient(app):
                assert _deps.plugin_holder.get("plugin") is mock_plugin
