# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License

"""
Tests for mesh memory/model sharing and borrowed-use tracking.

Covers:
  Phase 1: Markdown memory push/pull/list via MemoryBridge
  Phase 2: Model registry, catalog exchange, chunked download
  Phase 3: Usage tracking, Dross minting, chain anchoring
"""

import base64
import hashlib
import json
import zipfile
from unittest.mock import MagicMock, patch

import pytest

# ============================================================
# FIXTURES
# ============================================================


@pytest.fixture
def mesh_dir(tmp_path):
    d = tmp_path / "mesh"
    d.mkdir()
    return d


@pytest.fixture
def trust_manager():
    """Mock trust manager that approves everything."""
    tm = MagicMock()
    tm.check_permission.return_value = True
    tm.get_trusted_peers.return_value = []
    return tm


@pytest.fixture
def trust_manager_deny():
    """Mock trust manager that denies everything."""
    tm = MagicMock()
    tm.check_permission.return_value = False
    tm.get_trusted_peers.return_value = []
    return tm


@pytest.fixture
def mock_memory():
    """Mock Memory instance."""
    mem = MagicMock()
    mem.memories = {}
    mem.search.return_value = []
    return mem


@pytest.fixture
def mock_markdown_store():
    """Mock markdown memory store."""
    store = MagicMock()
    store.read_topic.return_value = "# Test Topic\nSome content here."
    store.list_topics.return_value = ["test-topic", "recipes"]
    store.get_tags.return_value = ["test"]
    store.get_revision.return_value = 1
    return store


# ============================================================
# PHASE 1: TRUST PERMISSIONS
# ============================================================


class TestPermissionMatrix:
    def test_new_permissions_exist(self):
        from familiar.core.mesh.trust import PermissionMatrix

        pm = PermissionMatrix()
        assert pm.share_markdown_memories is False
        assert pm.share_models is False

    def test_all_deny_includes_new(self):
        from familiar.core.mesh.trust import PermissionMatrix

        pm = PermissionMatrix.all_deny()
        assert pm.share_markdown_memories is False
        assert pm.share_models is False

    def test_all_grant_includes_new(self):
        from familiar.core.mesh.trust import PermissionMatrix

        pm = PermissionMatrix.all_grant()
        assert pm.share_markdown_memories is True
        assert pm.share_models is True

    def test_grant_revoke_new_permissions(self):
        from familiar.core.mesh.trust import PermissionMatrix

        pm = PermissionMatrix()
        assert pm.grant("share_markdown_memories") is True
        assert pm.share_markdown_memories is True
        assert pm.grant("share_models") is True
        assert pm.share_models is True
        assert pm.revoke("share_models") is True
        assert pm.share_models is False

    def test_from_dict_handles_new_fields(self):
        from familiar.core.mesh.trust import PermissionMatrix

        # Old trust.json without new fields
        old_data = {"list_skills": True, "invoke_skills": False}
        pm = PermissionMatrix.from_dict(old_data)
        assert pm.list_skills is True
        assert pm.share_markdown_memories is False  # default
        assert pm.share_models is False  # default

    def test_granted_list_includes_new(self):
        from familiar.core.mesh.trust import PermissionMatrix

        pm = PermissionMatrix.all_grant()
        granted = pm.granted_list
        assert "share_markdown_memories" in granted
        assert "share_models" in granted


# ============================================================
# PHASE 1: SHARED MEMORY META
# ============================================================


class TestSharedMemoryMeta:
    def test_origin_genesis_hash_field(self):
        from familiar.core.mesh.memory_bridge import SharedMemoryMeta

        meta = SharedMemoryMeta(
            key="test",
            mesh_origin="node_abc",
            mesh_origin_genesis_hash="deadbeef",
        )
        assert meta.mesh_origin_genesis_hash == "deadbeef"

    def test_to_dict_includes_genesis_hash(self):
        from familiar.core.mesh.memory_bridge import SharedMemoryMeta

        meta = SharedMemoryMeta(key="test", mesh_origin_genesis_hash="abc123")
        d = meta.to_dict()
        assert d["mesh_origin_genesis_hash"] == "abc123"

    def test_from_dict_with_genesis_hash(self):
        from familiar.core.mesh.memory_bridge import SharedMemoryMeta

        d = {"key": "test", "mesh_origin_genesis_hash": "hash123"}
        meta = SharedMemoryMeta.from_dict(d)
        assert meta.mesh_origin_genesis_hash == "hash123"

    def test_from_dict_without_genesis_hash(self):
        from familiar.core.mesh.memory_bridge import SharedMemoryMeta

        d = {"key": "test"}
        meta = SharedMemoryMeta.from_dict(d)
        assert meta.mesh_origin_genesis_hash is None


# ============================================================
# PHASE 1: MARKDOWN MEMORY SHARING
# ============================================================


