"""
tests/test_openclaw_integration.py — Tests for the OpenClaw ↔ Cortex bridge.

Tests the integration layer (openclaw_plugin + openclaw_config) in isolation,
mocking CortexPlugin internals so no LLM or DB is needed.
"""
from __future__ import annotations

import asyncio
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Fixtures & helpers
# ---------------------------------------------------------------------------

SAMPLE_CONFIG = {
    "backend": "cortex",
    "cortex": {
        "db_path": "/tmp/test-cortex.db",
        "owner_id": "eva-test",
        "auto_sleep_on_session_end": True,
        "cornerstone_injection": True,
    },
}

SAMPLE_CONFIG_NO_SLEEP = {
    "backend": "cortex",
    "cortex": {
        "db_path": "/tmp/test-cortex.db",
        "owner_id": "eva-test",
        "auto_sleep_on_session_end": False,
        "cornerstone_injection": True,
    },
}

SAMPLE_CONFIG_NO_CORNERSTONES = {
    "backend": "cortex",
    "cortex": {
        "db_path": "/tmp/test-cortex.db",
        "owner_id": "eva-test",
        "cornerstone_injection": False,
    },
}

SAMPLE_CONVERSATION = [
    {"role": "user", "content": "I prefer dark mode in all my editors."},
    {"role": "assistant", "content": "Noted! I'll remember your dark mode preference."},
]


# Mock result types that mirror Cortex's actual return types
@dataclass
class MockRememberResult:
    session_id: str = "test-session"
    claims_extracted: int = 2
    claims_stored: int = 1
    entities_resolved: int = 1
    noise_filtered: int = 0
    errors: List[str] = field(default_factory=list)
    ok: bool = True


@dataclass
class MockRetrievedItem:
    item_type: str = "claim"
    item_id: str = "item-1"
    content: str = "User prefers dark mode"
    score: float = 0.95
    provenance: Optional[str] = None


@dataclass
class MockRetrievalResult:
    items: List[MockRetrievedItem] = field(default_factory=list)
    retrieval_id: str = "ret-1"
    mode: str = "lightweight"
    latency_ms: int = 50
    tokens_used: int = 100


@dataclass
class MockWakeReport:
    session_id: str = "test-session"
    cornerstones_loaded: int = 3
    errors: List[str] = field(default_factory=list)


@dataclass
class MockSleepReport:
    session_id: str = "test-session"
    memories_summarised: int = 0
    errors: List[str] = field(default_factory=list)


@dataclass
class MockHealthReport:
    ok: bool = True
    storage: str = "ok"
    modules: Dict[str, str] = field(default_factory=lambda: {
        "storage": "ok",
        "extraction": "ok",
        "retrieval": "ok",
        "llm": "ok",
    })
    errors: List[str] = field(default_factory=list)
    timestamp: str = "2026-03-06T15:00:00"


def _make_mock_plugin():
    """Create a mock CortexPlugin with all async methods."""
    mock = MagicMock()
    mock.remember = AsyncMock(return_value=MockRememberResult())
    mock.retrieve = AsyncMock(return_value=MockRetrievalResult(
        items=[
            MockRetrievedItem(item_type="cornerstone", content="I am Eva, Andrew's AI companion"),
            MockRetrievedItem(item_type="claim", content="User prefers dark mode"),
            MockRetrievedItem(item_type="claim", content="User uses fly.io for deployments"),
        ]
    ))
    mock.wake = AsyncMock(return_value=MockWakeReport())
    mock.sleep = AsyncMock(return_value=MockSleepReport())
    mock.health = AsyncMock(return_value=MockHealthReport())
    mock.config = MagicMock()
    mock.config.owner_id = "eva-test"
    return mock


@pytest.fixture
def mock_cortex_plugin():
    """Patch CortexPlugin constructor to return a mock."""
    mock = _make_mock_plugin()
    with patch(
        "cortex.integrations.openclaw_plugin.CortexPlugin",
        return_value=mock,
    ):
        yield mock


# ---------------------------------------------------------------------------
# Config bridge tests
# ---------------------------------------------------------------------------


