"""
Tests for the Novyx MCP server tool functions.

Mocks the Novyx SDK client so we can test each tool function
calls the right SDK method with the right arguments.
"""

import json
from unittest.mock import MagicMock, patch

import pytest

from novyx import Memory, SearchResult


# =========================================================================
# Helpers
# =========================================================================


def _mock_client(**overrides):
    """Create a mock Novyx client with sensible defaults."""
    client = MagicMock()
    client.remember.return_value = {"uuid": "test-uuid-123", "message": "Memory stored"}
    client.recall.return_value = SearchResult(
        query="test",
        total_results=1,
        memories=[
            Memory(
                uuid="mem-1",
                observation="Test observation",
                tags=["test"],
                importance=5,
                score=0.95,
                created_at="2026-02-24T00:00:00Z",
            )
        ],
    )
    client.forget.return_value = True
    client.memories.return_value = [
        {"uuid": "mem-1", "observation": "Test observation", "tags": ["test"]}
    ]
    client.memory.return_value = {
        "uuid": "mem-1",
        "observation": "Test observation",
        "tags": ["test"],
    }
    client.stats.return_value = {"total_memories": 42, "avg_importance": 5.5}
    client.draft_memory.return_value = {
        "draft_id": "drf_test123",
        "branch_id": "branch-1",
        "status": "draft",
        "review_summary": {"similar_count": 1, "similar_memories": []},
    }
    client.memory_drafts.return_value = {
        "total_count": 1,
        "drafts": [{"draft_id": "drf_test123", "status": "draft"}],
    }
    client.memory_draft.return_value = {
        "draft_id": "drf_test123",
        "status": "draft",
    }
    client.draft_diff.return_value = {
        "draft_id": "drf_test123",
        "recommendation": "merge_or_supersede",
        "changed_fields": [{"field": "observation", "changed": True}],
    }
    client.merge_draft.return_value = {
        "draft_id": "drf_test123",
        "status": "merged",
        "memory_id": "mem-merged",
    }
    client.reject_draft.return_value = {
        "draft_id": "drf_test123",
        "status": "rejected",
    }
    client.memory_branch.return_value = {
        "branch_id": "branch-1",
        "total_drafts": 2,
        "open_drafts": 2,
    }
    client.merge_branch.return_value = {
        "branch_id": "branch-1",
        "merged_count": 2,
        "merged_memory_ids": ["mem-1", "mem-2"],
    }
    client.reject_branch.return_value = {
        "branch_id": "branch-1",
        "rejected_drafts": 2,
    }
    client.rollback.return_value = {
        "success": True,
        "rolled_back_to": "2026-02-24T00:00:00Z",
        "artifacts_restored": 3,
    }
    client.audit.return_value = [
        {"timestamp": "2026-02-24T00:00:00Z", "operation": "CREATE", "artifact_id": "mem-1"}
    ]
    client.link.return_value = {
        "source_id": "mem-1",
        "target_id": "mem-2",
        "relation": "related",
    }
    client.triple.return_value = {
        "triple_id": "triple-1",
        "subject": "blake",
        "predicate": "likes",
        "object": "python",
    }
    client.triples.return_value = {
        "triples": [
            {"subject": {"name": "blake"}, "predicate": "likes", "object": {"name": "python"}}
        ],
        "total": 1,
    }
    client.usage.return_value = {
        "tier": "free",
        "api_calls": {"current": 10, "limit": 5000},
    }

    # Context Spaces
    client.create_space.return_value = {
        "space_id": "cs_test123",
        "name": "Test Space",
        "owner_tenant_id": "test-tenant",
        "allowed_agent_ids": [],
        "tags": [],
        "memory_count": 0,
    }
    client.list_spaces.return_value = {
        "spaces": [{"space_id": "cs_test123", "name": "Test Space"}],
        "total_count": 1,
    }
    client.get_space.return_value = {
        "space_id": "cs_test123",
        "name": "Test Space",
        "memory_count": 5,
    }
    client.update_space.return_value = {
        "space_id": "cs_test123",
        "name": "Updated Space",
    }
    client.delete_space.return_value = True
    client.space_memories.return_value = {
        "total_count": 1,
        "memories": [{"uuid": "mem-1", "observation": "Test", "tags": ["test"]}],
    }

    # Replay
    client.replay_timeline.return_value = {
        "entries": [{"operation": "create", "timestamp": "2026-02-27T00:00:00Z"}],
        "total_count": 1,
    }
    client.replay_snapshot.return_value = {
        "snapshot_at": "2026-02-27T00:00:00Z",
        "total_memories": 10,
        "memories": [],
    }
    client.replay_lifecycle.return_value = {
        "memory_id": "mem-1",
        "current_state": "active",
        "events": [{"type": "create", "timestamp": "2026-02-27T00:00:00Z"}],
    }
    client.replay_diff.return_value = {
        "from_timestamp": "2026-02-26T00:00:00Z",
        "to_timestamp": "2026-02-27T00:00:00Z",
        "added": [{"uuid": "mem-2", "observation": "New memory"}],
        "removed": [],
        "modified": [],
        "summary": {"added": 1, "removed": 0, "modified": 0},
    }

    # Cortex
    client.cortex_status.return_value = {
        "enabled": True,
        "last_run": "2026-02-27T00:00:00Z",
        "consolidations": 3,
    }
    client.cortex_run.return_value = {
        "consolidated": 2,
        "reinforced": 5,
        "decayed": 1,
    }
    client.cortex_insights.return_value = {
        "insights": [{"type": "pattern", "description": "API errors correlate with deploys"}],
        "total": 1,
    }

    # share_context for share_space
    client.share_context.return_value = {
        "token": "ns_abc123",
        "tag": "project:test",
        "permission": "read",
    }

    for key, value in overrides.items():
        setattr(client, key, value)

    return client


