"""
Integration tests for memstate-langchain.

These tests run against the live Memstate AI API and require a valid API key.
They test the full end-to-end workflow including memory persistence, semantic
search, version history, and agent tool execution.

Prerequisites:
    pip install "memstate-langchain[dev]"
    export MEMSTATE_API_KEY=mst_your_key_here
    export OPENAI_API_KEY=sk_your_key_here  # optional: for agent tests

Run with:
    MEMSTATE_API_KEY=mst_... pytest tests/test_integration.py -v
    MEMSTATE_API_KEY=mst_... pytest tests/test_integration.py -v -k "not agent"  # skip agent tests
"""

import os
import time
import uuid
import pytest

# Skip all integration tests if no API key is set
pytestmark = pytest.mark.skipif(
    not os.environ.get("MEMSTATE_API_KEY"),
    reason="MEMSTATE_API_KEY environment variable not set",
)

API_KEY = os.environ.get("MEMSTATE_API_KEY", "")
BASE_URL = os.environ.get("MEMSTATE_BASE_URL", "https://api.memstate.ai")

# Use a unique project ID per test run to avoid cross-test contamination
TEST_RUN_ID = uuid.uuid4().hex[:8]
PROJECT_ID = f"langchain-test-{TEST_RUN_ID}"


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


@pytest.fixture(scope="module")
def client():
    """Shared MemstateClient for all integration tests."""
    from memstate_langchain import MemstateClient
    return MemstateClient(api_key=API_KEY, base_url=BASE_URL)


@pytest.fixture(scope="module", autouse=True)
def cleanup_project(client):
    """Clean up the test project after all tests in this module complete."""
    yield
    try:
        client.delete_memory(
            keypath=f"project.{PROJECT_ID}",
            project_id=PROJECT_ID,
            recursive=True,
        )
    except Exception:
        pass  # Best-effort cleanup


# ---------------------------------------------------------------------------
# MemstateClient integration tests
# ---------------------------------------------------------------------------


class TestMemstateClientIntegration:
    """Live API tests for the MemstateClient."""

    def test_get_status(self, client):
        """Verify the API is reachable and returns valid status."""
        result = client.get_status()
        assert result["status"] == "ok"
        assert "memory_count" in result
        assert "features" in result

    def test_remember_and_retrieve(self, client):
        """Store a memory and retrieve it by keypath."""
        keypath = f"project.{PROJECT_ID}.database.type"
        result = client.remember(
            content="PostgreSQL 15 on Railway",
            keypath=keypath,
            project_id=PROJECT_ID,
            category="fact",
            source="langchain-test",
        )
        assert "memory_id" in result
        assert result["action"] in ("created", "updated", "versioned")
        memory_id = result["memory_id"]

        # Retrieve by ID
        memory = client.get_memory(memory_id)
        assert memory["content"] == "PostgreSQL 15 on Railway"
        assert memory["keypath"] == keypath

    def test_remember_creates_version_on_update(self, client):
        """Storing a new value at the same keypath creates a new version."""
        keypath = f"project.{PROJECT_ID}.version.current"

        r1 = client.remember(
            content="1.0.0",
            keypath=keypath,
            project_id=PROJECT_ID,
            category="fact",
        )
        assert r1["version"] == 1

        r2 = client.remember(
            content="1.1.0",
            keypath=keypath,
            project_id=PROJECT_ID,
            category="fact",
        )
        assert r2["version"] == 2

    def test_search_finds_stored_memory(self, client):
        """Semantic search should find memories by meaning."""
        # Store a distinctive memory
        keypath = f"project.{PROJECT_ID}.auth.provider"
        client.remember(
            content="Using JWT tokens with RS256 signing for authentication",
            keypath=keypath,
            project_id=PROJECT_ID,
            category="decision",
        )

        # Wait briefly for indexing
        time.sleep(1)

        # Search by meaning
        results = client.search(
            query="authentication token signing",
            project_id=PROJECT_ID,
            limit=5,
        )
        assert results["total_found"] >= 1
        keypaths = [r["keypath"] for r in results["results"]]
        assert keypath in keypaths

    def test_browse_returns_memories_under_prefix(self, client):
        """Browse should return all memories under a keypath prefix."""
        # Store multiple memories
        for key in ["host", "port", "name"]:
            client.remember(
                content=f"db_{key}_value",
                keypath=f"project.{PROJECT_ID}.db.{key}",
                project_id=PROJECT_ID,
            )

        result = client.browse(
            keypath_prefix=f"project.{PROJECT_ID}.db",
            project_id=PROJECT_ID,
        )
        assert result["total_found"] >= 3
        keypaths = [m["keypath"] for m in result["memories"]]
        assert f"project.{PROJECT_ID}.db.host" in keypaths
        assert f"project.{PROJECT_ID}.db.port" in keypaths

    def test_history_shows_versions(self, client):
        """Version history should show all versions of a memory."""
        keypath = f"project.{PROJECT_ID}.status.deployment"
        client.remember(content="staging", keypath=keypath, project_id=PROJECT_ID)
        client.remember(content="production", keypath=keypath, project_id=PROJECT_ID)

        history = client.history(keypath=keypath, project_id=PROJECT_ID)
        assert history["total_versions"] >= 2
        latest = next(v for v in history["versions"] if v["is_latest"])
        assert "production" in latest.get("summary", latest.get("content", ""))

    def test_delete_memory_creates_tombstone(self, client):
        """Soft-delete should create a tombstone version."""
        keypath = f"project.{PROJECT_ID}.temp.setting"
        client.remember(content="temporary value", keypath=keypath, project_id=PROJECT_ID)

        result = client.delete_memory(keypath=keypath, project_id=PROJECT_ID)
        assert result["deleted_count"] >= 1

        # History should still show the deleted memory
        history = client.history(keypath=keypath, project_id=PROJECT_ID)
        assert history["total_versions"] >= 1

    def test_get_keypaths_returns_tree(self, client):
        """get_keypaths should return the project memory tree."""
        # Ensure at least one memory exists
        client.remember(
            content="tree_test_value",
            keypath=f"project.{PROJECT_ID}.tree.test",
            project_id=PROJECT_ID,
        )

        result = client.get_keypaths(
            project_id=PROJECT_ID,
            keypath=f"project.{PROJECT_ID}",
            recursive=True,
        )
        assert "memories" in result
        assert isinstance(result["memories"], dict)


