# Tests for Memory Tools and API Endpoints
# Created: 2026-02-05
# Tests for RememberTool, RecallTool, and session list API

import pytest
import json
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch, AsyncMock

from pocketclaw.tools.builtin.memory import RememberTool, RecallTool
from pocketclaw.memory.manager import MemoryManager, get_memory_manager
from pocketclaw.memory.protocol import MemoryType, MemoryEntry
from pocketclaw.memory.file_store import FileMemoryStore


@pytest.fixture
def temp_memory_path():
    """Create a temporary directory for memory tests."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir)


@pytest.fixture
def memory_manager(temp_memory_path):
    """Create a MemoryManager with temp path."""
    return MemoryManager(base_path=temp_memory_path)


@pytest.fixture
def mock_memory_manager(temp_memory_path):
    """Mock get_memory_manager to return our test manager."""
    manager = MemoryManager(base_path=temp_memory_path)
    with patch("pocketclaw.tools.builtin.memory.get_memory_manager", return_value=manager):
        yield manager


# =============================================================================
# RememberTool Tests
# =============================================================================

class TestRememberTool:
    """Tests for RememberTool."""

    def test_tool_definition(self):
        """Test tool has correct name and definition."""
        tool = RememberTool()
        assert tool.name == "remember"
        assert "Save important information" in tool.description

        params = tool.parameters
        assert "content" in params["properties"]
        assert "tags" in params["properties"]
        assert "content" in params["required"]

    def test_definition_formats(self):
        """Test definition converts to OpenAI and Anthropic formats."""
        tool = RememberTool()
        defn = tool.definition

        openai = defn.to_openai_schema()
        assert openai["function"]["name"] == "remember"

        anthropic = defn.to_anthropic_schema()
        assert anthropic["name"] == "remember"
        assert "input_schema" in anthropic

    @pytest.mark.asyncio
    async def test_remember_content(self, mock_memory_manager):
        """Test saving content to memory."""
        tool = RememberTool()
        result = await tool.execute(content="User prefers dark mode")

        assert "Remembered" in result
        assert "dark mode" in result

    @pytest.mark.asyncio
    async def test_remember_with_tags(self, mock_memory_manager):
        """Test saving content with tags."""
        tool = RememberTool()
        result = await tool.execute(
            content="User's favorite color is blue",
            tags=["preferences", "color"]
        )

        assert "Remembered" in result
        assert "preferences" in result or "color" in result

    @pytest.mark.asyncio
    async def test_remember_persists(self, mock_memory_manager):
        """Test that remembered content is actually persisted."""
        tool = RememberTool()
        await tool.execute(content="User works at Anthropic", tags=["work"])

        # Verify it was saved
        results = await mock_memory_manager.search("Anthropic")
        assert len(results) >= 1
        assert "Anthropic" in results[0].content

    @pytest.mark.asyncio
    async def test_remember_empty_content(self, mock_memory_manager):
        """Test error handling for empty content."""
        tool = RememberTool()
        result = await tool.execute(content="")
        # Should still work but with empty content warning or success
        # The tool doesn't explicitly check for empty, so it will save
        assert "Remembered" in result or "Error" in result

    @pytest.mark.asyncio
    async def test_remember_long_content_truncated_in_response(self, mock_memory_manager):
        """Test that long content is truncated in the response message."""
        tool = RememberTool()
        long_content = "A" * 200  # 200 characters
        result = await tool.execute(content=long_content)

        # Response should be truncated with "..."
        assert "..." in result
        assert len(result) < 200


# =============================================================================
# RecallTool Tests
# =============================================================================

class TestRecallTool:
    """Tests for RecallTool."""

    def test_tool_definition(self):
        """Test tool has correct name and definition."""
        tool = RecallTool()
        assert tool.name == "recall"
        assert "Search long-term memories" in tool.description

        params = tool.parameters
        assert "query" in params["properties"]
        assert "limit" in params["properties"]
        assert "query" in params["required"]

    @pytest.mark.asyncio
    async def test_recall_no_results(self, mock_memory_manager):
        """Test recall when no memories match."""
        tool = RecallTool()
        result = await tool.execute(query="nonexistent topic xyz123")

        assert "No memories found" in result

    @pytest.mark.asyncio
    async def test_recall_with_results(self, mock_memory_manager):
        """Test recall finds matching memories."""
        # First save some memories
        await mock_memory_manager.remember("User prefers Python programming", tags=["skills"])
        await mock_memory_manager.remember("User likes dark mode", tags=["preferences"])

        tool = RecallTool()
        result = await tool.execute(query="Python")

        assert "Found" in result
        assert "Python" in result

    @pytest.mark.asyncio
    async def test_recall_with_limit(self, mock_memory_manager):
        """Test recall respects limit parameter."""
        # Save multiple memories
        for i in range(5):
            await mock_memory_manager.remember(f"Test memory {i} about coding")

        tool = RecallTool()
        result = await tool.execute(query="coding", limit=2)

        # Should find memories but respect limit
        assert "Found" in result

    @pytest.mark.asyncio
    async def test_recall_shows_tags(self, mock_memory_manager):
        """Test recall includes tags in results."""
        await mock_memory_manager.remember("User likes hiking", tags=["hobbies", "outdoor"])

        tool = RecallTool()
        result = await tool.execute(query="hiking")

        assert "Found" in result
        # Tags should be shown in brackets
        assert "hobbies" in result or "outdoor" in result


# =============================================================================
# Integration Tests
# =============================================================================

class TestMemoryToolsIntegration:
    """Integration tests for memory tools working together."""

    @pytest.mark.asyncio
    async def test_remember_then_recall_workflow(self, mock_memory_manager):
        """Test the full remember → recall workflow."""
        remember_tool = RememberTool()
        recall_tool = RecallTool()

        # Remember something
        remember_result = await remember_tool.execute(
            content="The user's project is called PocketClaw",
            tags=["project", "name"]
        )
        assert "Remembered" in remember_result

        # Recall it
        recall_result = await recall_tool.execute(query="PocketClaw")
        assert "Found" in recall_result
        assert "PocketClaw" in recall_result

    @pytest.mark.asyncio
    async def test_multiple_memories_recall(self, mock_memory_manager):
        """Test recalling from multiple related memories."""
        remember_tool = RememberTool()
        recall_tool = RecallTool()

        # Remember multiple related facts
        await remember_tool.execute(content="User uses MacOS for development")
        await remember_tool.execute(content="User prefers VSCode as editor")
        await remember_tool.execute(content="User develops in Python and TypeScript")

        # Recall with broader query
        result = await recall_tool.execute(query="development")

        assert "Found" in result
        # Should find at least one of the memories
        assert "development" in result.lower() or "MacOS" in result or "VSCode" in result


# =============================================================================
# PocketPaw Native Memory Tool Execution Tests
# =============================================================================

class TestPocketPawNativeMemoryExecution:
    """Test memory tool execution in PocketPaw Native orchestrator."""

    @pytest.fixture
    def mock_settings(self, temp_memory_path):
        """Create mock settings."""
        from pocketclaw.config import Settings
        return Settings(
            anthropic_api_key="test-key",
            file_jail_path=temp_memory_path.parent,
        )

    @pytest.mark.asyncio
    async def test_remember_tool_execution(self, mock_settings, temp_memory_path):
        """Test remember tool execution through orchestrator._execute_tool."""
        from pocketclaw.agents.pocketpaw_native import PocketPawOrchestrator

        # Patch memory manager at the import location (local import in _execute_tool)
        manager = MemoryManager(base_path=temp_memory_path)
        with patch("pocketclaw.memory.manager.get_memory_manager", return_value=manager):
            orchestrator = PocketPawOrchestrator(mock_settings)

            # Execute remember tool
            result = await orchestrator._execute_tool("remember", {
                "content": "User prefers vim keybindings",
                "tags": ["preferences", "editor"]
            })

            assert "Remembered" in result
            assert "vim" in result

    @pytest.mark.asyncio
    async def test_recall_tool_execution(self, mock_settings, temp_memory_path):
        """Test recall tool execution through orchestrator._execute_tool."""
        from pocketclaw.agents.pocketpaw_native import PocketPawOrchestrator

        # Patch memory manager at the import location
        manager = MemoryManager(base_path=temp_memory_path)
        with patch("pocketclaw.memory.manager.get_memory_manager", return_value=manager):
            orchestrator = PocketPawOrchestrator(mock_settings)

            # First remember something
            await orchestrator._execute_tool("remember", {"content": "User likes coffee"})

            # Then recall it
            result = await orchestrator._execute_tool("recall", {"query": "coffee"})

            assert "Found" in result or "coffee" in result

    @pytest.mark.asyncio
    async def test_remember_empty_content_error(self, mock_settings, temp_memory_path):
        """Test error when remembering empty content."""
        from pocketclaw.agents.pocketpaw_native import PocketPawOrchestrator

        manager = MemoryManager(base_path=temp_memory_path)
        with patch("pocketclaw.memory.manager.get_memory_manager", return_value=manager):
            orchestrator = PocketPawOrchestrator(mock_settings)

            result = await orchestrator._execute_tool("remember", {"content": ""})

            assert "Error" in result

    @pytest.mark.asyncio
    async def test_recall_empty_query_error(self, mock_settings, temp_memory_path):
        """Test error when recalling with empty query."""
        from pocketclaw.agents.pocketpaw_native import PocketPawOrchestrator

        manager = MemoryManager(base_path=temp_memory_path)
        with patch("pocketclaw.memory.manager.get_memory_manager", return_value=manager):
            orchestrator = PocketPawOrchestrator(mock_settings)

            result = await orchestrator._execute_tool("recall", {"query": ""})

            assert "Error" in result


# =============================================================================
# Session List API Tests
# =============================================================================

class TestSessionListAPI:
    """Tests for the session list API endpoint."""

    @pytest.fixture
    def sessions_path(self, temp_memory_path):
        """Create sessions directory with test data."""
        sessions_dir = temp_memory_path / "sessions"
        sessions_dir.mkdir(parents=True, exist_ok=True)

        # Create test session files
        session1 = [
            {"id": "1", "role": "user", "content": "Hello, how are you?", "timestamp": "2026-02-05T10:00:00"},
            {"id": "2", "role": "assistant", "content": "I'm doing great!", "timestamp": "2026-02-05T10:01:00"},
        ]
        session2 = [
            {"id": "3", "role": "user", "content": "What's the weather?", "timestamp": "2026-02-05T11:00:00"},
            {"id": "4", "role": "assistant", "content": "It's sunny today.", "timestamp": "2026-02-05T11:01:00"},
            {"id": "5", "role": "user", "content": "Thanks!", "timestamp": "2026-02-05T11:02:00"},
        ]

        (sessions_dir / "websocket_session1.json").write_text(json.dumps(session1))
        (sessions_dir / "websocket_session2.json").write_text(json.dumps(session2))

        return sessions_dir

    @pytest.mark.asyncio
    async def test_list_sessions(self, sessions_path, temp_memory_path):
        """Test listing sessions from directory."""
        # Simulate what the API does
        sessions = []
        for session_file in sorted(sessions_path.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
            data = json.loads(session_file.read_text())
            if data:
                first_msg = data[0]
                last_msg = data[-1]
                sessions.append({
                    "id": session_file.stem,
                    "message_count": len(data),
                    "first_message": first_msg.get("content", "")[:100],
                    "last_message": last_msg.get("content", "")[:100],
                    "updated_at": last_msg.get("timestamp", ""),
                    "created_at": first_msg.get("timestamp", ""),
                })

        assert len(sessions) == 2
        # Check session data
        session_ids = [s["id"] for s in sessions]
        assert "websocket_session1" in session_ids
        assert "websocket_session2" in session_ids

        # Check message counts
        for s in sessions:
            if s["id"] == "websocket_session1":
                assert s["message_count"] == 2
            elif s["id"] == "websocket_session2":
                assert s["message_count"] == 3

    @pytest.mark.asyncio
    async def test_list_sessions_empty_directory(self, temp_memory_path):
        """Test listing sessions when directory is empty."""
        sessions_dir = temp_memory_path / "sessions"
        sessions_dir.mkdir(parents=True, exist_ok=True)

        sessions = list(sessions_dir.glob("*.json"))
        assert len(sessions) == 0

    @pytest.mark.asyncio
    async def test_list_sessions_malformed_json(self, temp_memory_path):
        """Test handling of malformed JSON session files."""
        sessions_dir = temp_memory_path / "sessions"
        sessions_dir.mkdir(parents=True, exist_ok=True)

        # Create a malformed file
        (sessions_dir / "bad_session.json").write_text("not valid json {")

        # Create a valid file
        valid_session = [{"id": "1", "role": "user", "content": "Test", "timestamp": "2026-02-05T10:00:00"}]
        (sessions_dir / "good_session.json").write_text(json.dumps(valid_session))

        # Simulate API logic
        sessions = []
        for session_file in sessions_dir.glob("*.json"):
            try:
                data = json.loads(session_file.read_text())
                if data:
                    sessions.append({"id": session_file.stem})
            except json.JSONDecodeError:
                continue

        # Should only have the valid session
        assert len(sessions) == 1
        assert sessions[0]["id"] == "good_session"


# =============================================================================
# Memory Tool Registration Tests
# =============================================================================

class TestMemoryToolRegistration:
    """Test that memory tools are properly registered."""

    def test_tools_importable(self):
        """Test that memory tools can be imported from builtin package."""
        from pocketclaw.tools.builtin import RememberTool, RecallTool

        assert RememberTool is not None
        assert RecallTool is not None

    def test_tools_in_all_exports(self):
        """Test that memory tools are in __all__ exports."""
        from pocketclaw.tools import builtin

        assert "RememberTool" in builtin.__all__
        assert "RecallTool" in builtin.__all__

    def test_tools_work_with_registry(self, mock_memory_manager):
        """Test that tools work with ToolRegistry."""
        from pocketclaw.tools.registry import ToolRegistry
        from pocketclaw.tools.builtin.memory import RememberTool, RecallTool

        registry = ToolRegistry()
        registry.register(RememberTool())
        registry.register(RecallTool())

        assert registry.has("remember")
        assert registry.has("recall")

        # Check definitions
        defs = registry.get_definitions("anthropic")
        names = [d["name"] for d in defs]
        assert "remember" in names
        assert "recall" in names