class TestOpenClawConfig:
    """Tests for openclaw_config.py — the config bridge."""

    def test_basic_config_mapping(self):
        from cortex.integrations.openclaw_config import openclaw_config_to_cortex

        kwargs, extra = openclaw_config_to_cortex(SAMPLE_CONFIG)

        assert kwargs["storage_db_path"].endswith("/tmp/test-cortex.db")
        assert kwargs["owner_id"] == "eva-test"
        assert extra["auto_sleep_on_session_end"] is True
        assert extra["cornerstone_injection"] is True
        assert extra["config_path"] is None

    def test_config_with_tilde_expansion(self):
        from cortex.integrations.openclaw_config import openclaw_config_to_cortex

        config = {
            "backend": "cortex",
            "cortex": {"db_path": "~/test.db", "owner_id": "eva"},
        }
        kwargs, _ = openclaw_config_to_cortex(config)
        assert "~" not in kwargs["storage_db_path"]
        assert kwargs["storage_db_path"].endswith("test.db")

    def test_config_defaults(self):
        from cortex.integrations.openclaw_config import openclaw_config_to_cortex

        kwargs, extra = openclaw_config_to_cortex({"backend": "cortex"})
        # Should get defaults
        assert kwargs["owner_id"] == "default"
        assert extra["auto_sleep_on_session_end"] is True
        assert extra["cornerstone_injection"] is True

    def test_config_with_toml_path(self):
        from cortex.integrations.openclaw_config import openclaw_config_to_cortex

        config = {
            "backend": "cortex",
            "cortex": {
                "db_path": "/tmp/test.db",
                "config_path": "/tmp/cortex.toml",
                "owner_id": "eva",
            },
        }
        kwargs, extra = openclaw_config_to_cortex(config)
        assert extra["config_path"].endswith("/tmp/cortex.toml")

    def test_resolve_path_with_env_vars(self):
        from cortex.integrations.openclaw_config import resolve_path

        os.environ["TEST_CORTEX_DIR"] = "/tmp/test-env"
        result = resolve_path("$TEST_CORTEX_DIR/cortex.db")
        assert result.endswith("/tmp/test-env/cortex.db")
        del os.environ["TEST_CORTEX_DIR"]


# ---------------------------------------------------------------------------
# Plugin bridge tests
# ---------------------------------------------------------------------------


