"""
tests/test_openclaw_loader.py — Safe OpenClaw plugin loading (#147).

Tests that the loader never crashes, regardless of:
- Cortex not installed
- Bad config
- Corrupt/inaccessible DB
- Good config (happy path)
- Lazy init on first call
"""
from __future__ import annotations

import asyncio
import logging
import os
import tempfile
from pathlib import Path
from typing import Any, Dict, Optional
from unittest.mock import MagicMock, patch, AsyncMock

import pytest

# ---------------------------------------------------------------------------
# We CAN import the loader itself (it has no hard deps on cortex internals)
# ---------------------------------------------------------------------------
from cortex.integrations.openclaw_loader import (
    load_cortex_plugin,
    validate_config,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def tmp_db_dir(tmp_path):
    """A writable temp directory for db_path."""
    return str(tmp_path / "cortex.db")


@pytest.fixture
def good_config(tmp_db_dir):
    """A valid OpenClaw memory config."""
    return {
        "backend": "cortex",
        "cortex": {
            "db_path": tmp_db_dir,
            "owner_id": "eva-test",
            "auto_sleep_on_session_end": True,
            "cornerstone_injection": True,
        },
    }


@pytest.fixture
def bad_config_missing_cortex():
    """Config missing the cortex section entirely."""
    return {"backend": "cortex"}


@pytest.fixture
def bad_config_missing_db():
    """Config with cortex section but no db_path."""
    return {
        "backend": "cortex",
        "cortex": {"owner_id": "eva"},
    }


@pytest.fixture
def bad_config_unwritable_db():
    """Config with db_path in a non-existent, non-writable directory."""
    return {
        "backend": "cortex",
        "cortex": {
            "db_path": "/nonexistent/impossible/path/cortex.db",
            "owner_id": "eva",
        },
    }


# ---------------------------------------------------------------------------
# Config Validation Tests
# ---------------------------------------------------------------------------

class TestConfigValidation:
    """Test validate_config() catches bad configs before loading."""

    def test_valid_config(self, good_config):
        errors = validate_config(good_config)
        assert errors == []

    def test_non_dict_config(self):
        errors = validate_config("not a dict")
        assert len(errors) == 1
        assert "dict" in errors[0].lower()

    def test_missing_cortex_section(self, bad_config_missing_cortex):
        errors = validate_config(bad_config_missing_cortex)
        assert len(errors) >= 1
        assert "cortex" in errors[0].lower()

    def test_missing_db_path(self, bad_config_missing_db):
        errors = validate_config(bad_config_missing_db)
        assert any("db_path" in e for e in errors)

    def test_unwritable_db_path(self, bad_config_unwritable_db):
        errors = validate_config(bad_config_unwritable_db)
        assert len(errors) >= 1
        assert any("not exist" in e or "not writable" in e for e in errors)

    def test_empty_config(self):
        errors = validate_config({})
        assert len(errors) >= 1


# ---------------------------------------------------------------------------
# Import Guard Tests
# ---------------------------------------------------------------------------

class TestImportGuard:
    """Test that load_cortex_plugin() handles import failures gracefully."""

    def test_import_error_returns_none(self, good_config, caplog):
        """Simulates cortex not being installed."""
        with patch(
            "cortex.integrations.openclaw_loader.validate_config",
            return_value=[],
        ):
            with patch.dict(
                "sys.modules",
                {"cortex.integrations.openclaw_plugin": None},
            ):
                # Force ImportError on the inner import
                import builtins
                original_import = builtins.__import__

                def mock_import(name, *args, **kwargs):
                    if name == "cortex.integrations.openclaw_plugin":
                        raise ImportError("No module named 'cortex'")
                    return original_import(name, *args, **kwargs)

                with patch("builtins.__import__", side_effect=mock_import):
                    result = load_cortex_plugin(good_config)

        assert result is None

    def test_bad_config_returns_none(self, bad_config_missing_cortex, caplog):
        """Bad config → returns None, never tries to import."""
        with caplog.at_level(logging.ERROR, logger="cortex.loader"):
            result = load_cortex_plugin(bad_config_missing_cortex)
        assert result is None
        assert any("validation failed" in r.message.lower() for r in caplog.records)

    def test_bad_config_no_db_returns_none(self, bad_config_missing_db, caplog):
        with caplog.at_level(logging.ERROR, logger="cortex.loader"):
            result = load_cortex_plugin(bad_config_missing_db)
        assert result is None

    def test_unwritable_db_returns_none(self, bad_config_unwritable_db, caplog):
        with caplog.at_level(logging.ERROR, logger="cortex.loader"):
            result = load_cortex_plugin(bad_config_unwritable_db)
        assert result is None


# ---------------------------------------------------------------------------
# Lazy Init Tests
# ---------------------------------------------------------------------------

class TestLazyInit:
    """Test that plugin delays DB connection to first use."""

    def test_init_does_not_create_plugin(self, good_config):
        """__init__ should NOT call CortexPlugin()."""
        with patch(
            "cortex.integrations.openclaw_plugin.CortexPlugin"
        ) as mock_cortex:
            from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin
            plugin = OpenClawCortexPlugin(good_config)
            # CortexPlugin should NOT have been called in __init__
            mock_cortex.assert_not_called()
            # Internal state should reflect lazy init
            assert plugin._plugin is None
            assert plugin._init_failed is False

    def test_first_call_triggers_init(self, good_config):
        """First call to _ensure_plugin() should create the CortexPlugin."""
        mock_cortex_instance = MagicMock()
        mock_cortex_instance.config = MagicMock()

        with patch(
            "cortex.integrations.openclaw_plugin.CortexPlugin",
            return_value=mock_cortex_instance,
        ) as mock_cortex_cls:
            from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin
            plugin = OpenClawCortexPlugin(good_config)

            # Not yet initialized
            assert plugin._plugin is None

            # Trigger lazy init
            result = plugin._ensure_plugin()

            # Now it should be initialized
            assert result is mock_cortex_instance
            assert plugin._plugin is mock_cortex_instance
            mock_cortex_cls.assert_called_once()

    def test_init_failure_returns_none(self, good_config, caplog):
        """If CortexPlugin() raises, _ensure_plugin returns None."""
        with patch(
            "cortex.integrations.openclaw_plugin.CortexPlugin",
            side_effect=RuntimeError("DB is corrupt"),
        ):
            from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin
            plugin = OpenClawCortexPlugin(good_config)

            with caplog.at_level(logging.ERROR):
                result = plugin._ensure_plugin()

            assert result is None
            assert plugin._init_failed is True
            assert "DB is corrupt" in (plugin._init_error or "")

    def test_init_failure_does_not_retry(self, good_config):
        """After init failure, subsequent calls return None without retrying."""
        call_count = 0

        def counting_init(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            raise RuntimeError("still broken")

        with patch(
            "cortex.integrations.openclaw_plugin.CortexPlugin",
            side_effect=counting_init,
        ):
            from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin
            plugin = OpenClawCortexPlugin(good_config)

            plugin._ensure_plugin()  # first call → tries and fails
            plugin._ensure_plugin()  # second call → should NOT retry
            plugin._ensure_plugin()  # third call → should NOT retry

            assert call_count == 1  # only tried once


# ---------------------------------------------------------------------------
# Graceful Degradation Tests
# ---------------------------------------------------------------------------

class TestGracefulDegradation:
    """Test that all public methods degrade gracefully when init fails."""

    @pytest.fixture
    def broken_plugin(self, good_config):
        """A plugin where _ensure_plugin always returns None."""
        with patch(
            "cortex.integrations.openclaw_plugin.CortexPlugin",
            side_effect=RuntimeError("corrupt DB"),
        ):
            from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin
            plugin = OpenClawCortexPlugin(good_config)
            plugin._ensure_plugin()  # trigger the failure
            return plugin

    @pytest.mark.asyncio
    async def test_remember_degraded(self, broken_plugin):
        result = await broken_plugin.remember(
            [{"role": "user", "content": "hello"}]
        )
        assert result["ok"] is False
        assert result["claims_stored"] == 0
        assert len(result["errors"]) > 0

    @pytest.mark.asyncio
    async def test_recall_degraded(self, broken_plugin):
        result = await broken_plugin.recall("what do I like?")
        assert result == ""

    @pytest.mark.asyncio
    async def test_on_session_start_degraded(self, broken_plugin):
        result = await broken_plugin.on_session_start("session-test")
        assert result["cornerstones_loaded"] == 0
        assert len(result["errors"]) > 0

    @pytest.mark.asyncio
    async def test_on_session_end_degraded(self, broken_plugin):
        result = await broken_plugin.on_session_end("session-test")
        assert result["session_id"] == "session-test"
        # Should not raise

    @pytest.mark.asyncio
    async def test_health_degraded(self, broken_plugin):
        result = await broken_plugin.health()
        assert result["ok"] is False
        assert result["backend"] == "cortex"
        assert len(result["errors"]) > 0


# ---------------------------------------------------------------------------
# Happy Path (with mock CortexPlugin)
# ---------------------------------------------------------------------------

class TestHappyPath:
    """Test normal operation with a mocked CortexPlugin."""

    @pytest.fixture
    def working_plugin(self, good_config):
        """A plugin with a fully mocked CortexPlugin (lazy init)."""
        mock_instance = MagicMock()
        mock_instance.config = MagicMock()

        # Mock async methods
        remember_result = MagicMock(
            ok=True, claims_extracted=3, claims_stored=2,
            entities_resolved=1, errors=[],
        )
        mock_instance.remember = AsyncMock(return_value=remember_result)

        retrieve_result = MagicMock(items=[])
        mock_instance.retrieve = AsyncMock(return_value=retrieve_result)

        wake_result = MagicMock(
            session_id="s1", cornerstones_loaded=5, errors=[],
        )
        mock_instance.wake = AsyncMock(return_value=wake_result)

        sleep_result = MagicMock(session_id="s1", errors=[])
        mock_instance.sleep = AsyncMock(return_value=sleep_result)

        health_result = MagicMock(
            ok=True, storage="sqlite", modules={"extraction": True}, errors=[],
        )
        mock_instance.health = AsyncMock(return_value=health_result)

        with patch(
            "cortex.integrations.openclaw_plugin.CortexPlugin",
            return_value=mock_instance,
        ):
            from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin
            plugin = OpenClawCortexPlugin(good_config)
            # Pre-trigger lazy init so all methods use the mock
            plugin._ensure_plugin()
            return plugin

    @pytest.mark.asyncio
    async def test_remember_works(self, working_plugin):
        result = await working_plugin.remember(
            [{"role": "user", "content": "I like cats"}],
            {"session_id": "s1"},
        )
        assert result["ok"] is True
        assert result["claims_extracted"] == 3
        assert result["claims_stored"] == 2

    @pytest.mark.asyncio
    async def test_recall_works(self, working_plugin):
        result = await working_plugin.recall("what do I like?")
        assert isinstance(result, str)

    @pytest.mark.asyncio
    async def test_session_lifecycle(self, working_plugin):
        start = await working_plugin.on_session_start("s1")
        assert start["cornerstones_loaded"] == 5
        assert start["errors"] == []

        end = await working_plugin.on_session_end("s1")
        assert end["session_id"] == "s1"

    @pytest.mark.asyncio
    async def test_health_works(self, working_plugin):
        result = await working_plugin.health()
        assert result["ok"] is True
        assert result["backend"] == "cortex"


# ---------------------------------------------------------------------------
# Full Loader Integration Test (mocked)
# ---------------------------------------------------------------------------

class TestLoaderIntegration:
    """Test load_cortex_plugin() end-to-end with mocks."""

    def test_full_load_success(self, good_config):
        """Full happy path: validate → import → instantiate → health."""
        mock_instance = MagicMock()
        mock_instance.config = MagicMock()
        mock_instance._plugin = None
        mock_instance._init_failed = False
        mock_instance._init_error = None

        health_result = {
            "ok": True, "backend": "cortex",
            "storage": "sqlite", "modules": {}, "errors": [],
        }
        mock_instance.health = AsyncMock(return_value=health_result)

        with patch(
            "cortex.integrations.openclaw_plugin.OpenClawCortexPlugin",
            return_value=mock_instance,
        ):
            result = load_cortex_plugin(good_config)

        assert result is mock_instance

    def test_full_load_with_health_timeout(self, good_config):
        """Health check times out but plugin still loads."""
        mock_instance = MagicMock()
        mock_instance.config = MagicMock()
        mock_instance._plugin = None
        mock_instance._init_failed = False
        mock_instance._init_error = None

        async def slow_health():
            await asyncio.sleep(999)

        mock_instance.health = slow_health

        with patch(
            "cortex.integrations.openclaw_plugin.OpenClawCortexPlugin",
            return_value=mock_instance,
        ):
            # Should still return the plugin despite timeout
            result = load_cortex_plugin(good_config, health_timeout=0.1)

        assert result is mock_instance
