"""Tests for mubit-agno integration with mocked MubitControlClient."""

from __future__ import annotations

import json
from unittest.mock import MagicMock
import pytest

from mubit_agno import MubitMemoryDb, MubitToolkit, MubitAgnoMemory
from mubit_agno import _memory_type_to_intent


# ---------------------------------------------------------------------------
# Intent mapping
# ---------------------------------------------------------------------------

class TestIntentMapping:
    def test_semantic_to_fact(self):
        assert _memory_type_to_intent("semantic") == "fact"
        assert _memory_type_to_intent("fact") == "fact"

    def test_episodic_to_trace(self):
        assert _memory_type_to_intent("episodic") == "trace"
        assert _memory_type_to_intent("trace") == "trace"
        assert _memory_type_to_intent("event") == "trace"

    def test_procedural_to_lesson(self):
        assert _memory_type_to_intent("procedural") == "lesson"
        assert _memory_type_to_intent("lesson") == "lesson"
        assert _memory_type_to_intent("rule") == "lesson"
        assert _memory_type_to_intent("skill") == "lesson"

    def test_default_to_trace(self):
        assert _memory_type_to_intent(None) == "trace"
        assert _memory_type_to_intent("") == "trace"
        assert _memory_type_to_intent("unknown") == "trace"

    def test_case_insensitive(self):
        assert _memory_type_to_intent("SEMANTIC") == "fact"
        assert _memory_type_to_intent("Episodic") == "trace"
        assert _memory_type_to_intent("PROCEDURAL") == "lesson"


# ---------------------------------------------------------------------------
# MubitMemoryDb
# ---------------------------------------------------------------------------

class TestMubitMemoryDb:
    def _make_db(self, mock_client=None):
        mock = mock_client or MagicMock()
        return MubitMemoryDb(
            session_id="test-session",
            user_id="user-1",
            agent_id="test-agent",
            mubit_client=mock,
        ), mock

    def test_create_is_noop(self):
        db, _ = self._make_db()
        db.create()  # Should not raise

    def test_table_exists_returns_true(self):
        db, _ = self._make_db()
        assert db.table_exists() is True

    def test_upsert_memory_calls_remember(self):
        db, mock = self._make_db()
        mock.remember.return_value = {"job_id": "j1"}

        memory = MagicMock()
        memory.id = "mem-1"
        memory.memory = "User prefers Python"
        memory.user_id = "user-1"
        memory.topics = ["python", "preferences"]
        memory.memory_type = "semantic"

        db.upsert_memory(memory)

        mock.remember.assert_called_once()
        call_kwargs = mock.remember.call_args[1]
        assert call_kwargs["session_id"] == "test-session"
        assert call_kwargs["text"] == "User prefers Python"
        assert call_kwargs["intent"] == "fact"
        assert call_kwargs["upsert_key"] == "mem-1"
        assert call_kwargs["metadata"]["topics"] == ["python", "preferences"]
        assert call_kwargs["metadata"]["agno_memory_id"] == "mem-1"

    def test_read_memories_calls_recall(self):
        db, mock = self._make_db()
        mock.recall.return_value = {
            "evidence": [
                {
                    "id": "ev-1",
                    "content": "User prefers Python",
                    "metadata": {
                        "agno_memory_id": "mem-1",
                        "topics": ["python"],
                    },
                }
            ]
        }

        rows = db.read_memories(user_id="user-1")

        mock.recall.assert_called_once()
        assert len(rows) == 1
        assert rows[0].memory == "User prefers Python"
        assert rows[0].id == "mem-1"

    def test_delete_memory_upserts_marker(self):
        db, mock = self._make_db()
        mock.remember.return_value = {"job_id": "j1"}

        db.delete_memory("mem-1")

        call_kwargs = mock.remember.call_args[1]
        assert call_kwargs["text"] == "[deleted]"
        assert call_kwargs["upsert_key"] == "mem-1"
        assert call_kwargs["metadata"]["deleted"] is True

    def test_clear_calls_delete_run(self):
        db, mock = self._make_db()
        db.clear()
        mock.delete_run.assert_called_once_with(session_id="test-session")

    def test_drop_table_calls_delete_run(self):
        db, mock = self._make_db()
        db.drop_table()
        mock.delete_run.assert_called_once_with(session_id="test-session")


# ---------------------------------------------------------------------------
# MubitToolkit
# ---------------------------------------------------------------------------

