"""
Tests for MCard GraphRAG System

Tests for graph extractor, graph store, and GraphRAG engine.
"""

import pytest

from mcard import MCard

# ─────────────────────────────────────────────────────────────────────────────
# Unit Tests: Entity and Relationship Data Classes
# ─────────────────────────────────────────────────────────────────────────────


class TestDataClasses:
    """Tests for Entity and Relationship data classes."""

    def test_entity_creation(self):
        """Test Entity creation."""
        from mcard.rag.graph import Entity

        entity = Entity(
            name="MCard",
            type="TECHNOLOGY",
            description="Content-addressable storage library",
        )

        assert entity.name == "MCard"
        assert entity.type == "TECHNOLOGY"
        assert entity.description == "Content-addressable storage library"

    def test_entity_to_dict(self):
        """Test Entity to_dict method."""
        from mcard.rag.graph import Entity

        entity = Entity("Python", "TECHNOLOGY")
        result = entity.to_dict()

        assert result["name"] == "Python"
        assert result["type"] == "TECHNOLOGY"

    def test_relationship_creation(self):
        """Test Relationship creation."""
        from mcard.rag.graph import Relationship

        rel = Relationship(
            source="MCard", target="Python", relationship="implemented_in"
        )

        assert rel.source == "MCard"
        assert rel.target == "Python"
        assert rel.relationship == "implemented_in"
        assert rel.weight == 1.0


# ─────────────────────────────────────────────────────────────────────────────
# Unit Tests: Graph Store
# ─────────────────────────────────────────────────────────────────────────────


class TestGraphStore:
    """Tests for GraphStore."""

    def test_initialization(self):
        """Test graph store initialization."""
        from mcard.rag.graph import GraphStore

        store = GraphStore(db_path=":memory:")
        assert store.conn is not None

    def test_add_entity(self):
        """Test adding an entity."""
        from mcard.rag.graph import Entity, GraphStore

        store = GraphStore(db_path=":memory:")
        entity = Entity("MCard", "TECHNOLOGY", "Test description")

        entity_id = store.add_entity(entity, "test_hash")

        assert entity_id > 0
        assert store.count_entities() == 1

    def test_get_entity_by_name(self):
        """Test retrieving entity by name."""
        from mcard.rag.graph import Entity, GraphStore

        store = GraphStore(db_path=":memory:")
        entity = Entity("Python", "TECHNOLOGY")
        store.add_entity(entity, "test_hash")

        result = store.get_entity_by_name("Python")

        assert result is not None
        assert result["name"] == "Python"
        assert result["type"] == "TECHNOLOGY"

    def test_add_relationship(self):
        """Test adding a relationship."""
        from mcard.rag.graph import Entity, GraphStore

        store = GraphStore(db_path=":memory:")
        e1 = Entity("MCard", "TECHNOLOGY")
        e2 = Entity("Python", "TECHNOLOGY")

        id1 = store.add_entity(e1, "test_hash")
        id2 = store.add_entity(e2, "test_hash")

        rel_id = store.add_relationship(id1, id2, "implemented_in", "test_hash")

        assert rel_id > 0
        assert store.count_relationships() == 1

    def test_find_related(self):
        """Test finding related entities."""
        from mcard.rag.graph import Entity, GraphStore

        store = GraphStore(db_path=":memory:")

        # Create entities
        e1 = Entity("MCard", "TECHNOLOGY")
        e2 = Entity("Python", "TECHNOLOGY")
        e3 = Entity("SQLite", "TECHNOLOGY")

        id1 = store.add_entity(e1, "test")
        id2 = store.add_entity(e2, "test")
        id3 = store.add_entity(e3, "test")

        # Create relationships
        store.add_relationship(id1, id2, "implemented_in", "test")
        store.add_relationship(id1, id3, "uses", "test")

        # Find related
        related = store.find_related("MCard", hops=1)

        assert len(related) == 2
        names = [r["entity"]["name"] for r in related]
        assert "Python" in names
        assert "SQLite" in names

    def test_extraction_tracking(self):
        """Test extraction tracking."""
        from mcard.rag.graph import GraphStore

        store = GraphStore(db_path=":memory:")

        assert not store.is_extracted("hash123")

        store.mark_extracted("hash123", 5, 3)

        assert store.is_extracted("hash123")

    def test_get_stats(self):
        """Test getting statistics."""
        from mcard.rag.graph import Entity, GraphStore

        store = GraphStore(db_path=":memory:")
        e1 = Entity("MCard", "TECHNOLOGY")
        store.add_entity(e1, "test")

        stats = store.get_stats()

        assert "entity_count" in stats
        assert "relationship_count" in stats
        assert "entity_types" in stats


# ─────────────────────────────────────────────────────────────────────────────
# Unit Tests: Graph Extractor
# ─────────────────────────────────────────────────────────────────────────────


