# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Tests for Sprints 10-12: FileStore integration, Mesh sharing, Flip the switch."""

import threading
import time
from dataclasses import dataclass
from unittest.mock import MagicMock, patch

import pytest

from familiar.core.memory_tree import MemoryTree


@pytest.fixture
def tree(tmp_path):
    return MemoryTree(db_path=tmp_path / "integ_test.db")


# ── Sprint 10: FileStore Integration ──


class TestFileStoreIntegration:
    def test_store_creates_tree_entry(self, tree, tmp_path):
        """FileStore.store() should create a MemoryTree long-tier entry."""
        from familiar.core.files import FileStore

        fs = FileStore(files_dir=tmp_path / "files")
        with patch("familiar.core.memory_tree.get_memory_tree", return_value=tree):
            record = fs.store(
                data=b"test file content",
                filename="report.pdf",
                job_class="nonprofit",
                record_type="grants",
                record_id="grant-1",
                description="Annual report",
            )

        results = tree.search("report.pdf")
        assert len(results) >= 1
        entry = results[0]
        assert entry.tier == "long"
        assert "file" in entry.tags
        assert "nonprofit" in entry.tags
        assert entry.meta["file_id"] == record.file_id

    def test_store_includes_description(self, tree, tmp_path):
        from familiar.core.files import FileStore

        fs = FileStore(files_dir=tmp_path / "files")
        with patch("familiar.core.memory_tree.get_memory_tree", return_value=tree):
            fs.store(
                data=b"content",
                filename="doc.txt",
                job_class="helper",
                record_type="tasks",
                record_id="t-1",
                description="Task document",
            )

        results = tree.search("doc.txt")
        assert "Task document" in results[0].body

    def test_store_mime_tag(self, tree, tmp_path):
        from familiar.core.files import FileStore

        fs = FileStore(files_dir=tmp_path / "files")
        with patch("familiar.core.memory_tree.get_memory_tree", return_value=tree):
            fs.store(
                data=b"\x89PNG\r\n" + b"\x00" * 100,
                filename="photo.png",
                job_class="artist",
                record_type="pieces",
                record_id="p-1",
            )

        results = tree.search("photo.png")
        assert len(results) >= 1
        assert "image" in results[0].tags

    def test_delete_invalidates_tree_entry(self, tree, tmp_path):
        from familiar.core.files import FileStore

        fs = FileStore(files_dir=tmp_path / "files")
        with patch("familiar.core.memory_tree.get_memory_tree", return_value=tree):
            record = fs.store(
                data=b"delete me",
                filename="temp.txt",
                job_class="helper",
                record_type="tasks",
                record_id="t-2",
            )
            fs.delete(record.file_id)

        results = tree.search("temp.txt", include_expired=True)
        if results:
            assert not results[0].is_valid()

    def test_store_graceful_without_tree(self, tmp_path):
        """FileStore.store() should work even if MemoryTree is unavailable."""
        from familiar.core.files import FileStore

        fs = FileStore(files_dir=tmp_path / "files")
        with patch(
            "familiar.core.memory_tree.get_memory_tree",
            side_effect=Exception("unavailable"),
        ):
            record = fs.store(
                data=b"still works",
                filename="ok.txt",
                job_class="helper",
                record_type="tasks",
                record_id="t-3",
            )
        assert record.filename == "ok.txt"

    def test_store_idempotent(self, tree, tmp_path):
        """Storing the same file twice should create two tree entries (different file_ids)."""
        from familiar.core.files import FileStore

        fs = FileStore(files_dir=tmp_path / "files")
        with patch("familiar.core.memory_tree.get_memory_tree", return_value=tree):
            fs.store(
                data=b"data1",
                filename="same.txt",
                job_class="helper",
                record_type="tasks",
                record_id="t-4",
            )
            fs.store(
                data=b"data2",
                filename="same.txt",
                job_class="helper",
                record_type="tasks",
                record_id="t-5",
            )

        results = tree.search("same.txt")
        assert len(results) >= 2


# ── Sprint 11: Mesh Memory Sharing ──