# ---------------------------------------------------------------------------
# MemstateSemanticMemory integration tests
# ---------------------------------------------------------------------------


class TestMemstateMemoryIntegration:
    """Live API tests for MemstateSemanticMemory (modern LCEL pattern)."""

    def _make_memory(self, session_id: str = "default") -> "MemstateMemory":
        from memstate_langchain import MemstateMemory
        return MemstateMemory(
            api_key=API_KEY,
            project_id=PROJECT_ID,
            session_id=session_id,
            base_url=BASE_URL,
        )

    def test_retrieve_returns_dict_with_memory_key(self):
        """retrieve() should return the input dict with memory_key injected."""
        memory = self._make_memory(session_id="test-retrieve")
        result = memory.retrieve({"input": "What is the database?"})
        assert isinstance(result, dict)
        assert "relevant_memories" in result
        assert "input" in result

    def test_save_persists_conversation_turn(self):
        """save() should ingest a conversation turn without raising."""
        from langchain_core.messages import AIMessage

        memory = self._make_memory(session_id="test-save")
        # save() should not raise
        memory.save(
            inputs={"input": "My name is Alice and I prefer Python."},
            output=AIMessage(content="Nice to meet you, Alice! Python is a great choice."),
        )
        # No assertion on content — async ingest may not be complete immediately

    def test_get_memories_with_query(self):
        """get_memories with a query should use semantic search."""
        memory = self._make_memory()
        # Store something first
        memory.client.remember(
            content="FastAPI framework for REST APIs",
            keypath=f"project.{PROJECT_ID}.stack.framework",
            project_id=PROJECT_ID,
        )
        time.sleep(1)

        results = memory.get_memories(query="web framework", limit=5)
        assert isinstance(results, list)

    def test_get_memories_without_query(self):
        """get_memories without a query should browse all memories."""
        memory = self._make_memory()
        results = memory.get_memories(limit=10)
        assert isinstance(results, list)

    def test_wrap_returns_runnable(self):
        """wrap() should return a Runnable that can be invoked."""
        from langchain_core.runnables import Runnable

        memory = self._make_memory(session_id="test-wrap")
        # Create a simple passthrough chain
        from langchain_core.runnables import RunnableLambda
        chain = RunnableLambda(lambda x: {"content": f"Echo: {x.get('input', '')}"})
        wrapped = memory.wrap(chain)
        assert isinstance(wrapped, Runnable)
        result = wrapped.invoke({"input": "hello"})
        assert result is not None


# ---------------------------------------------------------------------------
# MemstateChatMessageHistory integration tests
# ---------------------------------------------------------------------------