class TestGraphExtractor:
    """Tests for GraphExtractor."""

    def test_initialization(self):
        """Test extractor initialization."""
        from mcard.rag.graph import GraphExtractor

        extractor = GraphExtractor()
        assert extractor.model == "gemma3:latest"
        assert extractor.temperature == 0.1

    def test_clean_json(self):
        """Test JSON cleaning."""
        from mcard.rag.graph import GraphExtractor

        extractor = GraphExtractor()

        # Test trailing comma removal
        messy = '{"entities": [{"name": "test",}]}'
        clean = extractor._clean_json(messy)
        assert ",}" not in clean


# ─────────────────────────────────────────────────────────────────────────────
# Unit Tests: store_extraction_result
# ─────────────────────────────────────────────────────────────────────────────


class TestStoreExtractionResult:
    """Tests for store_extraction_result function."""

    def test_store_result(self):
        """Test storing extraction result."""
        from mcard.rag.graph import (
            Entity,
            ExtractionResult,
            GraphStore,
            Relationship,
            store_extraction_result,
        )

        store = GraphStore(db_path=":memory:")

        result = ExtractionResult(
            entities=[
                Entity("MCard", "TECHNOLOGY"),
                Entity("Python", "TECHNOLOGY"),
            ],
            relationships=[
                Relationship("MCard", "Python", "implemented_in"),
            ],
        )

        entity_count, rel_count = store_extraction_result(store, result, "test_hash")

        assert entity_count == 2
        assert rel_count == 1
        assert store.is_extracted("test_hash")


# ─────────────────────────────────────────────────────────────────────────────
# Integration Tests (Require Ollama)
# ─────────────────────────────────────────────────────────────────────────────


@pytest.mark.network
@pytest.mark.slow
@pytest.mark.skipif(
    False,  # Will be enabled when running live tests
    reason="Live tests require Ollama to be running",
)
class TestLiveGraphRAG:
    """Live integration tests for GraphRAG."""

    def test_entity_extraction(self):
        """Test entity extraction with LLM."""
        from mcard.rag.graph import GraphExtractor

        extractor = GraphExtractor()
        result = extractor.extract("Python is a programming language.")

        assert result.success
        assert len(result.entities) > 0

    def test_graphrag_query(self):
        """Test GraphRAG query."""
        from mcard.rag.graph import GraphRAGEngine

        engine = GraphRAGEngine(vector_db_path=":memory:")

        mcard = MCard("MCard is a content-addressable storage library.")
        engine.index(mcard, extract_graph=True)

        response = engine.query("What is MCard?")
        assert response.answer


class TestCommunityDetection:
    """Tests for community detection and summarization."""

    def test_community_storage(self):
        """Test adding and retrieving communities."""
        from mcard.rag.graph import GraphStore

        store = GraphStore(db_path=":memory:")

        comm_id = store.add_community(
            title="Test Community",
            summary="This is a test summary.",
            member_ids=[1, 2, 3],
        )

        assert comm_id > 0

        communities = store.get_communities()
        assert len(communities) == 1
        assert communities[0]["title"] == "Test Community"
        assert communities[0]["member_ids"] == [1, 2, 3]

    def test_detect_communities_algorithm(self):
        """Test LPA algorithm logic."""
        from mcard.rag.graph import GraphStore
        from mcard.rag.graph.community import detect_communities

        store = GraphStore(db_path=":memory:")

        # Create a simple connected component: 1-2-3
        store.add_relationship(1, 2, "link", "hash1")
        store.add_relationship(2, 3, "link", "hash1")

        # Create another component: 4-5
        store.add_relationship(4, 5, "link", "hash1")

        # We need entities to exist for relationships to be valid in FK
        # But for detect_communities which just reads graph_relationships,
        # it might work if FKs weren't enforced or if we populate them.
        # SQLite usually enforces FKs if enabled. Let's populate entities.
        from mcard.rag.graph import Entity

        for i in range(1, 6):
            store.add_entity(Entity(f"E{i}", "TEST"), "hash1")

        # Re-add rels (IDs are auto-incremented, so we need to be careful)
        # Actually easier to use the returned IDs
        id1 = store.add_entity(Entity("E1", "TEST"), "hash1")
        id2 = store.add_entity(Entity("E2", "TEST"), "hash1")
        id3 = store.add_entity(Entity("E3", "TEST"), "hash1")
        id4 = store.add_entity(Entity("E4", "TEST"), "hash1")
        id5 = store.add_entity(Entity("E5", "TEST"), "hash1")

        store.add_relationship(id1, id2, "link", "hash1")
        store.add_relationship(id2, id3, "link", "hash1")
        store.add_relationship(id4, id5, "link", "hash1")

        communities = detect_communities(store, max_iter=5)

        # Expect 2 communities
        # Note: LPA can sometimes fail on very small graphs or specific topologies,
        # but 1-2-3 and 4-5 should be stable.
        # 1-2-3 is a line. 2 has neighbors 1,3. 1 has 2. 3 has 2.
        # Label prop: 1<-2, 3<-2. 2 usually takes label of 1 or 3.
        # Converges to single label for connected component.

        assert len(communities) == 2
        sizes = sorted([len(c) for c in communities])
        assert sizes == [2, 3]