class TestMeshMemoryTreeSharing:
    def _make_bridge(self, tree):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=MagicMock(),
            node_id="test-node",
            node_name="Test Node",
        )
        bridge.init_memory_tree(tree)
        return bridge

    def test_init_memory_tree(self, tree):
        bridge = self._make_bridge(tree)
        assert bridge._memory_tree is tree

    def test_share_tree_memory(self, tree):
        bridge = self._make_bridge(tree)
        bridge._gateway = MagicMock()
        bridge.trust_manager = MagicMock()
        bridge.trust_manager.list_peers.return_value = [
            MagicMock(peer_id="peer-1"),
        ]
        bridge.trust_manager.check_permission.return_value = True

        entry = tree.store(title="Shared Fact", body="share me", tier="working")
        count = bridge.share_tree_memory(entry.memory_id)
        assert count == 1
        bridge._gateway.send_to_peer.assert_called_once()

    def test_share_not_found(self, tree):
        bridge = self._make_bridge(tree)
        count = bridge.share_tree_memory("nonexistent")
        assert count == 0

    def test_share_hipaa_blocked(self, tree):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=MagicMock(),
            node_id="test",
            node_name="Test",
            hipaa_enabled=True,
        )
        bridge.init_memory_tree(tree)
        entry = tree.store(title="HIPAA", body="protected")
        assert bridge.share_tree_memory(entry.memory_id) == 0

    def test_handle_tree_push(self, tree):
        bridge = self._make_bridge(tree)
        bridge.trust_manager = MagicMock()
        bridge.trust_manager.check_permission.return_value = True

        result = bridge.handle_tree_push(
            {"title": "Remote Fact", "body": "from peer", "tags": ["remote"]},
            sender_id="peer-1",
        )
        assert result is True
        entries = tree.search("Remote Fact")
        assert len(entries) >= 1
        assert "mesh" in entries[0].tags
        assert entries[0].tier == "long"

    def test_handle_tree_push_dedup(self, tree):
        bridge = self._make_bridge(tree)
        bridge.trust_manager = MagicMock()
        bridge.trust_manager.check_permission.return_value = True

        bridge.handle_tree_push({"title": "Dup", "body": "same content"}, sender_id="p1")
        result = bridge.handle_tree_push({"title": "Dup", "body": "same content"}, sender_id="p1")
        assert result is False

    def test_handle_tree_push_permission_denied(self, tree):
        bridge = self._make_bridge(tree)
        bridge.trust_manager = MagicMock()
        bridge.trust_manager.check_permission.return_value = False

        result = bridge.handle_tree_push(
            {"title": "Blocked", "body": "no access"}, sender_id="bad-peer"
        )
        assert result is False

    def test_search_tree(self, tree):
        bridge = self._make_bridge(tree)
        tree.store(title="Searchable", body="find me", tier="long")

        results = bridge.search_tree("find me")
        assert len(results) >= 1
        assert results[0]["title"] == "Searchable"

    def test_search_tree_empty(self, tree):
        bridge = self._make_bridge(tree)
        results = bridge.search_tree("nonexistent")
        assert results == []


# ── Sprint 12: Flip the Switch ──


class TestFlipTheSwitch:
    def test_memory_manager_inits_tree(self):
        """MemoryManager.__init__ should call _init_memory_tree."""
        with patch("familiar.core.memory_manager.MemoryManager._init_memory_tree") as mock:
            from familiar.core.memory_manager import MemoryManager

            agent = MagicMock()
            agent.config = MagicMock()
            agent.config.agent = MagicMock()
            agent.config.agent.memory_enabled = False
            mm = MemoryManager(agent)
            mock.assert_called_once()

    def test_auto_import_on_empty_tree(self, tree):
        """Auto-import should run when tree is empty."""
        with (
            patch("familiar.core.memory_tree.get_memory_tree", return_value=tree),
            patch("familiar.core.memory_import.import_all") as mock_import,
        ):
            mock_import.return_value = {
                "markdown": {"imported": 3, "skipped": 0, "errors": 0},
                "brain": {"imported": 2, "skipped": 0, "errors": 0},
                "files": {"imported": 0, "skipped": 0, "errors": 0},
            }

            from familiar.core.memory_manager import MemoryManager

            agent = MagicMock()
            agent.config = MagicMock()
            agent.config.agent = MagicMock()
            agent.config.agent.memory_enabled = False
            mm = MemoryManager(agent)

            # Wait for background import thread
            time.sleep(0.5)
            mock_import.assert_called_once()

    def test_no_import_when_tree_has_data(self, tree):
        """Auto-import should NOT run when tree already has entries."""
        tree.store(title="Existing", body="data")
        with (
            patch("familiar.core.memory_tree.get_memory_tree", return_value=tree),
            patch("familiar.core.memory_import.import_all") as mock_import,
        ):
            from familiar.core.memory_manager import MemoryManager

            agent = MagicMock()
            agent.config = MagicMock()
            agent.config.agent = MagicMock()
            agent.config.agent.memory_enabled = False
            mm = MemoryManager(agent)

            time.sleep(0.3)
            mock_import.assert_not_called()

    def test_graceful_without_tree(self):
        """MemoryManager should work even if MemoryTree fails to init."""
        with patch(
            "familiar.core.memory_tree.get_memory_tree",
            side_effect=Exception("db error"),
        ):
            from familiar.core.memory_manager import MemoryManager

            agent = MagicMock()
            agent.config = MagicMock()
            agent.config.agent = MagicMock()
            agent.config.agent.memory_enabled = False
            mm = MemoryManager(agent)
            # Should not crash
            assert mm is not None

    def test_consolidation_is_primary_curation(self):
        """Verify consolidation_v2 is imported before markdown auto_curate."""
        # Read agent.py and verify ordering
        from pathlib import Path

        agent_src = Path(__file__).parent.parent / "core" / "agent.py"
        content = agent_src.read_text()

        # consolidation_v2 should appear BEFORE auto_curate_check
        tree_pos = content.find("memory_consolidation_v2")
        legacy_pos = content.find("auto_curate_check")

        assert tree_pos > 0, "consolidation_v2 not found in agent.py"
        assert legacy_pos > 0, "auto_curate_check not found in agent.py"
        assert tree_pos < legacy_pos, "consolidation_v2 should come before auto_curate_check"

    def test_legacy_is_fallback_only(self):
        """Verify legacy curation only runs when tree fails."""
        from pathlib import Path

        agent_src = Path(__file__).parent.parent / "core" / "agent.py"
        content = agent_src.read_text()

        # Find the fallback block
        assert "if not _tree_ok:" in content, "Fallback guard not found"
        # auto_curate_check should only appear after the guard
        guard_pos = content.find("if not _tree_ok:")
        curate_pos = content.find("auto_curate_check", guard_pos)
        assert curate_pos > guard_pos