# =========================================================================
# Tool tests
# =========================================================================


class TestRemember:
    def setup_method(self):
        """Reset git repo cache before each test."""
        import novyx_mcp.server as srv
        srv._git_repo_name = None

    @patch("novyx_mcp.server._detect_git_repo", return_value="")
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend, _mock_git):
        from novyx_mcp.server import remember

        mock_get_backend.return_value = _mock_client()
        result = json.loads(remember("User likes dark mode"))
        assert result["uuid"] == "test-uuid-123"
        mock_get_backend.return_value.remember.assert_called_once_with(
            observation="User likes dark mode",
            importance=5,
        )

    @patch("novyx_mcp.server._detect_git_repo", return_value="")
    @patch("novyx_mcp.server._get_backend")
    def test_with_all_params(self, mock_get_backend, _mock_git):
        from novyx_mcp.server import remember

        mock_get_backend.return_value = _mock_client()
        result = json.loads(
            remember(
                "Test memory",
                tags=["ui", "prefs"],
                importance=8,
                context="testing",
                ttl_seconds=3600,
            )
        )
        assert result["uuid"] == "test-uuid-123"
        mock_get_backend.return_value.remember.assert_called_once_with(
            observation="Test memory",
            tags=["ui", "prefs"],
            importance=8,
            context="testing",
            ttl_seconds=3600,
        )

    @patch("novyx_mcp.server._detect_git_repo", return_value="")
    @patch("novyx_mcp.server._get_backend")
    def test_error_handling(self, mock_get_backend, _mock_git):
        from novyx_mcp.server import remember

        mock_get_backend.side_effect = ValueError("NOVYX_API_KEY environment variable is required")
        result = json.loads(remember("test"))
        assert "error" in result
        assert "NOVYX_API_KEY" in result["error"]