class TestMubitToolkit:
    def _make_toolkit(self, mock_client=None):
        mock = mock_client or MagicMock()
        tk = MubitToolkit(
            session_id="test-session",
            user_id="user-1",
            agent_id="test-agent",
            mubit_client=mock,
        )
        return tk, mock

    def test_remember_tool(self):
        tk, mock = self._make_toolkit()
        mock.remember.return_value = {"job_id": "j1"}

        result = tk.mubit_remember("Important fact", intent="fact")

        mock.remember.assert_called_once()
        parsed = json.loads(result)
        assert parsed["job_id"] == "j1"

    def test_recall_tool(self):
        tk, mock = self._make_toolkit()
        mock.recall.return_value = {"evidence": [{"content": "test"}]}

        result = tk.mubit_recall("find test memories", limit=3)

        mock.recall.assert_called_once()
        call_kwargs = mock.recall.call_args[1]
        assert call_kwargs["query"] == "find test memories"
        assert call_kwargs["limit"] == 3

    def test_reflect_tool(self):
        tk, mock = self._make_toolkit()
        mock.reflect.return_value = {"lessons": []}

        result = tk.mubit_reflect()

        mock.reflect.assert_called_once()
        parsed = json.loads(result)
        assert "lessons" in parsed

    def test_get_context_tool(self):
        tk, mock = self._make_toolkit()
        mock.get_context.return_value = {"context_block": "relevant context"}

        result = tk.mubit_get_context("what happened?")

        mock.get_context.assert_called_once()

    def test_checkpoint_tool(self):
        tk, mock = self._make_toolkit()
        mock.checkpoint.return_value = {"ok": True}

        result = tk.mubit_checkpoint("phase-1", "Research complete")

        mock.checkpoint.assert_called_once()
        call_kwargs = mock.checkpoint.call_args[1]
        assert call_kwargs["label"] == "phase-1"
        assert call_kwargs["snapshot"] == "Research complete"

    def test_diagnose_tool(self):
        tk, mock = self._make_toolkit()
        mock.diagnose.return_value = {"lessons": []}

        result = tk.mubit_diagnose("connection timeout")

        mock.diagnose.assert_called_once()
        call_kwargs = mock.diagnose.call_args[1]
        assert call_kwargs["error_text"] == "connection timeout"

    def test_memory_health_tool(self):
        tk, mock = self._make_toolkit()
        mock.memory_health.return_value = {"status": "healthy"}

        result = tk.mubit_memory_health()

        mock.memory_health.assert_called_once()


# ---------------------------------------------------------------------------
# MubitAgnoMemory
# ---------------------------------------------------------------------------

class TestMubitAgnoMemory:
    def _make_memory(self, mock_client=None):
        mock = mock_client or MagicMock()
        mem = MubitAgnoMemory(
            session_id="test-session",
            user_id="user-1",
            agent_id="test-agent",
            mubit_client=mock,
        )
        return mem, mock

    def test_as_memory_db_returns_instance(self):
        mem, _ = self._make_memory()
        db = mem.as_memory_db()
        assert isinstance(db, MubitMemoryDb)

    def test_as_toolkit_returns_instance(self):
        mem, _ = self._make_memory()
        tk = mem.as_toolkit()
        assert isinstance(tk, MubitToolkit)

    def test_get_context(self):
        mem, mock = self._make_memory()
        mock.get_context.return_value = {"context_block": "ctx"}

        result = mem.get_context("test query")
        assert result == "ctx"

    def test_checkpoint(self):
        mem, mock = self._make_memory()
        mock.checkpoint.return_value = {"ok": True}

        mem.checkpoint("label", "snapshot")
        mock.checkpoint.assert_called_once()

    def test_record_outcome(self):
        mem, mock = self._make_memory()
        mock.record_outcome.return_value = {"ok": True}

        mem.record_outcome("ref-1", "success", rationale="All good")
        mock.record_outcome.assert_called_once()
        call_kwargs = mock.record_outcome.call_args[1]
        assert call_kwargs["reference_id"] == "ref-1"
        assert call_kwargs["outcome"] == "success"

    def test_register_agent(self):
        mem, mock = self._make_memory()
        mock.register_agent.return_value = {"ok": True}

        mem.register_agent("planner", role="planner",
                          read_scopes=["fact", "lesson"],
                          write_scopes=["trace"])
        mock.register_agent.assert_called_once()

    def test_handoff(self):
        mem, mock = self._make_memory()
        mock.handoff.return_value = {"handoff_id": "h1"}

        mem.handoff("agent-a", "agent-b", "Please review this")
        mock.handoff.assert_called_once()

    def test_reflect(self):
        mem, mock = self._make_memory()
        mock.reflect.return_value = {"lessons": []}

        result = mem.reflect()
        mock.reflect.assert_called_once()
        assert "lessons" in result

    def test_surface_strategies(self):
        mem, mock = self._make_memory()
        mock.surface_strategies.return_value = {"strategies": []}

        mem.surface_strategies(max_strategies=3)
        mock.surface_strategies.assert_called_once()

    def test_archive_and_dereference(self):
        mem, mock = self._make_memory()
        mock.archive.return_value = {"reference_id": "ref-1"}
        mock.dereference.return_value = {"content": "archived text"}

        mem.archive("some code", "code_snippet", labels=["python"])
        mock.archive.assert_called_once()

        mem.dereference("ref-1")
        mock.dereference.assert_called_once()

    def test_diagnose(self):
        mem, mock = self._make_memory()
        mock.diagnose.return_value = {"lessons": []}

        mem.diagnose("timeout error", error_type="network")
        call_kwargs = mock.diagnose.call_args[1]
        assert call_kwargs["error_text"] == "timeout error"
        assert call_kwargs["error_type"] == "network"

    def test_memory_health(self):
        mem, mock = self._make_memory()
        mock.memory_health.return_value = {"status": "ok"}

        result = mem.memory_health()
        assert result["status"] == "ok"