class TestMemstateChatMessageHistoryIntegration:
    """Live API tests for the LangChain BaseChatMessageHistory implementation."""

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

    def test_add_and_retrieve_messages(self):
        """Messages added to history should be retrievable."""
        from langchain_core.messages import AIMessage, HumanMessage

        session_id = f"test-session-{uuid.uuid4().hex[:6]}"
        history = self._make_history(session_id)

        # Start with empty history
        assert history.messages == []

        # Add messages
        history.add_message(HumanMessage(content="What is the capital of France?"))
        history.add_message(AIMessage(content="The capital of France is Paris."))

        # Retrieve — should have both messages
        messages = history.messages
        assert len(messages) == 2
        assert isinstance(messages[0], HumanMessage)
        assert messages[0].content == "What is the capital of France?"
        assert isinstance(messages[1], AIMessage)
        assert messages[1].content == "The capital of France is Paris."

    def test_add_messages_batch(self):
        """add_messages should persist multiple messages in one write."""
        from langchain_core.messages import AIMessage, HumanMessage

        session_id = f"test-batch-{uuid.uuid4().hex[:6]}"
        history = self._make_history(session_id)

        history.add_messages([
            HumanMessage(content="Hello"),
            AIMessage(content="Hi there!"),
            HumanMessage(content="How are you?"),
        ])

        messages = history.messages
        assert len(messages) == 3

    def test_clear_removes_history(self):
        """clear() should delete the session history."""
        from langchain_core.messages import HumanMessage

        session_id = f"test-clear-{uuid.uuid4().hex[:6]}"
        history = self._make_history(session_id)

        history.add_message(HumanMessage(content="Message to be cleared"))
        assert len(history.messages) == 1

        history.clear()
        # After clear, messages should be empty (404 → empty list)
        assert history.messages == []

    def test_multiple_sessions_are_isolated(self):
        """Different session IDs should have independent message histories."""
        from langchain_core.messages import HumanMessage

        session_a = f"session-a-{uuid.uuid4().hex[:6]}"
        session_b = f"session-b-{uuid.uuid4().hex[:6]}"

        history_a = self._make_history(session_a)
        history_b = self._make_history(session_b)

        history_a.add_message(HumanMessage(content="Message for session A"))
        history_b.add_message(HumanMessage(content="Message for session B"))

        messages_a = history_a.messages
        messages_b = history_b.messages

        assert len(messages_a) == 1
        assert len(messages_b) == 1
        assert messages_a[0].content == "Message for session A"
        assert messages_b[0].content == "Message for session B"


# ---------------------------------------------------------------------------
# Agent Tools integration tests
# ---------------------------------------------------------------------------


class TestAgentToolsIntegration:
    """Live API tests for the LangChain agent tools."""

    @pytest.fixture
    def tools(self):
        from memstate_langchain import create_memstate_tools
        return create_memstate_tools(
            api_key=API_KEY,
            project_id=PROJECT_ID,
            base_url=BASE_URL,
        )

    def _get_tool(self, tools, name):
        return next(t for t in tools if t.name == name)

    def test_remember_tool_saves_content(self, tools):
        """memstate_remember should ingest content successfully."""
        tool = self._get_tool(tools, "memstate_remember")
        result = tool._run(
            content="## Architecture Decision\n\nWe chose FastAPI for the backend because of its async support and automatic OpenAPI docs.",
            source="agent",
            context="backend framework selection",
        )
        assert "Error" not in result
        assert any(word in result for word in ["complete", "processing", "job_id"])

    def test_set_tool_stores_value(self, tools):
        """memstate_set should store a key-value pair."""
        tool = self._get_tool(tools, "memstate_set")
        result = tool._run(
            keypath="config.server.port",
            value="8080",
            category="fact",
        )
        assert "Error" not in result
        assert "config.server.port" in result or "project." in result

    def test_get_tool_retrieves_tree(self, tools):
        """memstate_get should return the project memory tree."""
        # First store something
        set_tool = self._get_tool(tools, "memstate_set")
        set_tool._run(keypath="test.get.key", value="test_value")

        # Then get the tree
        get_tool = self._get_tool(tools, "memstate_get")
        result = get_tool._run()
        # Should return either the tree or "No memories found"
        assert "Error" not in result

    def test_search_tool_finds_memories(self, tools):
        """memstate_search should return relevant results."""
        # Store something distinctive
        set_tool = self._get_tool(tools, "memstate_set")
        set_tool._run(
            keypath="stack.language",
            value="TypeScript for frontend, Python for backend",
        )
        time.sleep(1)

        search_tool = self._get_tool(tools, "memstate_search")
        result = search_tool._run(query="programming language stack")
        # Should return results or "No memories found"
        assert "Error" not in result

    def test_history_tool_shows_versions(self, tools):
        """memstate_history should show version history."""
        # Create two versions
        set_tool = self._get_tool(tools, "memstate_set")
        set_tool._run(keypath="deploy.env", value="staging")
        set_tool._run(keypath="deploy.env", value="production")

        history_tool = self._get_tool(tools, "memstate_history")
        result = history_tool._run(keypath="deploy.env")
        assert "Error" not in result

    def test_delete_tool_removes_memory(self, tools):
        """memstate_delete should soft-delete a keypath."""
        # Create a memory to delete
        set_tool = self._get_tool(tools, "memstate_set")
        set_tool._run(keypath="temp.delete_me", value="to be deleted")

        delete_tool = self._get_tool(tools, "memstate_delete")
        result = delete_tool._run(keypath="temp.delete_me")
        assert "Error" not in result
        assert "Deleted" in result