class TestOpenClawPlugin:
    """Tests for openclaw_plugin.py — the main bridge class."""

    @pytest.mark.asyncio
    async def test_remember_maps_correctly(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        result = await plugin.remember(SAMPLE_CONVERSATION)

        assert result["ok"] is True
        assert result["claims_extracted"] == 2
        assert result["claims_stored"] == 1
        mock_cortex_plugin.remember.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_remember_passes_session_from_metadata(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        await plugin.remember(
            SAMPLE_CONVERSATION,
            metadata={"session_id": "my-session-42"},
        )

        call_kwargs = mock_cortex_plugin.remember.call_args
        assert call_kwargs.kwargs.get("session_id") == "my-session-42"

    @pytest.mark.asyncio
    async def test_remember_uses_current_session(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        await plugin.on_session_start("active-session")
        await plugin.remember(SAMPLE_CONVERSATION)

        call_kwargs = mock_cortex_plugin.remember.call_args
        assert call_kwargs.kwargs.get("session_id") == "active-session"

    @pytest.mark.asyncio
    async def test_remember_graceful_on_error(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        mock_cortex_plugin.remember = AsyncMock(
            side_effect=RuntimeError("LLM provider down")
        )
        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        result = await plugin.remember(SAMPLE_CONVERSATION)

        assert result["ok"] is False
        assert "LLM provider down" in result["errors"][0]

    @pytest.mark.asyncio
    async def test_recall_returns_formatted_string(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        context = await plugin.recall("What are the user's preferences?")

        assert isinstance(context, str)
        assert "Core Identity & Preferences" in context
        assert "Eva, Andrew's AI companion" in context
        assert "dark mode" in context
        assert "fly.io" in context

    @pytest.mark.asyncio
    async def test_recall_without_cornerstone_injection(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG_NO_CORNERSTONES)
        context = await plugin.recall("preferences")

        assert "Core Identity" not in context
        # Cornerstones should still appear as regular memories
        assert "Relevant Memories" in context

    @pytest.mark.asyncio
    async def test_recall_returns_empty_on_error(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        mock_cortex_plugin.retrieve = AsyncMock(
            side_effect=RuntimeError("DB locked")
        )
        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        context = await plugin.recall("anything")

        assert context == ""

    @pytest.mark.asyncio
    async def test_recall_returns_empty_on_none_result(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        mock_cortex_plugin.retrieve = AsyncMock(return_value=None)
        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        context = await plugin.recall("anything")

        assert context == ""

    @pytest.mark.asyncio
    async def test_recall_passes_token_budget(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        await plugin.recall("query", options={"token_budget": 2000})

        call_kwargs = mock_cortex_plugin.retrieve.call_args
        assert call_kwargs.kwargs.get("token_budget") == 2000


# ---------------------------------------------------------------------------
# Session lifecycle tests
# ---------------------------------------------------------------------------


class TestSessionLifecycle:
    """Tests for session start/end mapping to wake/sleep."""

    @pytest.mark.asyncio
    async def test_session_start_calls_wake(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        result = await plugin.on_session_start("sess-1")

        assert result["session_id"] == "test-session"
        assert result["cornerstones_loaded"] == 3
        mock_cortex_plugin.wake.assert_awaited_once_with(session_id="sess-1")

    @pytest.mark.asyncio
    async def test_session_end_calls_sleep(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        await plugin.on_session_start("sess-1")
        result = await plugin.on_session_end("sess-1")

        assert result["session_id"] == "test-session"
        mock_cortex_plugin.sleep.assert_awaited_once_with(session_id="sess-1")

    @pytest.mark.asyncio
    async def test_session_end_skips_sleep_when_disabled(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG_NO_SLEEP)
        result = await plugin.on_session_end("sess-1")

        assert result["errors"] == []
        mock_cortex_plugin.sleep.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_full_session_lifecycle(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)

        # Start session
        start = await plugin.on_session_start("lifecycle-test")
        assert start["cornerstones_loaded"] == 3

        # Remember
        remember = await plugin.remember(SAMPLE_CONVERSATION)
        assert remember["ok"] is True

        # Recall
        context = await plugin.recall("dark mode preference")
        assert len(context) > 0

        # End session
        end = await plugin.on_session_end("lifecycle-test")
        assert end["errors"] == []

    @pytest.mark.asyncio
    async def test_session_start_graceful_on_error(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        mock_cortex_plugin.wake = AsyncMock(
            side_effect=RuntimeError("DB init failed")
        )
        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        result = await plugin.on_session_start("sess-fail")

        assert result["session_id"] == "sess-fail"
        assert result["cornerstones_loaded"] == 0
        assert "DB init failed" in result["errors"][0]

    @pytest.mark.asyncio
    async def test_session_end_graceful_on_error(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        mock_cortex_plugin.sleep = AsyncMock(
            side_effect=RuntimeError("sleep failed")
        )
        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        result = await plugin.on_session_end("sess-fail")

        assert result["session_id"] == "sess-fail"
        assert "sleep failed" in result["errors"][0]


# ---------------------------------------------------------------------------
# Health check tests
# ---------------------------------------------------------------------------


class TestHealth:
    """Tests for health() mapping."""

    @pytest.mark.asyncio
    async def test_health_ok(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        result = await plugin.health()

        assert result["ok"] is True
        assert result["backend"] == "cortex"
        assert result["storage"] == "ok"
        assert "extraction" in result["modules"]

    @pytest.mark.asyncio
    async def test_health_graceful_on_error(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        mock_cortex_plugin.health = AsyncMock(
            side_effect=RuntimeError("total failure")
        )
        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        result = await plugin.health()

        assert result["ok"] is False
        assert result["backend"] == "cortex"
        assert "total failure" in result["errors"][0]


# ---------------------------------------------------------------------------
# Cornerstone injection formatting tests
# ---------------------------------------------------------------------------


class TestCornerstoneInjection:
    """Tests for cornerstone injection in recall responses."""

    @pytest.mark.asyncio
    async def test_cornerstones_appear_first_in_context(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        context = await plugin.recall("test")

        lines = context.split("\n")
        # First non-empty section should be cornerstones
        assert lines[0] == "## Core Identity & Preferences"
        # Regular memories come after
        assert "## Relevant Memories" in context

    @pytest.mark.asyncio
    async def test_no_cornerstones_when_disabled(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG_NO_CORNERSTONES)
        context = await plugin.recall("test")

        assert "Core Identity" not in context
        # But cornerstone items should still show up as regular items
        assert "cornerstone" in context.lower() or "Relevant Memories" in context

    @pytest.mark.asyncio
    async def test_empty_retrieval_returns_empty_string(self, mock_cortex_plugin):
        from cortex.integrations.openclaw_plugin import OpenClawCortexPlugin

        mock_cortex_plugin.retrieve = AsyncMock(
            return_value=MockRetrievalResult(items=[])
        )
        plugin = OpenClawCortexPlugin(config=SAMPLE_CONFIG)
        context = await plugin.recall("test")

        assert context == ""