class TestMarkdownMemorySharing:
    def test_topic_constants_exist(self):
        from familiar.core.mesh.memory_bridge import (
            MESH_MARKDOWN_LIST,
            MESH_MARKDOWN_LIST_RESPONSE,
            MESH_MARKDOWN_PUSH,
            MESH_MARKDOWN_REQUEST,
            MESH_MARKDOWN_RESPONSE,
            MESH_USAGE_RECORD,
        )

        assert MESH_MARKDOWN_PUSH == "mesh.markdown.push"
        assert MESH_MARKDOWN_LIST == "mesh.markdown.list"
        assert MESH_MARKDOWN_LIST_RESPONSE == "mesh.markdown.list_response"
        assert MESH_MARKDOWN_REQUEST == "mesh.markdown.request"
        assert MESH_MARKDOWN_RESPONSE == "mesh.markdown.response"
        assert MESH_USAGE_RECORD == "mesh.usage.record"

    def test_init_markdown_store(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.init_markdown_store(MagicMock())
        assert bridge._markdown_store is not None

    def test_set_usage_tracker(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        tracker = MagicMock()
        bridge.set_usage_tracker(tracker)
        assert bridge._usage_tracker is tracker

    def test_push_markdown_hipaa_blocked(
        self, mock_memory, mesh_dir, trust_manager, mock_markdown_store
    ):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
            hipaa_enabled=True,
        )
        bridge.init_markdown_store(mock_markdown_store)
        assert bridge.push_markdown_memory("test-topic") == 0

    def test_push_markdown_no_store(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        assert bridge.push_markdown_memory("test-topic") == 0

    def test_push_markdown_topic_not_found(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        store = MagicMock()
        store.read_topic.return_value = None

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.init_markdown_store(store)
        assert bridge.push_markdown_memory("nonexistent") == 0

    def test_handle_markdown_push(self, mock_memory, mesh_dir, trust_manager, mock_markdown_store):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_b",
            node_name="Test B",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.init_markdown_store(mock_markdown_store)

        content = "# Hello\nWorld"
        content_hash = hashlib.sha256(content.encode()).hexdigest()
        bridge.handle_markdown_push(
            {
                "origin_node": "node_a",
                "origin_name": "Test A",
                "origin_genesis_hash": "gen_abc",
                "topic": "hello",
                "content": content,
                "content_hash": content_hash,
            }
        )

        mock_markdown_store.write_topic.assert_called_once()

    def test_handle_markdown_push_hash_mismatch(
        self, mock_memory, mesh_dir, trust_manager, mock_markdown_store
    ):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_b",
            node_name="Test B",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.init_markdown_store(mock_markdown_store)

        bridge.handle_markdown_push(
            {
                "origin_node": "node_a",
                "origin_name": "Test A",
                "topic": "hello",
                "content": "# Hello\nWorld",
                "content_hash": "badhash",
            }
        )

        mock_markdown_store.write_topic.assert_not_called()

    def test_handle_markdown_push_denied(
        self, mock_memory, mesh_dir, trust_manager_deny, mock_markdown_store
    ):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_b",
            node_name="Test B",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager_deny,
        )
        bridge.init_markdown_store(mock_markdown_store)

        bridge.handle_markdown_push(
            {
                "origin_node": "node_a",
                "topic": "hello",
                "content": "test",
                "content_hash": hashlib.sha256(b"test").hexdigest(),
            }
        )

        mock_markdown_store.write_topic.assert_not_called()

    def test_handle_markdown_list(self, mock_memory, mesh_dir, trust_manager, mock_markdown_store):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.init_markdown_store(mock_markdown_store)

        response = bridge.handle_markdown_list(
            {
                "requesting_node": "node_b",
                "query_id": "q1",
            }
        )

        assert response is not None
        assert response["query_id"] == "q1"
        assert response["origin_node"] == "node_a"
        assert len(response["topics"]) == 2

    def test_handle_markdown_list_hipaa(
        self, mock_memory, mesh_dir, trust_manager, mock_markdown_store
    ):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
            hipaa_enabled=True,
        )
        bridge.init_markdown_store(mock_markdown_store)
        assert bridge.handle_markdown_list({"requesting_node": "b", "query_id": "q1"}) is None

    def test_handle_markdown_request(
        self, mock_memory, mesh_dir, trust_manager, mock_markdown_store
    ):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.init_markdown_store(mock_markdown_store)

        response = bridge.handle_markdown_request(
            {
                "requesting_node": "node_b",
                "query_id": "q2",
                "topic": "test-topic",
            }
        )

        assert response is not None
        assert response["query_id"] == "q2"
        assert response["topic"] == "test-topic"
        assert response["content"] == "# Test Topic\nSome content here."
        assert response["content_hash"]

    def test_handle_markdown_request_not_found(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        store = MagicMock()
        store.read_topic.return_value = None

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.init_markdown_store(store)

        response = bridge.handle_markdown_request(
            {
                "requesting_node": "node_b",
                "query_id": "q2",
                "topic": "missing",
            }
        )
        assert response is None

    def test_usage_tracking_in_get_mesh_context(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge, MeshMemoryResult

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )

        tracker = MagicMock()
        bridge.set_usage_tracker(tracker)

        # Mock search_mesh to return results
        results = [
            MeshMemoryResult(
                key="k1",
                value="v1",
                category="fact",
                importance=5,
                origin_node_id="peer_x",
                origin_node_name="Peer X",
            )
        ]
        with patch.object(bridge, "search_mesh", return_value=results):
            ctx = bridge.get_mesh_context("test query")

        assert "<mesh_context>" in ctx
        tracker.record_usage.assert_called_once()
        call_kwargs = tracker.record_usage.call_args
        assert (
            call_kwargs[1]["resource_type"] == "memory_query"
            or call_kwargs.kwargs.get("resource_type") == "memory_query"
        )


# ============================================================
# PHASE 2: MODEL REGISTRY
# ============================================================


class TestModelRegistry:
    def test_register_and_get(self, mesh_dir):
        from familiar.core.mesh.model_bridge import ModelEntry, ModelRegistry

        registry = ModelRegistry(mesh_dir)
        entry = ModelEntry(
            model_id="abc123",
            name="test-model",
            package_path="/tmp/test.familiar-model",
            package_size=1024,
            genesis_hash="gen_hash",
        )
        registry.register(entry)
        got = registry.get("abc123")
        assert got is not None
        assert got.name == "test-model"

    def test_unregister(self, mesh_dir):
        from familiar.core.mesh.model_bridge import ModelEntry, ModelRegistry

        registry = ModelRegistry(mesh_dir)
        entry = ModelEntry(
            model_id="abc",
            name="test",
            package_path="/tmp/test",
            package_size=100,
            genesis_hash="",
        )
        registry.register(entry)
        assert registry.unregister("abc") is True
        assert registry.get("abc") is None

    def test_get_shareable(self, mesh_dir):
        from familiar.core.mesh.model_bridge import ModelEntry, ModelRegistry

        registry = ModelRegistry(mesh_dir)

        m1 = ModelEntry(
            model_id="m1",
            name="shared",
            package_path="/tmp/m1",
            package_size=100,
            genesis_hash="",
            shareable=True,
        )
        m2 = ModelEntry(
            model_id="m2",
            name="private",
            package_path="/tmp/m2",
            package_size=100,
            genesis_hash="",
            shareable=False,
        )
        registry.register(m1)
        registry.register(m2)

        shareable = registry.get_shareable()
        assert len(shareable) == 1
        assert shareable[0].model_id == "m1"

    def test_share_and_unshare(self, mesh_dir):
        from familiar.core.mesh.model_bridge import ModelEntry, ModelRegistry

        registry = ModelRegistry(mesh_dir)
        entry = ModelEntry(
            model_id="m1",
            name="test",
            package_path="/tmp/test",
            package_size=100,
            genesis_hash="",
        )
        registry.register(entry)
        assert registry.share("m1") is True
        assert registry.get("m1").shareable is True
        assert registry.unshare("m1") is True
        assert registry.get("m1").shareable is False

    def test_persistence(self, mesh_dir):
        from familiar.core.mesh.model_bridge import ModelEntry, ModelRegistry

        registry1 = ModelRegistry(mesh_dir)
        registry1.register(
            ModelEntry(
                model_id="m1",
                name="persistent",
                package_path="/tmp/m1",
                package_size=100,
                genesis_hash="gen1",
            )
        )

        # Create new registry reading from same path
        registry2 = ModelRegistry(mesh_dir)
        got = registry2.get("m1")
        assert got is not None
        assert got.name == "persistent"

    def test_catalog_entry(self):
        from familiar.core.mesh.model_bridge import ModelEntry

        entry = ModelEntry(
            model_id="m1",
            name="test",
            package_path="/tmp/secret/path",
            package_size=1024,
            genesis_hash="gen",
            backend="ollama",
            base_model="qwen2.5:3b",
            tags=["nlp"],
        )
        cat = entry.catalog_entry()
        assert "package_path" not in cat
        assert cat["model_id"] == "m1"
        assert cat["backend"] == "ollama"

    def test_scan_packages_dir(self, mesh_dir):
        from familiar.core.mesh.model_bridge import ModelRegistry

        # Create a models directory with a test package
        models_dir = mesh_dir / "models"
        models_dir.mkdir()

        pkg_path = models_dir / "test.familiar-model"
        with zipfile.ZipFile(pkg_path, "w") as zf:
            meta = {"name": "scanned-model", "backend": "peft", "base_model": "llama3.2"}
            zf.writestr("metadata.json", json.dumps(meta))

        registry = ModelRegistry(mesh_dir)
        found = registry.scan_packages_dir()
        assert found == 1
        models = registry.get_all()
        assert len(models) == 1
        assert models[0].name == "scanned-model"


# ============================================================
# PHASE 2: MODEL BRIDGE
# ============================================================


class TestModelBridge:
    def test_init(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge

        bridge = ModelBridge(
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        assert bridge.node_id == "node_a"
        assert bridge.hipaa_enabled is False

    def test_handle_advertise(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge

        bridge = ModelBridge(
            node_id="node_b",
            node_name="Test B",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.handle_advertise(
            {
                "origin_node": "node_a",
                "origin_name": "Test A",
                "models": [
                    {"model_id": "m1", "name": "model1", "package_size": 1024},
                    {"model_id": "m2", "name": "model2", "package_size": 2048},
                ],
            }
        )

        catalogs = bridge.get_peer_catalogs()
        assert "node_a" in catalogs
        assert len(catalogs["node_a"]) == 2

    def test_handle_catalog_request(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge, ModelEntry

        bridge = ModelBridge(
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.registry.register(
            ModelEntry(
                model_id="m1",
                name="shared",
                package_path="/tmp/m1",
                package_size=100,
                genesis_hash="gen1",
                shareable=True,
            )
        )

        response = bridge.handle_catalog_request(
            {
                "requesting_node": "node_b",
                "query_id": "q1",
            }
        )

        assert response is not None
        assert len(response["models"]) == 1
        assert response["models"][0]["model_id"] == "m1"

    def test_handle_catalog_request_denied(self, mesh_dir, trust_manager_deny):
        from familiar.core.mesh.model_bridge import ModelBridge

        bridge = ModelBridge(
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager_deny,
        )
        response = bridge.handle_catalog_request(
            {
                "requesting_node": "node_b",
                "query_id": "q1",
            }
        )
        assert response is None

    def test_handle_catalog_request_hipaa(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge

        bridge = ModelBridge(
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
            hipaa_enabled=True,
        )
        response = bridge.handle_catalog_request(
            {
                "requesting_node": "node_b",
                "query_id": "q1",
            }
        )
        assert response is None

    def test_handle_download_chunks(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge

        bridge = ModelBridge(
            node_id="node_b",
            node_name="Test B",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )

        # Simulate a transfer
        transfer_id = "tf_001"
        dl_path = mesh_dir / "downloads" / f"{transfer_id}.familiar-model"
        dl_path.parent.mkdir(parents=True, exist_ok=True)

        # Set up download state
        bridge._downloads[transfer_id] = {
            "model_id": "m1",
            "peer_id": "node_a",
            "chunks": {},
            "total_chunks": None,
            "file_path": str(dl_path),
            "complete": False,
            "package_hash": "",
        }
        bridge._download_events[transfer_id] = MagicMock()

        # Send two chunks
        data1 = b"Hello, "
        data2 = b"World!"
        bridge.handle_download_chunk(
            {
                "transfer_id": transfer_id,
                "chunk_index": 0,
                "total_chunks": 2,
                "data": base64.b64encode(data1).decode(),
                "chunk_hash": hashlib.sha256(data1).hexdigest(),
            }
        )
        bridge.handle_download_chunk(
            {
                "transfer_id": transfer_id,
                "chunk_index": 1,
                "total_chunks": 2,
                "data": base64.b64encode(data2).decode(),
                "chunk_hash": hashlib.sha256(data2).hexdigest(),
            }
        )

        full_data = data1 + data2
        package_hash = hashlib.sha256(full_data).hexdigest()

        bridge.handle_download_complete(
            {
                "transfer_id": transfer_id,
                "package_hash": package_hash,
                "total_size": len(full_data),
                "model_id": package_hash,
            }
        )

        assert bridge._downloads[transfer_id]["complete"] is True
        assert dl_path.exists()
        assert dl_path.read_bytes() == full_data

    def test_handle_download_hash_mismatch(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge

        bridge = ModelBridge(
            node_id="node_b",
            node_name="Test B",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )

        transfer_id = "tf_bad"
        dl_path = mesh_dir / "downloads" / f"{transfer_id}.familiar-model"
        dl_path.parent.mkdir(parents=True, exist_ok=True)

        bridge._downloads[transfer_id] = {
            "model_id": "m1",
            "peer_id": "node_a",
            "chunks": {},
            "total_chunks": None,
            "file_path": str(dl_path),
            "complete": False,
            "package_hash": "",
        }
        bridge._download_events[transfer_id] = MagicMock()

        data = b"test data"
        bridge.handle_download_chunk(
            {
                "transfer_id": transfer_id,
                "chunk_index": 0,
                "total_chunks": 1,
                "data": base64.b64encode(data).decode(),
                "chunk_hash": hashlib.sha256(data).hexdigest(),
            }
        )

        bridge.handle_download_complete(
            {
                "transfer_id": transfer_id,
                "package_hash": "wrong_hash",
                "total_size": len(data),
                "model_id": "wrong",
            }
        )

        # Should NOT be marked complete
        assert bridge._downloads[transfer_id]["complete"] is False

    def test_handle_infer_request(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge, ModelEntry

        bridge = ModelBridge(
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.registry.register(
            ModelEntry(
                model_id="m1",
                name="test-model",
                package_path="/tmp/m1",
                package_size=100,
                genesis_hash="gen",
                shareable=True,
                backend="ollama",
                base_model="qwen2.5:3b",
            )
        )

        # Mock the ollama inference
        with patch.object(bridge, "_infer_ollama", return_value="Hello!"):
            response = bridge.handle_infer_request(
                {
                    "requesting_node": "node_b",
                    "query_id": "q1",
                    "model_id": "m1",
                    "prompt": "Say hello",
                }
            )

        assert response is not None
        assert response["response"] == "Hello!"

    def test_handle_infer_request_denied(self, mesh_dir, trust_manager_deny):
        from familiar.core.mesh.model_bridge import ModelBridge

        bridge = ModelBridge(
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager_deny,
        )
        response = bridge.handle_infer_request(
            {
                "requesting_node": "node_b",
                "query_id": "q1",
                "model_id": "m1",
                "prompt": "hi",
            }
        )
        assert response is None

    def test_mesh_event_dispatcher(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import (
            MESH_MODEL_ADVERTISE,
            ModelBridge,
        )

        bridge = ModelBridge(
            node_id="node_b",
            node_name="Test B",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )

        bridge.handle_mesh_event(
            MESH_MODEL_ADVERTISE,
            {"origin_node": "node_a", "models": [{"model_id": "m1", "name": "test"}]},
            "node_a",
        )
        assert "node_a" in bridge.get_peer_catalogs()

    def test_get_status(self, mesh_dir, trust_manager):
        from familiar.core.mesh.model_bridge import ModelBridge, ModelEntry

        bridge = ModelBridge(
            node_id="node_a",
            node_name="Test A",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        bridge.registry.register(
            ModelEntry(
                model_id="m1",
                name="test",
                package_path="/tmp/m1",
                package_size=100,
                genesis_hash="",
                shareable=True,
            )
        )

        status = bridge.get_status()
        assert status["total_models"] == 1
        assert status["shareable_models"] == 1


# ============================================================
# PHASE 3: USAGE TRACKER
# ============================================================


class TestUsageTracker:
    def test_record_usage(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="consumer_a", mesh_dir=mesh_dir)

        record = tracker.record_usage(
            provider_node="provider_b",
            consumer_node="consumer_a",
            resource_type="memory_query",
            resource_id="test_query",
            count=3,
        )

        assert record is not None
        assert record.resource_type == "memory_query"
        assert record.dross_amount == 0.3  # 0.1 * 3

    def test_record_usage_hipaa(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="consumer_a", mesh_dir=mesh_dir, hipaa_enabled=True)
        record = tracker.record_usage(
            provider_node="p",
            consumer_node="c",
            resource_type="memory_query",
            resource_id="q",
        )
        assert record is None

    def test_model_download_multiplier(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="consumer_a", mesh_dir=mesh_dir)

        record = tracker.record_usage(
            provider_node="provider_b",
            consumer_node="consumer_a",
            resource_type="model_download",
            resource_id="model_123",
        )

        assert record.dross_amount == 5.0

    def test_model_inference_multiplier(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="consumer_a", mesh_dir=mesh_dir)

        record = tracker.record_usage(
            provider_node="provider_b",
            consumer_node="consumer_a",
            resource_type="model_inference",
            resource_id="model_123",
        )

        assert record.dross_amount == 1.0

    def test_get_earnings_and_spending(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="node_a", mesh_dir=mesh_dir)

        # Record usage where we are the provider
        tracker.record_usage(
            provider_node="node_a",
            consumer_node="node_b",
            resource_type="model_inference",
            resource_id="m1",
        )

        # Record usage where we are the consumer
        tracker.record_usage(
            provider_node="node_c",
            consumer_node="node_a",
            resource_type="memory_query",
            resource_id="q1",
        )

        assert tracker.get_earnings() == 1.0
        assert tracker.get_spending() == 0.1

    def test_get_usage_summary(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="node_a", mesh_dir=mesh_dir)

        tracker.record_usage(
            provider_node="node_a",
            consumer_node="node_b",
            resource_type="memory_query",
            resource_id="q1",
            count=5,
        )
        tracker.record_usage(
            provider_node="node_c",
            consumer_node="node_a",
            resource_type="model_download",
            resource_id="m1",
        )

        summary = tracker.get_usage_summary()
        assert summary["total_provided"] == 1
        assert summary["total_consumed"] == 1
        assert "memory_query" in summary["by_type"]
        assert "model_download" in summary["by_type"]

    def test_handle_remote_usage(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="node_a", mesh_dir=mesh_dir)

        tracker.handle_remote_usage(
            {
                "provider_node": "node_a",
                "provider_genesis_hash": "gen_a",
                "consumer_node": "node_b",
                "consumer_genesis_hash": "gen_b",
                "resource_type": "memory_query",
                "resource_id": "q1",
                "usage_count": 1,
                "dross_amount": 0.1,
                "chain_block_hash": "",
                "timestamp": "2026-01-01T00:00:00",
            }
        )

        # Should be recorded as provider
        records = tracker.get_usage_as_provider()
        assert len(records) == 1

    def test_handle_remote_usage_not_for_us(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker = UsageTracker(node_id="node_a", mesh_dir=mesh_dir)

        # Provider is someone else — should be ignored
        tracker.handle_remote_usage(
            {
                "provider_node": "node_c",
                "consumer_node": "node_b",
                "resource_type": "memory_query",
                "resource_id": "q1",
            }
        )

        records = tracker.get_usage_as_provider()
        assert len(records) == 0

    def test_dross_minting(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        dross = MagicMock()
        tracker = UsageTracker(node_id="consumer_a", mesh_dir=mesh_dir, dross_ledger=dross)

        tracker.record_usage(
            provider_node="provider_b",
            consumer_node="consumer_a",
            resource_type="model_download",
            resource_id="m1",
            provider_genesis_hash="gen_b",
        )

        dross.mint_usage.assert_called_once()
        call_kwargs = dross.mint_usage.call_args
        assert call_kwargs[1]["address"] == "gen_b" or call_kwargs.kwargs.get("address") == "gen_b"

    def test_persistence(self, mesh_dir):
        from familiar.core.mesh.usage_tracker import UsageTracker

        tracker1 = UsageTracker(node_id="node_a", mesh_dir=mesh_dir)
        tracker1.record_usage(
            provider_node="node_a",
            consumer_node="node_b",
            resource_type="memory_query",
            resource_id="q1",
        )

        # New tracker reading from same DB
        tracker2 = UsageTracker(node_id="node_a", mesh_dir=mesh_dir)
        assert tracker2.get_earnings() == 0.1


# ============================================================
# PHASE 3: DROSS LEDGER mint_usage
# ============================================================


class TestDrossLedgerMintUsage:
    def test_mint_usage_positive(self, tmp_path):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_path / "dross.db")

        entry = ledger.mint_usage(address="gen_abc", amount=5.0, block_hash="blk1")
        assert entry.amount == 5.0
        assert entry.reason == "usage"
        assert ledger.balance("gen_abc") == 5.0

    def test_mint_usage_zero(self, tmp_path):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_path / "dross.db")

        entry = ledger.mint_usage(address="gen_abc", amount=0.0)
        assert entry.amount == 0.0
        assert ledger.balance("gen_abc") == 0.0

    def test_mint_usage_accumulates(self, tmp_path):
        from familiar.core.dross import DrossLedger

        ledger = DrossLedger(db_path=tmp_path / "dross.db")

        ledger.mint_usage(address="gen_abc", amount=1.0)
        ledger.mint_usage(address="gen_abc", amount=2.5)
        assert ledger.balance("gen_abc") == 3.5


# ============================================================
# PHASE 3: CHAIN ANCHOR
# ============================================================


class TestAnchorUsage:
    def test_anchor_usage_no_genesis(self):
        from familiar.skills.training.bridge import anchor_usage

        with patch("familiar.skills.training.bridge._get_store") as mock_store:
            mock_store.return_value.get_genesis.return_value = None
            result = anchor_usage({"resource_type": "memory_query"})
        assert result == ""

    def test_anchor_usage_with_genesis(self):

        mock_store = MagicMock()
        mock_store.get_genesis.return_value = MagicMock(block_hash="gen_hash")
        mock_latest = MagicMock(index=5, block_hash="prev_hash")
        mock_store.get_latest.return_value = mock_latest

        mock_block_cls = MagicMock()
        mock_block_instance = MagicMock()
        mock_block_instance.block_hash = "new_hash"
        mock_block_cls.return_value = mock_block_instance

        with (
            patch("familiar.skills.training.bridge._get_store", return_value=mock_store),
            patch(
                "familiar.skills.training.bridge._get_keypair",
                return_value=(MagicMock(), MagicMock()),
            ),
            patch("familiar.skills.training.bridge.ChainBlock", mock_block_cls)
            if False
            else patch("familiar.core.genesis.ChainBlock", mock_block_cls),
        ):
            # This test verifies the function structure exists
            # Full chain integration tested in test_genesis.py
            pass


# ============================================================
# INTEGRATION: __init__.py EXPORTS
# ============================================================


class TestMeshExports:
    def test_model_bridge_importable(self):
        from familiar.core.mesh import ModelBridge, ModelEntry, ModelRegistry

        assert ModelBridge is not None
        assert ModelEntry is not None
        assert ModelRegistry is not None

    def test_usage_tracker_importable(self):
        from familiar.core.mesh import UsageRecord, UsageTracker

        assert UsageTracker is not None
        assert UsageRecord is not None


# ============================================================
# INTEGRATION: GATEWAY ROUTING
# ============================================================


class TestGatewayRouting:
    def test_gateway_has_model_bridge_attr(self):
        """Verify MeshGateway has the model_bridge attribute."""
        # Check the class constructor sets it
        import inspect

        from familiar.core.mesh.gateway import MeshGateway

        source = inspect.getsource(MeshGateway.__init__)
        assert "model_bridge" in source

    def test_gateway_has_usage_tracker_attr(self):
        """Verify MeshGateway has the _usage_tracker attribute."""
        import inspect

        from familiar.core.mesh.gateway import MeshGateway

        source = inspect.getsource(MeshGateway.__init__)
        assert "_usage_tracker" in source

    def test_gateway_has_init_model_bridge(self):
        from familiar.core.mesh.gateway import MeshGateway

        assert hasattr(MeshGateway, "init_model_bridge")

    def test_gateway_has_init_usage_tracker(self):
        from familiar.core.mesh.gateway import MeshGateway

        assert hasattr(MeshGateway, "init_usage_tracker")


# ============================================================
# USAGE RECORD DATACLASS
# ============================================================


class TestUsageRecord:
    def test_defaults(self):
        from familiar.core.mesh.usage_tracker import UsageRecord

        rec = UsageRecord(
            provider_node="p",
            provider_genesis_hash="pg",
            consumer_node="c",
            consumer_genesis_hash="cg",
            resource_type="memory_query",
            resource_id="q1",
        )
        assert rec.usage_count == 1
        assert rec.dross_amount == 0.0
        assert rec.timestamp  # auto-set

    def test_to_dict(self):
        from familiar.core.mesh.usage_tracker import UsageRecord

        rec = UsageRecord(
            provider_node="p",
            provider_genesis_hash="pg",
            consumer_node="c",
            consumer_genesis_hash="cg",
            resource_type="memory_query",
            resource_id="q1",
            synced=True,
        )
        d = rec.to_dict()
        assert d["synced"] == 1  # bool -> int for SQLite compat

    def test_from_dict(self):
        from familiar.core.mesh.usage_tracker import UsageRecord

        d = {
            "provider_node": "p",
            "provider_genesis_hash": "pg",
            "consumer_node": "c",
            "consumer_genesis_hash": "cg",
            "resource_type": "memory_query",
            "resource_id": "q1",
            "synced": 1,
        }
        rec = UsageRecord.from_dict(d)
        assert rec.synced is True


# ============================================================
# MARKDOWN MEMORY STORE ADAPTER METHODS
# ============================================================


class TestMarkdownMemoryStoreAdapters:
    """Test the adapter methods added to MarkdownMemoryStore for mesh bridge."""

    def test_read_topic_returns_body(self, tmp_path):
        from familiar.core.markdown_memory import MarkdownMemoryStore

        store = MarkdownMemoryStore(tmp_path, "test")
        store.save_memory("hello", "world content")
        body = store.read_topic("hello")
        assert body is not None
        assert "world content" in body

    def test_read_topic_missing_returns_none(self, tmp_path):
        from familiar.core.markdown_memory import MarkdownMemoryStore

        store = MarkdownMemoryStore(tmp_path, "test")
        assert store.read_topic("nonexistent") is None

    def test_write_topic_creates_entry(self, tmp_path):
        from familiar.core.markdown_memory import MarkdownMemoryStore

        store = MarkdownMemoryStore(tmp_path, "test")
        result = store.write_topic("greetings", "hi there", origin_name="PeerBot")
        assert result is not None
        slug, rev = result
        assert rev >= 1
        body = store.read_topic("greetings")
        assert "hi there" in body

    def test_write_topic_tags_mesh_received(self, tmp_path):
        from familiar.core.markdown_memory import MarkdownMemoryStore

        store = MarkdownMemoryStore(tmp_path, "test")
        store.write_topic("tagged", "data", origin_genesis_hash="abc123def456789")
        tags = store.get_tags("tagged")
        assert "mesh-received" in tags
        assert any(t.startswith("genesis:") for t in tags)

    def test_get_tags_missing(self, tmp_path):
        from familiar.core.markdown_memory import MarkdownMemoryStore

        store = MarkdownMemoryStore(tmp_path, "test")
        assert store.get_tags("nope") == []

    def test_get_revision(self, tmp_path):
        from familiar.core.markdown_memory import MarkdownMemoryStore

        store = MarkdownMemoryStore(tmp_path, "test")
        store.save_memory("rev-test", "first")
        assert store.get_revision("rev-test") == 1
        store.save_memory("rev-test", "second")
        assert store.get_revision("rev-test") == 2

    def test_get_revision_missing(self, tmp_path):
        from familiar.core.markdown_memory import MarkdownMemoryStore

        store = MarkdownMemoryStore(tmp_path, "test")
        assert store.get_revision("nope") == 0


# ============================================================
# HANDLE_MARKDOWN_LIST DICT ITERATION
# ============================================================


class TestHandleMarkdownListDict:
    """Test that handle_markdown_list works with dict results from list_topics."""

    def test_list_with_dict_topics(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node1",
            node_name="TestNode",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        store = MagicMock()
        store.list_topics.return_value = [
            {"topic": "recipes", "tags": ["food"], "revision": 3, "updated": "2026-01-01"},
            {"topic": "notes", "tags": [], "revision": 1, "updated": "2026-01-02"},
        ]
        store.read_topic.return_value = "content"
        bridge.init_markdown_store(store)

        result = bridge.handle_markdown_list({"requesting_node": "peer1", "query_id": "q1"})
        assert result is not None
        topics = result["topics"]
        assert len(topics) == 2
        assert topics[0]["topic"] == "recipes"
        assert topics[0]["tags"] == ["food"]
        assert topics[0]["revision"] == 3

    def test_list_with_string_topics(self, mock_memory, mesh_dir, trust_manager):
        from familiar.core.mesh.memory_bridge import MemoryBridge

        bridge = MemoryBridge(
            memory=mock_memory,
            node_id="node1",
            node_name="TestNode",
            mesh_dir=mesh_dir,
            trust_manager=trust_manager,
        )
        store = MagicMock()
        store.list_topics.return_value = ["recipes", "notes"]
        store.read_topic.return_value = "content"
        bridge.init_markdown_store(store)

        result = bridge.handle_markdown_list({"requesting_node": "peer1", "query_id": "q1"})
        assert result is not None
        topics = result["topics"]
        assert len(topics) == 2
        assert topics[0]["topic"] == "recipes"
        assert topics[0]["tags"] == []


# ============================================================
# DROSS TRANSFER AND SPEND
# ============================================================


class TestDrossTransferSpend:
    """Test transfer() and spend() methods on DrossLedger."""

    def test_transfer_success(self, tmp_path):
        from familiar.core.dross import DrossLedger

        dl = DrossLedger(db_path=tmp_path / "test.db")
        dl.mint_usage("alice", 10.0)
        result = dl.transfer("alice", "bob", 3.0)
        assert result is not None
        debit, credit = result
        assert debit.amount == -3.0
        assert credit.amount == 3.0
        assert dl.balance("alice") == 7.0
        assert dl.balance("bob") == 3.0

    def test_transfer_insufficient_balance(self, tmp_path):
        from familiar.core.dross import DrossLedger

        dl = DrossLedger(db_path=tmp_path / "test.db")
        dl.mint_usage("alice", 5.0)
        result = dl.transfer("alice", "bob", 10.0)
        assert result is None
        assert dl.balance("alice") == 5.0
        assert dl.balance("bob") == 0.0

    def test_transfer_zero_amount(self, tmp_path):
        from familiar.core.dross import DrossLedger

        dl = DrossLedger(db_path=tmp_path / "test.db")
        dl.mint_usage("alice", 10.0)
        assert dl.transfer("alice", "bob", 0) is None
        assert dl.transfer("alice", "bob", -1.0) is None

    def test_spend_success(self, tmp_path):
        from familiar.core.dross import DrossLedger

        dl = DrossLedger(db_path=tmp_path / "test.db")
        dl.mint_usage("alice", 10.0)
        result = dl.spend("alice", 4.0, reason="model_download")
        assert result is not None
        assert result.amount == -4.0
        assert result.reason == "model_download"
        assert dl.balance("alice") == 6.0

    def test_spend_insufficient_balance(self, tmp_path):
        from familiar.core.dross import DrossLedger

        dl = DrossLedger(db_path=tmp_path / "test.db")
        dl.mint_usage("alice", 2.0)
        result = dl.spend("alice", 5.0)
        assert result is None
        assert dl.balance("alice") == 2.0

    def test_spend_zero_amount(self, tmp_path):
        from familiar.core.dross import DrossLedger

        dl = DrossLedger(db_path=tmp_path / "test.db")
        dl.mint_usage("alice", 10.0)
        assert dl.spend("alice", 0) is None
        assert dl.spend("alice", -1.0) is None

    def test_transfer_appears_in_ledger(self, tmp_path):
        from familiar.core.dross import DrossLedger

        dl = DrossLedger(db_path=tmp_path / "test.db")
        dl.mint_usage("alice", 10.0)
        dl.transfer("alice", "bob", 3.0)
        entries = dl.ledger()
        reasons = [e["reason"] for e in entries]
        assert "transfer_out" in reasons
        assert "transfer_in" in reasons