class TestRememberAutoTag:
    """Tests for auto-tagging memories with git repo name."""

    def setup_method(self):
        """Reset git repo cache before each test."""
        import novyx_mcp.server as srv
        srv._git_repo_name = None

    @patch("novyx_mcp.server._detect_git_repo", return_value="novyx-core")
    @patch("novyx_mcp.server._get_backend")
    def test_adds_repo_tag_no_existing_tags(self, mock_get_backend, _mock_git):
        from novyx_mcp.server import remember

        mock_get_backend.return_value = _mock_client()
        remember("Test memory")
        mock_get_backend.return_value.remember.assert_called_once_with(
            observation="Test memory",
            importance=5,
            tags=["repo:novyx-core"],
        )

    @patch("novyx_mcp.server._detect_git_repo", return_value="novyx-core")
    @patch("novyx_mcp.server._get_backend")
    def test_adds_repo_tag_with_existing_tags(self, mock_get_backend, _mock_git):
        from novyx_mcp.server import remember

        mock_get_backend.return_value = _mock_client()
        remember("Test memory", tags=["ui", "prefs"])
        mock_get_backend.return_value.remember.assert_called_once_with(
            observation="Test memory",
            importance=5,
            tags=["ui", "prefs", "repo:novyx-core"],
        )

    @patch("novyx_mcp.server._detect_git_repo", return_value="novyx-core")
    @patch("novyx_mcp.server._get_backend")
    def test_no_duplicate_repo_tag(self, mock_get_backend, _mock_git):
        from novyx_mcp.server import remember

        mock_get_backend.return_value = _mock_client()
        remember("Test memory", tags=["repo:novyx-core", "other"])
        mock_get_backend.return_value.remember.assert_called_once_with(
            observation="Test memory",
            importance=5,
            tags=["repo:novyx-core", "other"],
        )

    @patch("novyx_mcp.server._detect_git_repo", return_value="")
    @patch("novyx_mcp.server._get_backend")
    def test_no_tag_when_not_in_repo(self, mock_get_backend, _mock_git):
        from novyx_mcp.server import remember

        mock_get_backend.return_value = _mock_client()
        remember("Test memory")
        mock_get_backend.return_value.remember.assert_called_once_with(
            observation="Test memory",
            importance=5,
        )

    def test_detect_git_repo_caches_result(self):
        import novyx_mcp.server as srv

        with patch("novyx_mcp.server.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="/home/user/my-repo\n"
            )
            result1 = srv._detect_git_repo()
            assert result1 == "my-repo"
            # Second call should use cache, not call subprocess again
            result2 = srv._detect_git_repo()
            assert result2 == "my-repo"
            mock_run.assert_called_once()

    def test_detect_git_repo_handles_failure(self):
        import novyx_mcp.server as srv

        with patch("novyx_mcp.server.subprocess.run", side_effect=FileNotFoundError("git not found")):
            result = srv._detect_git_repo()
            assert result == ""

    def test_detect_git_repo_handles_non_repo(self):
        import novyx_mcp.server as srv

        with patch("novyx_mcp.server.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=128, stdout="")
            result = srv._detect_git_repo()
            assert result == ""


class TestRecall:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import recall

        mock_get_backend.return_value = _mock_client()
        result = json.loads(recall("user preferences"))
        assert result["total_results"] == 1
        assert result["memories"][0]["uuid"] == "mem-1"
        assert result["memories"][0]["score"] == 0.95
        mock_get_backend.return_value.recall.assert_called_once_with(
            query="user preferences",
            limit=5,
            min_score=0.0,
        )

    @patch("novyx_mcp.server._get_backend")
    def test_with_tags(self, mock_get_backend):
        from novyx_mcp.server import recall

        mock_get_backend.return_value = _mock_client()
        recall("test query", limit=10, tags=["ui"], min_score=0.5)
        mock_get_backend.return_value.recall.assert_called_once_with(
            query="test query",
            limit=10,
            min_score=0.5,
            tags=["ui"],
        )

    @patch("novyx_mcp.server._get_backend")
    def test_explain_false_no_breakdown(self, mock_get_backend):
        """Default recall (explain=False) should not include scoring_breakdown."""
        from novyx_mcp.server import recall

        mock_get_backend.return_value = _mock_client()
        result = json.loads(recall("test query"))
        assert "scoring_breakdown" not in result["memories"][0]

    @patch("novyx_mcp.server._get_backend")
    def test_explain_true_cloud_backend(self, mock_get_backend):
        """explain=True with cloud backend should include scoring_breakdown."""
        from novyx_mcp.server import recall

        mock_get_backend.return_value = _mock_client()
        result = json.loads(recall("test query", explain=True))
        mem = result["memories"][0]
        assert "scoring_breakdown" in mem
        bd = mem["scoring_breakdown"]
        assert "cosine_score" in bd
        assert "importance_boost" in bd
        assert "confidence_boost" in bd
        assert "final_score" in bd
        assert "explanation" in bd
        assert "semantic similarity" in bd["explanation"]

    @patch("novyx_mcp.server._get_backend")
    def test_explain_true_local_backend(self, mock_get_backend):
        """explain=True with local backend (dict result) should include scoring_breakdown."""
        from novyx_mcp.server import recall

        backend = MagicMock()
        backend.recall.return_value = {
            "query": "test",
            "total_results": 1,
            "memories": [{
                "uuid": "mem-1",
                "observation": "Test observation",
                "tags": ["test"],
                "importance": 9,
                "confidence": 1.0,
                "score": 0.85,
                "similarity": 0.82,
                "created_at": "2026-02-24T00:00:00Z",
            }],
            "mode": "local",
        }
        mock_get_backend.return_value = backend
        result = json.loads(recall("test query", explain=True))
        mem = result["memories"][0]
        bd = mem["scoring_breakdown"]
        assert bd["cosine_score"] == 0.82
        assert bd["importance_boost"] == round(0.2 * (9 / 10.0), 4)
        assert bd["confidence_boost"] == round(0.1 * 1.0, 4)
        assert bd["final_score"] == 0.85
        assert "high importance (9/10)" in bd["explanation"]

    @patch("novyx_mcp.server._get_backend")
    def test_explain_true_low_confidence(self, mock_get_backend):
        """explain=True should mention low confidence when confidence < 0.5."""
        from novyx_mcp.server import recall

        backend = MagicMock()
        backend.recall.return_value = {
            "query": "test",
            "total_results": 1,
            "memories": [{
                "uuid": "mem-2",
                "observation": "Low confidence memory",
                "tags": [],
                "importance": 5,
                "confidence": 0.3,
                "score": 0.5,
                "similarity": 0.6,
                "created_at": "2026-02-24T00:00:00Z",
            }],
            "mode": "local",
        }
        mock_get_backend.return_value = backend
        result = json.loads(recall("test query", explain=True))
        bd = result["memories"][0]["scoring_breakdown"]
        assert "low confidence" in bd["explanation"]


class TestDraftMemory:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import draft_memory

        mock_get_backend.return_value = _mock_client()
        result = json.loads(draft_memory("Review this memory", tags=["draft"], importance=7, confidence=0.8))
        assert result["draft_id"] == "drf_test123"
        mock_get_backend.return_value.draft_memory.assert_called_once_with(
            observation="Review this memory",
            tags=["draft"],
            importance=7,
            confidence=0.8,
        )

    @patch("novyx_mcp.server._get_backend")
    def test_with_branch(self, mock_get_backend):
        from novyx_mcp.server import draft_memory

        mock_get_backend.return_value = _mock_client()
        result = json.loads(draft_memory("Review this memory", branch_id="branch-1"))
        assert result["branch_id"] == "branch-1"
        mock_get_backend.return_value.draft_memory.assert_called_once_with(
            observation="Review this memory",
            importance=5,
            confidence=1.0,
            branch_id="branch-1",
        )


class TestMemoryDrafts:
    @patch("novyx_mcp.server._get_backend")
    def test_list(self, mock_get_backend):
        from novyx_mcp.server import memory_drafts

        mock_get_backend.return_value = _mock_client()
        result = json.loads(memory_drafts(status="draft"))
        assert result["total_count"] == 1
        mock_get_backend.return_value.memory_drafts.assert_called_once_with(status="draft")

    @patch("novyx_mcp.server._get_backend")
    def test_list_by_branch(self, mock_get_backend):
        from novyx_mcp.server import memory_drafts

        mock_get_backend.return_value = _mock_client()
        memory_drafts(branch_id="branch-1")
        mock_get_backend.return_value.memory_drafts.assert_called_once_with(branch_id="branch-1")


class TestMergeDraft:
    @patch("novyx_mcp.server._get_backend")
    def test_merge(self, mock_get_backend):
        from novyx_mcp.server import merge_draft

        mock_get_backend.return_value = _mock_client()
        result = json.loads(merge_draft("drf_test123", supersede_memory_id="mem-1"))
        assert result["status"] == "merged"
        mock_get_backend.return_value.merge_draft.assert_called_once_with(
            "drf_test123",
            supersede_memory_id="mem-1",
        )


class TestMemoryBranch:
    @patch("novyx_mcp.server._get_backend")
    def test_branch_summary(self, mock_get_backend):
        from novyx_mcp.server import memory_branch

        mock_get_backend.return_value = _mock_client()
        result = json.loads(memory_branch("branch-1"))
        assert result["branch_id"] == "branch-1"
        mock_get_backend.return_value.memory_branch.assert_called_once_with("branch-1")

    @patch("novyx_mcp.server._get_backend")
    def test_merge_branch(self, mock_get_backend):
        from novyx_mcp.server import merge_branch

        mock_get_backend.return_value = _mock_client()
        result = json.loads(merge_branch("branch-1"))
        assert result["merged_count"] == 2
        mock_get_backend.return_value.merge_branch.assert_called_once_with("branch-1")

    @patch("novyx_mcp.server._get_backend")
    def test_reject_branch(self, mock_get_backend):
        from novyx_mcp.server import reject_branch

        mock_get_backend.return_value = _mock_client()
        result = json.loads(reject_branch("branch-1", reason="bad branch"))
        assert result["branch_id"] == "branch-1"
        mock_get_backend.return_value.reject_branch.assert_called_once_with(
            "branch-1",
            reason="bad branch",
        )


class TestDraftDiff:
    @patch("novyx_mcp.server._get_backend")
    def test_diff(self, mock_get_backend):
        from novyx_mcp.server import draft_diff

        mock_get_backend.return_value = _mock_client()
        result = json.loads(draft_diff("drf_test123", compare_to="mem-1"))
        assert result["recommendation"] == "merge_or_supersede"
        mock_get_backend.return_value.draft_diff.assert_called_once_with(
            "drf_test123",
            compare_to="mem-1",
        )


class TestRejectDraft:
    @patch("novyx_mcp.server._get_backend")
    def test_reject(self, mock_get_backend):
        from novyx_mcp.server import reject_draft

        mock_get_backend.return_value = _mock_client()
        result = json.loads(reject_draft("drf_test123", reason="not reliable"))
        assert result["status"] == "rejected"
        mock_get_backend.return_value.reject_draft.assert_called_once_with(
            "drf_test123",
            reason="not reliable",
        )


class TestForget:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import forget

        mock_get_backend.return_value = _mock_client()
        result = json.loads(forget("mem-1"))
        assert result["success"] is True
        assert result["memory_id"] == "mem-1"
        mock_get_backend.return_value.forget.assert_called_once_with("mem-1")


class TestListMemories:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import list_memories

        mock_get_backend.return_value = _mock_client()
        result = json.loads(list_memories())
        assert len(result) == 1
        assert result[0]["uuid"] == "mem-1"
        mock_get_backend.return_value.memories.assert_called_once_with(limit=50)

    @patch("novyx_mcp.server._get_backend")
    def test_with_tags(self, mock_get_backend):
        from novyx_mcp.server import list_memories

        mock_get_backend.return_value = _mock_client()
        list_memories(limit=10, tags=["work"])
        mock_get_backend.return_value.memories.assert_called_once_with(
            limit=10, tags=["work"]
        )


class TestMemoryStats:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import memory_stats

        mock_get_backend.return_value = _mock_client()
        result = json.loads(memory_stats())
        assert result["total_memories"] == 42
        mock_get_backend.return_value.stats.assert_called_once()


class TestRollback:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import rollback

        mock_get_backend.return_value = _mock_client()
        result = json.loads(rollback("2 hours ago"))
        assert result["success"] is True
        mock_get_backend.return_value.rollback.assert_called_once_with(
            "2 hours ago", dry_run=False
        )

    @patch("novyx_mcp.server._get_backend")
    def test_dry_run(self, mock_get_backend):
        from novyx_mcp.server import rollback

        mock_get_backend.return_value = _mock_client()
        rollback("2 hours ago", dry_run=True)
        mock_get_backend.return_value.rollback.assert_called_once_with(
            "2 hours ago", dry_run=True
        )


class TestAudit:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import audit

        mock_get_backend.return_value = _mock_client()
        result = json.loads(audit())
        assert len(result) == 1
        assert result[0]["operation"] == "CREATE"
        mock_get_backend.return_value.audit.assert_called_once_with(limit=20)

    @patch("novyx_mcp.server._get_backend")
    def test_with_operation_filter(self, mock_get_backend):
        from novyx_mcp.server import audit

        mock_get_backend.return_value = _mock_client()
        audit(limit=10, operation="DELETE")
        mock_get_backend.return_value.audit.assert_called_once_with(
            limit=10, operation="DELETE"
        )


class TestLinkMemories:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import link_memories

        mock_get_backend.return_value = _mock_client()
        result = json.loads(link_memories("mem-1", "mem-2"))
        assert result["source_id"] == "mem-1"
        assert result["relation"] == "related"
        mock_get_backend.return_value.link.assert_called_once_with(
            "mem-1", "mem-2", relation="related"
        )

    @patch("novyx_mcp.server._get_backend")
    def test_custom_relation(self, mock_get_backend):
        from novyx_mcp.server import link_memories

        mock_get_backend.return_value = _mock_client()
        link_memories("mem-1", "mem-2", relation="causes")
        mock_get_backend.return_value.link.assert_called_once_with(
            "mem-1", "mem-2", relation="causes"
        )


class TestAddTriple:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import add_triple

        mock_get_backend.return_value = _mock_client()
        result = json.loads(add_triple("blake", "likes", "python"))
        assert result["subject"] == "blake"
        assert result["predicate"] == "likes"
        mock_get_backend.return_value.triple.assert_called_once_with(
            "blake", "likes", "python"
        )


class TestQueryTriples:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import query_triples

        mock_get_backend.return_value = _mock_client()
        result = json.loads(query_triples(subject="blake"))
        assert result["total"] == 1
        mock_get_backend.return_value.triples.assert_called_once_with(subject="blake")

    @patch("novyx_mcp.server._get_backend")
    def test_all_filters(self, mock_get_backend):
        from novyx_mcp.server import query_triples

        mock_get_backend.return_value = _mock_client()
        query_triples(subject="blake", predicate="likes", object_name="python")
        mock_get_backend.return_value.triples.assert_called_once_with(
            subject="blake", predicate="likes", object="python"
        )

    @patch("novyx_mcp.server._get_backend")
    def test_no_filters(self, mock_get_backend):
        from novyx_mcp.server import query_triples

        mock_get_backend.return_value = _mock_client()
        query_triples()
        mock_get_backend.return_value.triples.assert_called_once_with()


# =========================================================================
# Resource tests
# =========================================================================


class TestResources:
    @patch("novyx_mcp.server._get_backend")
    def test_resource_memories(self, mock_get_backend):
        from novyx_mcp.server import resource_memories

        mock_get_backend.return_value = _mock_client()
        result = json.loads(resource_memories())
        assert isinstance(result, list)
        assert result[0]["uuid"] == "mem-1"

    @patch("novyx_mcp.server._get_backend")
    def test_resource_memory(self, mock_get_backend):
        from novyx_mcp.server import resource_memory

        mock_get_backend.return_value = _mock_client()
        result = json.loads(resource_memory("mem-1"))
        assert result["uuid"] == "mem-1"
        mock_get_backend.return_value.memory.assert_called_once_with("mem-1")

    @patch("novyx_mcp.server._get_backend")
    def test_resource_stats(self, mock_get_backend):
        from novyx_mcp.server import resource_stats

        mock_get_backend.return_value = _mock_client()
        result = json.loads(resource_stats())
        assert result["total_memories"] == 42

    @patch("novyx_mcp.server._get_backend")
    def test_resource_usage(self, mock_get_backend):
        from novyx_mcp.server import resource_usage

        mock_get_backend.return_value = _mock_client()
        result = json.loads(resource_usage())
        assert result["tier"] == "free"


# =========================================================================
# Prompt tests
# =========================================================================


class TestPrompts:
    @patch("novyx_mcp.server._get_backend")
    def test_memory_context(self, mock_get_backend):
        from novyx_mcp.server import memory_context

        mock_get_backend.return_value = _mock_client()
        result = memory_context("user preferences")
        assert "Relevant Memories for: user preferences" in result
        assert "Test observation" in result
        assert "0.95" in result

    @patch("novyx_mcp.server._get_backend")
    def test_memory_context_empty(self, mock_get_backend):
        from novyx_mcp.server import memory_context

        client = _mock_client()
        client.recall.return_value = SearchResult(
            query="nothing", total_results=0, memories=[]
        )
        mock_get_backend.return_value = client
        result = memory_context("nothing relevant")
        assert "No relevant memories found" in result

    @patch("novyx_mcp.server._get_backend")
    def test_session_summary(self, mock_get_backend):
        from novyx_mcp.server import session_summary

        mock_get_backend.return_value = _mock_client()
        result = session_summary("abc-123")
        assert "Session Summary: abc-123" in result
        mock_get_backend.return_value.memories.assert_called_once_with(
            tags=["session:abc-123"], limit=100
        )

    @patch("novyx_mcp.server._get_backend")
    def test_session_summary_empty(self, mock_get_backend):
        from novyx_mcp.server import session_summary

        client = _mock_client()
        client.memories.return_value = []
        mock_get_backend.return_value = client
        result = session_summary("empty-session")
        assert "No memories found for session" in result


# =========================================================================
# Context Spaces tests
# =========================================================================


class TestCreateSpace:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import create_space

        mock_get_backend.return_value = _mock_client()
        result = json.loads(create_space("Project Atlas"))
        assert result["space_id"] == "cs_test123"
        mock_get_backend.return_value.create_space.assert_called_once_with(
            name="Project Atlas", description=None, allowed_agent_ids=None, tags=None
        )

    @patch("novyx_mcp.server._get_backend")
    def test_with_agents_and_tags(self, mock_get_backend):
        from novyx_mcp.server import create_space

        mock_get_backend.return_value = _mock_client()
        create_space("Test", allowed_agents=["agent-1"], tags=["team:backend"])
        mock_get_backend.return_value.create_space.assert_called_once_with(
            name="Test", description=None, allowed_agent_ids=["agent-1"], tags=["team:backend"]
        )


class TestListSpaces:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import list_spaces

        mock_get_backend.return_value = _mock_client()
        result = json.loads(list_spaces())
        assert result["total_count"] == 1


class TestSpaceMemories:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import space_memories

        mock_get_backend.return_value = _mock_client()
        result = json.loads(space_memories("cs_test123"))
        assert result["total_count"] == 1
        mock_get_backend.return_value.space_memories.assert_called_once_with(
            "cs_test123", query=None, limit=50
        )


class TestDeleteSpace:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import delete_space

        mock_get_backend.return_value = _mock_client()
        result = json.loads(delete_space("cs_test123"))
        assert result["success"] is True


class TestShareSpace:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import share_space

        mock_get_backend.return_value = _mock_client()
        result = json.loads(share_space("project:test", "user@example.com"))
        assert result["token"] == "ns_abc123"


# =========================================================================
# Replay tests
# =========================================================================


class TestReplayTimeline:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import replay_timeline

        mock_get_backend.return_value = _mock_client()
        result = json.loads(replay_timeline())
        assert result["total_count"] == 1
        mock_get_backend.return_value.replay_timeline.assert_called_once()

    @patch("novyx_mcp.server._get_backend")
    def test_tier_gate(self, mock_get_backend):
        from novyx_mcp.server import replay_timeline

        client = _mock_client()
        client.replay_timeline.side_effect = Exception("403 Forbidden")
        mock_get_backend.return_value = client
        result = json.loads(replay_timeline())
        assert "Pro tier" in result["error"]


class TestReplaySnapshot:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import replay_snapshot

        mock_get_backend.return_value = _mock_client()
        result = json.loads(replay_snapshot("2026-02-27T00:00:00Z"))
        assert result["total_memories"] == 10


class TestReplayLifecycle:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import replay_lifecycle

        mock_get_backend.return_value = _mock_client()
        result = json.loads(replay_lifecycle("mem-1"))
        assert result["memory_id"] == "mem-1"
        assert result["current_state"] == "active"


class TestReplayDiff:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import replay_diff

        mock_get_backend.return_value = _mock_client()
        result = json.loads(replay_diff("2026-02-26T00:00:00Z", "2026-02-27T00:00:00Z"))
        assert len(result["added"]) == 1
        assert result["summary"]["added"] == 1


# =========================================================================
# Cortex tests
# =========================================================================


class TestCortexStatus:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import cortex_status

        mock_get_backend.return_value = _mock_client()
        result = json.loads(cortex_status())
        assert result["enabled"] is True

    @patch("novyx_mcp.server._get_backend")
    def test_tier_gate(self, mock_get_backend):
        from novyx_mcp.server import cortex_status

        client = _mock_client()
        client.cortex_status.side_effect = Exception("403 Forbidden")
        mock_get_backend.return_value = client
        result = json.loads(cortex_status())
        assert "Pro tier" in result["error"]


class TestCortexRun:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import cortex_run

        mock_get_backend.return_value = _mock_client()
        result = json.loads(cortex_run())
        assert result["consolidated"] == 2


class TestCortexInsights:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import cortex_insights

        mock_get_backend.return_value = _mock_client()
        result = json.loads(cortex_insights())
        assert result["total"] == 1

    @patch("novyx_mcp.server._get_backend")
    def test_tier_gate(self, mock_get_backend):
        from novyx_mcp.server import cortex_insights

        client = _mock_client()
        client.cortex_insights.side_effect = Exception("Forbidden: feature not available")
        mock_get_backend.return_value = client
        result = json.loads(cortex_insights())
        assert "Enterprise" in result["error"] or "Pro tier" in result["error"]


# =========================================================================
# New resource tests
# =========================================================================


class TestSpaceResources:
    @patch("novyx_mcp.server._get_backend")
    def test_resource_spaces(self, mock_get_backend):
        from novyx_mcp.server import resource_spaces

        mock_get_backend.return_value = _mock_client()
        result = json.loads(resource_spaces())
        assert result["total_count"] == 1

    @patch("novyx_mcp.server._get_backend")
    def test_resource_space(self, mock_get_backend):
        from novyx_mcp.server import resource_space

        mock_get_backend.return_value = _mock_client()
        result = json.loads(resource_space("cs_test123"))
        assert result["space_id"] == "cs_test123"


# =========================================================================
# New prompt tests
# =========================================================================


class TestSpaceContext:
    @patch("novyx_mcp.server._get_backend")
    def test_basic(self, mock_get_backend):
        from novyx_mcp.server import space_context

        mock_get_backend.return_value = _mock_client()
        result = space_context("cs_test123", "test query")
        assert "Context Space" in result
        assert "Test" in result

    @patch("novyx_mcp.server._get_backend")
    def test_empty(self, mock_get_backend):
        from novyx_mcp.server import space_context

        client = _mock_client()
        client.space_memories.return_value = {"total_count": 0, "memories": []}
        mock_get_backend.return_value = client
        result = space_context("cs_empty")
        assert "No memories found" in result
