"""
Unit tests for memstate-langchain.

These tests use the ``responses`` library to mock HTTP calls, so they run
without a real Memstate API key or network access.

Run with:
    pytest tests/test_unit.py -v
"""

import json
import pytest
import responses as responses_lib

from memstate_langchain import (
    MemstateAPIError,
    MemstateChatMessageHistory,
    MemstateClient,
    MemstateConnectionError,
    MemstateMemory,
    MemstateSemanticMemory,
    create_memstate_tools,
)
from memstate_langchain.tools import (
    MemstateDeleteTool,
    MemstateGetTool,
    MemstateHistoryTool,
    MemstateRememberTool,
    MemstateSearchTool,
    MemstateSetTool,
)

API_KEY = "mst_test_unit_key"
BASE_URL = "https://api.memstate.ai"
PROJECT_ID = "test-project"


# ---------------------------------------------------------------------------
# MemstateClient unit tests
# ---------------------------------------------------------------------------


class TestMemstateClient:
    """Tests for the low-level REST client."""

    def test_init_requires_api_key(self):
        with pytest.raises(ValueError, match="api_key is required"):
            MemstateClient(api_key="")

    def test_init_strips_trailing_slash(self):
        client = MemstateClient(api_key=API_KEY, base_url="https://api.memstate.ai/")
        assert client.base_url == "https://api.memstate.ai"

    @responses_lib.activate
    def test_get_status(self):
        responses_lib.add(
            responses_lib.GET,
            f"{BASE_URL}/api/v1/status",
            json={
                "status": "ok",
                "memory_count": 42,
                "project_count": 3,
                "storage_backend": "postgresql",
                "features": {
                    "conflict_detection": True,
                    "llm_processing": True,
                },
            },
            status=200,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        result = client.get_status()
        assert result["status"] == "ok"
        assert result["memory_count"] == 42

    @responses_lib.activate
    def test_remember(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/remember",
            json={
                "memory_id": "mem_abc123",
                "action": "created",
                "message": "Memory stored",
                "version": 1,
            },
            status=200,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        result = client.remember(
            content="PostgreSQL 15",
            keypath="project.test_project.database.type",
            project_id=PROJECT_ID,
            category="fact",
        )
        assert result["memory_id"] == "mem_abc123"
        assert result["action"] == "created"
        assert result["version"] == 1

    @responses_lib.activate
    def test_ingest(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/ingest",
            json={
                "ingestion_id": "ing_xyz789",
                "status": "complete",
                "memories_created": ["mem_1", "mem_2"],
                "keypaths_created": ["database.type", "database.host"],
                "keypaths_updated": [],
                "revision_number": 5,
                "conflicts": [],
                "processing_time_ms": 1234,
                "message": "Ingested 2 memories",
            },
            status=200,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        result = client.ingest(
            project_id=PROJECT_ID,
            content="## Database\nUsing PostgreSQL 15 on Railway.",
            source="agent",
        )
        assert result["status"] == "complete"
        assert len(result["memories_created"]) == 2
        assert "database.type" in result["keypaths_created"]

    @responses_lib.activate
    def test_search(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={
                "results": [
                    {
                        "id": "mem_abc123",
                        "keypath": "project.test_project.database.type",
                        "summary": "PostgreSQL 15",
                        "score": 0.95,
                        "category": "fact",
                        "is_latest": True,
                    }
                ],
                "total_found": 1,
                "query": "database configuration",
            },
            status=200,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        result = client.search(
            query="database configuration",
            project_id=PROJECT_ID,
            limit=5,
        )
        assert result["total_found"] == 1
        assert result["results"][0]["score"] == 0.95
        assert result["results"][0]["keypath"] == "project.test_project.database.type"

    @responses_lib.activate
    def test_api_error_raises_memstate_api_error(self):
        responses_lib.add(
            responses_lib.GET,
            f"{BASE_URL}/api/v1/status",
            json={"error": "Unauthorized"},
            status=401,
        )
        client = MemstateClient(api_key="bad_key", base_url=BASE_URL)
        with pytest.raises(MemstateAPIError) as exc_info:
            client.get_status()
        assert exc_info.value.status_code == 401
        assert "Unauthorized" in str(exc_info.value)

    @responses_lib.activate
    def test_404_raises_memstate_api_error(self):
        responses_lib.add(
            responses_lib.GET,
            f"{BASE_URL}/api/v1/memories/nonexistent",
            json={"error": "Memory not found"},
            status=404,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        with pytest.raises(MemstateAPIError) as exc_info:
            client.get_memory("nonexistent")
        assert exc_info.value.status_code == 404

    def test_build_keypath_with_project_and_subpath(self):
        # Hyphens in project_id are sanitized to underscores in the keypath
        assert (
            MemstateClient.build_keypath("my-app", "database.type")
            == "project.my_app.database.type"
        )

    def test_build_keypath_project_only(self):
        # Hyphens in project_id are sanitized to underscores in the keypath
        assert MemstateClient.build_keypath("my-app") == "project.my_app"

    def test_build_keypath_empty_project(self):
        assert MemstateClient.build_keypath("", "some.path") == "some.path"

    def test_build_keypath_empty_subpath(self):
        # Hyphens in project_id are sanitized to underscores in the keypath
        assert MemstateClient.build_keypath("my-app", "") == "project.my_app"

    @responses_lib.activate
    def test_browse(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/browse",
            json={
                "memories": [
                    {
                        "id": "mem_1",
                        "keypath": "project.test_project.auth.provider",
                        "summary": "OAuth2 with Google",
                        "category": "decision",
                        "version": 2,
                        "is_latest": True,
                    }
                ],
                "total_found": 1,
                "keypath_prefix": "project.test-project",
            },
            status=200,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        result = client.browse(
            keypath_prefix="project.test-project",
            project_id=PROJECT_ID,
        )
        assert result["total_found"] == 1
        assert result["memories"][0]["summary"] == "OAuth2 with Google"

    @responses_lib.activate
    def test_history(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/history",
            json={
                "versions": [
                    {
                        "id": "mem_v1",
                        "keypath": "project.test_project.auth.provider",
                        "summary": "Basic auth",
                        "version": 1,
                        "is_latest": False,
                        "created_at": "2025-01-01T00:00:00Z",
                    },
                    {
                        "id": "mem_v2",
                        "keypath": "project.test_project.auth.provider",
                        "summary": "OAuth2 with Google",
                        "version": 2,
                        "is_latest": True,
                        "created_at": "2025-06-01T00:00:00Z",
                    },
                ],
                "total_versions": 2,
            },
            status=200,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        result = client.history(
            keypath="project.test_project.auth.provider",
            project_id=PROJECT_ID,
        )
        assert result["total_versions"] == 2
        assert result["versions"][-1]["is_latest"] is True

    def test_history_requires_memory_id_or_keypath(self):
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        with pytest.raises(ValueError, match="Either memory_id or keypath is required"):
            client.history()

    @responses_lib.activate
    def test_delete_memory(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/delete",
            json={
                "deleted_keypaths": ["project.test_project.old.setting"],
                "deleted_count": 1,
                "message": "Deleted 1 memory",
            },
            status=200,
        )
        client = MemstateClient(api_key=API_KEY, base_url=BASE_URL)
        result = client.delete_memory(
            keypath="project.test_project.old.setting",
            project_id=PROJECT_ID,
        )
        assert result["deleted_count"] == 1


# ---------------------------------------------------------------------------
# MemstateSemanticMemory unit tests
# ---------------------------------------------------------------------------


class TestMemstateSemanticMemory:
    """Tests for the modern LCEL semantic memory class."""

    def _make_memory(self) -> MemstateSemanticMemory:
        return MemstateSemanticMemory(
            api_key=API_KEY,
            project_id=PROJECT_ID,
            base_url=BASE_URL,
        )

    def test_memstate_memory_alias(self):
        """MemstateMemory should be an alias for MemstateSemanticMemory."""
        assert MemstateMemory is MemstateSemanticMemory

    def test_default_memory_key(self):
        memory = self._make_memory()
        assert memory.memory_key == "relevant_memories"

    def test_custom_memory_key(self):
        memory = MemstateSemanticMemory(
            api_key=API_KEY,
            project_id=PROJECT_ID,
            base_url=BASE_URL,
            memory_key="context",
        )
        assert memory.memory_key == "context"

    @responses_lib.activate
    def test_retrieve_returns_relevant_memories(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={
                "results": [
                    {
                        "id": "mem_1",
                        "keypath": "project.test_project.user.name",
                        "summary": "User's name is Alice",
                        "score": 0.92,
                    }
                ],
                "total_found": 1,
                "query": "user preferences",
            },
            status=200,
        )
        memory = self._make_memory()
        result = memory.retrieve({"input": "user preferences"})
        assert "relevant_memories" in result
        assert "Alice" in result["relevant_memories"]
        assert "0.92" in result["relevant_memories"]
        # Original input should be preserved
        assert result["input"] == "user preferences"

    @responses_lib.activate
    def test_retrieve_empty_results(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={"results": [], "total_found": 0, "query": "nothing"},
            status=200,
        )
        memory = self._make_memory()
        result = memory.retrieve({"input": "nothing"})
        assert result["relevant_memories"] == ""

    @responses_lib.activate
    def test_retrieve_api_error_returns_empty(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={"error": "Internal server error"},
            status=500,
        )
        memory = self._make_memory()
        # Should not raise — returns empty string on error
        result = memory.retrieve({"input": "test"})
        assert result["relevant_memories"] == ""

    @responses_lib.activate
    def test_save_calls_ingest(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/ingest",
            json={
                "ingestion_id": "ing_123",
                "status": "complete",
                "memories_created": ["mem_1"],
                "keypaths_created": ["conversation.turn1"],
                "keypaths_updated": [],
                "revision_number": 1,
                "conflicts": [],
                "processing_time_ms": 500,
                "message": "Ingested",
            },
            status=200,
        )
        memory = self._make_memory()

        # Simulate AIMessage output
        class FakeAIMessage:
            content = "2+2 equals 4."

        memory.save(
            inputs={"input": "What is 2+2?"},
            output=FakeAIMessage(),
        )
        # Verify the ingest call was made
        assert len(responses_lib.calls) == 1
        body = json.loads(responses_lib.calls[0].request.body)
        assert body["project_id"] == PROJECT_ID
        assert "What is 2+2?" in body["content"]
        assert "2+2 equals 4." in body["content"]
        assert body["source"] == "langchain"

    @responses_lib.activate
    def test_save_handles_string_output(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/ingest",
            json={
                "ingestion_id": "ing_456",
                "status": "complete",
                "memories_created": [],
                "keypaths_created": [],
                "keypaths_updated": [],
                "revision_number": 1,
                "conflicts": [],
                "processing_time_ms": 100,
                "message": "OK",
            },
            status=200,
        )
        memory = self._make_memory()
        memory.save(
            inputs={"input": "Hello"},
            output="Hi there!",  # String output
        )
        body = json.loads(responses_lib.calls[0].request.body)
        assert "Hi there!" in body["content"]

    @responses_lib.activate
    def test_clear_deletes_project_memories(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/delete",
            json={
                "deleted_keypaths": ["project.test-project"],
                "deleted_count": 5,
                "message": "Deleted 5 memories",
            },
            status=200,
        )
        memory = self._make_memory()
        memory.clear()
        assert len(responses_lib.calls) == 1
        body = json.loads(responses_lib.calls[0].request.body)
        assert body["keypath"] == "project.test_project"
        assert body["recursive"] is True

    @responses_lib.activate
    def test_clear_handles_404_gracefully(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/delete",
            json={"error": "Memory not found"},
            status=404,
        )
        memory = self._make_memory()
        # Should not raise on 404
        memory.clear()

    @responses_lib.activate
    def test_get_memories_uses_search_when_query_provided(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={
                "results": [{"id": "mem_1", "summary": "test", "score": 0.9}],
                "total_found": 1,
                "query": "auth",
            },
            status=200,
        )
        memory = self._make_memory()
        results = memory.get_memories(query="auth")
        assert len(results) == 1

    @responses_lib.activate
    def test_get_memories_uses_browse_when_no_query(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/browse",
            json={
                "memories": [{"id": "mem_1", "summary": "test"}],
                "total_found": 1,
                "keypath_prefix": "project.test-project",
            },
            status=200,
        )
        memory = self._make_memory()
        results = memory.get_memories()
        assert len(results) == 1

    @responses_lib.activate
    def test_wrap_injects_memory_and_saves(self):
        """wrap() should inject memories before chain call and save after."""
        # Mock search (retrieve)
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={
                "results": [
                    {
                        "id": "mem_1",
                        "keypath": "project.test_project.user.name",
                        "summary": "User is Alice",
                        "score": 0.95,
                    }
                ],
                "total_found": 1,
            },
            status=200,
        )
        # Mock ingest (save)
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/ingest",
            json={
                "ingestion_id": "ing_789",
                "status": "complete",
                "memories_created": [],
                "keypaths_created": [],
                "keypaths_updated": [],
                "revision_number": 1,
                "conflicts": [],
                "processing_time_ms": 100,
                "message": "OK",
            },
            status=200,
        )

        memory = self._make_memory()

        # Create a simple mock chain that returns a string
        from langchain_core.runnables import RunnableLambda

        captured_inputs = {}

        def mock_chain(inputs):
            captured_inputs.update(inputs)
            return "Mock AI response"

        wrapped = memory.wrap(RunnableLambda(mock_chain))
        result = wrapped.invoke({"input": "What is my name?"})

        # Verify memories were injected into the chain
        assert "relevant_memories" in captured_inputs
        assert "Alice" in captured_inputs["relevant_memories"]

        # Verify the result is passed through
        assert result == "Mock AI response"

        # Verify both search and ingest were called
        assert len(responses_lib.calls) == 2
        assert "/api/v1/memories/search" in responses_lib.calls[0].request.url
        assert "/api/v1/ingest" in responses_lib.calls[1].request.url


# ---------------------------------------------------------------------------
# MemstateChatMessageHistory unit tests
# ---------------------------------------------------------------------------


class TestMemstateChatMessageHistory:
    """Tests for the LangChain BaseChatMessageHistory implementation."""

    def _make_history(self, session_id: str = "test-session") -> MemstateChatMessageHistory:
        return MemstateChatMessageHistory(
            api_key=API_KEY,
            project_id=PROJECT_ID,
            session_id=session_id,
            base_url=BASE_URL,
        )

    def test_keypath_format(self):
        history = self._make_history("user-123")
        # Hyphens in both project_id and session_id are sanitized to underscores
        assert history._keypath == "project.test_project.chat_history.user_123"

    @responses_lib.activate
    def test_messages_returns_empty_list_when_no_history(self):
        # browse returns no memories → empty list
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/browse",
            json={"memories": [], "total_found": 0},
            status=200,
        )
        history = self._make_history()
        assert history.messages == []

    @responses_lib.activate
    def test_messages_returns_empty_on_404(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/browse",
            json={"error": "Not found"},
            status=404,
        )
        history = self._make_history()
        assert history.messages == []

    @responses_lib.activate
    def test_add_message_persists_to_memstate(self):
        # First call: browse to load existing messages (empty)
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/browse",
            json={"memories": [], "total_found": 0},
            status=200,
        )
        # Second call: save the new message
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/remember",
            json={
                "memory_id": "mem_chat_1",
                "action": "created",
                "message": "Stored",
                "version": 1,
            },
            status=200,
        )
        from langchain_core.messages import HumanMessage

        history = self._make_history()
        history.add_message(HumanMessage(content="Hello, world!"))

        assert len(responses_lib.calls) == 2
        # Verify the remember call contains the serialized message
        remember_body = json.loads(responses_lib.calls[1].request.body)
        assert remember_body["keypath"] == "project.test_project.chat_history.test_session"
        assert remember_body["category"] == "context"
        assert remember_body["source"] == "langchain"
        messages_data = json.loads(remember_body["content"])
        assert len(messages_data) == 1
        assert messages_data[0]["data"]["content"] == "Hello, world!"

    @responses_lib.activate
    def test_messages_deserializes_correctly(self):
        from langchain_core.messages import AIMessage, HumanMessage, messages_to_dict

        stored_messages = [
            HumanMessage(content="What is 2+2?"),
            AIMessage(content="4"),
        ]
        serialized = json.dumps(messages_to_dict(stored_messages))
        keypath = "project.test_project.chat_history.test_session"
        memory_id = "mem_abc123"

        # Step 1: browse returns the memory with its ID
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/browse",
            json={
                "memories": [{
                    "id": memory_id,
                    "summary": serialized[:80] + "...",  # truncated summary
                    "keypath": keypath,
                    "category": "context",
                    "project_id": PROJECT_ID,
                    "version": 1,
                    "is_latest": True,
                    "is_deleted": False,
                }],
                "total_found": 1,
            },
            status=200,
        )
        # Step 2: get_memory returns the full content
        responses_lib.add(
            responses_lib.GET,
            f"{BASE_URL}/api/v1/memories/{memory_id}",
            json={
                "id": memory_id,
                "content": serialized,  # full content
                "summary": serialized[:80] + "...",
                "keypath": keypath,
                "category": "context",
                "project_id": PROJECT_ID,
                "version": 1,
                "is_latest": True,
                "is_deleted": False,
            },
            status=200,
        )
        history = self._make_history()
        messages = history.messages
        assert len(messages) == 2
        assert isinstance(messages[0], HumanMessage)
        assert messages[0].content == "What is 2+2?"
        assert isinstance(messages[1], AIMessage)
        assert messages[1].content == "4"

    @responses_lib.activate
    def test_clear_deletes_session_history(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/delete",
            json={
                "deleted_keypaths": ["project.test_project.chat_history.test_session"],
                "deleted_count": 1,
                "message": "Deleted",
            },
            status=200,
        )
        history = self._make_history()
        history.clear()
        assert len(responses_lib.calls) == 1
        body = json.loads(responses_lib.calls[0].request.body)
        assert body["keypath"] == "project.test_project.chat_history.test_session"


# ---------------------------------------------------------------------------
# Agent Tools unit tests
# ---------------------------------------------------------------------------


class TestMemstateTools:
    """Tests for the LangChain agent tool implementations."""

    def _make_client(self) -> MemstateClient:
        return MemstateClient(api_key=API_KEY, base_url=BASE_URL)

    def test_create_memstate_tools_returns_all_six(self):
        tools = create_memstate_tools(
            api_key=API_KEY,
            project_id=PROJECT_ID,
        )
        assert len(tools) == 6
        tool_names = {t.name for t in tools}
        assert "memstate_remember" in tool_names
        assert "memstate_set" in tool_names
        assert "memstate_get" in tool_names
        assert "memstate_search" in tool_names
        assert "memstate_history" in tool_names
        assert "memstate_delete" in tool_names

    def test_create_memstate_tools_with_include_filter(self):
        tools = create_memstate_tools(
            api_key=API_KEY,
            project_id=PROJECT_ID,
            include_tools=["memstate_remember", "memstate_search"],
        )
        assert len(tools) == 2
        tool_names = {t.name for t in tools}
        assert "memstate_remember" in tool_names
        assert "memstate_search" in tool_names

    @responses_lib.activate
    def test_remember_tool_calls_ingest(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/ingest",
            json={
                "ingestion_id": "ing_1",
                "status": "complete",
                "memories_created": ["mem_1"],
                "keypaths_created": ["auth.provider"],
                "keypaths_updated": [],
                "revision_number": 1,
                "conflicts": [],
                "processing_time_ms": 100,
                "message": "OK",
            },
            status=200,
        )
        client = self._make_client()
        tool = MemstateRememberTool(client=client, project_id=PROJECT_ID)
        result = tool._run(
            content="## Auth\nUsing OAuth2 with Google.",
            source="agent",
        )
        assert "complete" in result
        assert "memories_created=1" in result

    @responses_lib.activate
    def test_set_tool_calls_remember(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/remember",
            json={
                "memory_id": "mem_set_1",
                "action": "created",
                "message": "Stored",
                "version": 1,
            },
            status=200,
        )
        client = self._make_client()
        tool = MemstateSetTool(client=client, project_id=PROJECT_ID)
        result = tool._run(keypath="config.port", value="8080")
        assert "project.test_project.config.port" in result
        assert "created" in result

    def test_set_tool_rejects_values_over_2000_chars(self):
        client = self._make_client()
        tool = MemstateSetTool(client=client, project_id=PROJECT_ID)
        result = tool._run(keypath="config.x", value="x" * 2001)
        assert "too long" in result

    @responses_lib.activate
    def test_get_tool_returns_tree(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/keypaths",
            json={
                "memories": {
                    "project.test_project.auth.provider": "OAuth2 with Google",
                    "project.test_project.database.type": "PostgreSQL 15",
                },
                "total_count": 2,
                "revision_number": 3,
            },
            status=200,
        )
        client = self._make_client()
        tool = MemstateGetTool(client=client, project_id=PROJECT_ID)
        result = tool._run()
        assert "auth.provider" in result
        assert "OAuth2 with Google" in result
        assert "PostgreSQL 15" in result
        assert "revision=3" in result

    @responses_lib.activate
    def test_get_tool_empty_project(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/keypaths",
            json={"memories": {}, "total_count": 0, "revision_number": 0},
            status=200,
        )
        client = self._make_client()
        tool = MemstateGetTool(client=client, project_id=PROJECT_ID)
        result = tool._run()
        assert "No memories found" in result

    @responses_lib.activate
    def test_search_tool_returns_formatted_results(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={
                "results": [
                    {
                        "id": "mem_1",
                        "keypath": "project.test_project.auth.provider",
                        "summary": "OAuth2 with Google",
                        "score": 0.97,
                    }
                ],
                "total_found": 1,
                "query": "authentication",
            },
            status=200,
        )
        client = self._make_client()
        tool = MemstateSearchTool(client=client, project_id=PROJECT_ID)
        result = tool._run(query="authentication")
        assert "OAuth2 with Google" in result
        assert "0.970" in result
        assert "auth.provider" in result

    @responses_lib.activate
    def test_search_tool_no_results(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={"results": [], "total_found": 0, "query": "xyz"},
            status=200,
        )
        client = self._make_client()
        tool = MemstateSearchTool(client=client, project_id=PROJECT_ID)
        result = tool._run(query="xyz")
        assert "No memories found" in result

    @responses_lib.activate
    def test_history_tool_formats_versions(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/history",
            json={
                "versions": [
                    {
                        "id": "mem_v1",
                        "version": 1,
                        "summary": "Basic auth",
                        "is_latest": False,
                        "created_at": "2025-01-01T00:00:00Z",
                    },
                    {
                        "id": "mem_v2",
                        "version": 2,
                        "summary": "OAuth2 with Google",
                        "is_latest": True,
                        "created_at": "2025-06-01T00:00:00Z",
                    },
                ],
                "total_versions": 2,
            },
            status=200,
        )
        client = self._make_client()
        tool = MemstateHistoryTool(client=client, project_id=PROJECT_ID)
        result = tool._run(keypath="auth.provider")
        assert "v1" in result
        assert "v2" in result
        assert "LATEST" in result
        assert "Basic auth" in result
        assert "OAuth2 with Google" in result

    def test_history_tool_requires_keypath_or_memory_id(self):
        client = self._make_client()
        tool = MemstateHistoryTool(client=client, project_id=PROJECT_ID)
        result = tool._run()
        assert "required" in result.lower()

    @responses_lib.activate
    def test_delete_tool_soft_deletes(self):
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/delete",
            json={
                "deleted_keypaths": ["project.test_project.old.config"],
                "deleted_count": 1,
                "message": "Deleted",
            },
            status=200,
        )
        client = self._make_client()
        tool = MemstateDeleteTool(client=client, project_id=PROJECT_ID)
        result = tool._run(keypath="old.config")
        assert "Deleted 1 memories" in result
        assert "project.test_project.old.config" in result

    @responses_lib.activate
    def test_tool_error_returns_error_string(self):
        """Tools should return error strings, not raise exceptions."""
        responses_lib.add(
            responses_lib.POST,
            f"{BASE_URL}/api/v1/memories/search",
            json={"error": "Internal server error"},
            status=500,
        )
        client = self._make_client()
        tool = MemstateSearchTool(client=client, project_id=PROJECT_ID)
        result = tool._run(query="test")
        assert "Error" in result
