"""
tests/test_graphstore.py
GraphStore ABC + CRUD + traverse + factory + handler integration tests.
"""
from __future__ import annotations

import pytest

from trustmem.graphstore import create_graphstore
from trustmem.graphstore.base import GraphStore
from trustmem.graphstore.sqlite import SQLiteGraphStore
from trustmem.storage.sqlite_store import SQLiteStore


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

USER = "test-agent"


def _make_node(
    entity_id: str,
    name: str,
    *,
    user_id: str = USER,
    entity_type: str = "person",
    confidence: str = "medium",
    is_active: bool = True,
) -> dict:
    return {
        "entity_id": entity_id,
        "name": name,
        "entity_type": entity_type,
        "user_id": user_id,
        "confidence": confidence,
        "is_active": is_active,
        "mention_count": 1,
    }


def _make_edge(
    edge_id: str,
    subject_id: str,
    subject_name: str,
    predicate: str,
    object_id: str,
    object_name: str,
    *,
    user_id: str = USER,
    confidence: str = "medium",
    is_active: bool = True,
) -> dict:
    return {
        "edge_id": edge_id,
        "subject_id": subject_id,
        "subject_name": subject_name,
        "predicate": predicate,
        "object_id": object_id,
        "object_name": object_name,
        "user_id": user_id,
        "confidence": confidence,
        "is_active": is_active,
        "mention_count": 1,
    }


# ============================================================
# Fixtures
# ============================================================


@pytest.fixture
def gs() -> SQLiteGraphStore:
    """Fresh SQLiteGraphStore backed by in-memory SQLite."""
    store = SQLiteStore(":memory:")
    return SQLiteGraphStore(store)


@pytest.fixture
def populated_gs(gs: SQLiteGraphStore) -> SQLiteGraphStore:
    """SQLiteGraphStore pre-loaded with a small graph.

    Graph:  Alice --(works_at)--> Acme --(located_in)--> Tokyo
            Alice --(knows)--> Bob
    """
    gs.put_node(_make_node("ent_alice", "Alice"))
    gs.put_node(_make_node("ent_bob", "Bob"))
    gs.put_node(_make_node("ent_acme", "Acme", entity_type="organization"))
    gs.put_node(_make_node("ent_tokyo", "Tokyo", entity_type="location"))

    gs.put_edge(
        _make_edge("edge_1", "ent_alice", "Alice", "works_at", "ent_acme", "Acme")
    )
    gs.put_edge(
        _make_edge("edge_2", "ent_acme", "Acme", "located_in", "ent_tokyo", "Tokyo")
    )
    gs.put_edge(
        _make_edge("edge_3", "ent_alice", "Alice", "knows", "ent_bob", "Bob")
    )
    return gs


# ============================================================
# 1. ABC Compliance
# ============================================================


class TestABCCompliance:
    def test_sqlite_is_graphstore(self):
        assert issubclass(SQLiteGraphStore, GraphStore)

    def test_sqlite_instance(self, gs: SQLiteGraphStore):
        assert isinstance(gs, GraphStore)

    def test_cannot_instantiate_abc(self):
        with pytest.raises(TypeError):
            GraphStore()  # type: ignore[abstract]


# ============================================================
# 2. Node CRUD
# ============================================================


class TestNodeCRUD:
    def test_put_get_node(self, gs: SQLiteGraphStore):
        node = _make_node("ent_1", "Alice")
        gs.put_node(node)
        result = gs.get_node("ent_1")
        assert result is not None
        assert result["name"] == "Alice"

    def test_get_node_not_found(self, gs: SQLiteGraphStore):
        assert gs.get_node("nonexistent") is None

    def test_get_node_by_name(self, gs: SQLiteGraphStore):
        gs.put_node(_make_node("ent_1", "Alice"))
        result = gs.get_node_by_name("alice", USER)  # case-insensitive
        assert result is not None
        assert result["entity_id"] == "ent_1"

    def test_get_node_by_name_not_found(self, gs: SQLiteGraphStore):
        assert gs.get_node_by_name("nobody", USER) is None

    def test_delete_node_soft(self, gs: SQLiteGraphStore):
        gs.put_node(_make_node("ent_1", "Alice"))
        assert gs.delete_node("ent_1") is True
        # After soft-delete, get_node_by_name should return None
        assert gs.get_node_by_name("Alice", USER) is None

    def test_delete_node_not_found(self, gs: SQLiteGraphStore):
        assert gs.delete_node("nonexistent") is False

    def test_put_node_upsert(self, gs: SQLiteGraphStore):
        gs.put_node(_make_node("ent_1", "Alice", confidence="low"))
        gs.put_node(_make_node("ent_1", "Alice Updated", confidence="high"))
        result = gs.get_node("ent_1")
        assert result["name"] == "Alice Updated"
        assert result["confidence"] == "high"


# ============================================================
# 3. Edge CRUD
# ============================================================


class TestEdgeCRUD:
    def _setup_nodes(self, gs: SQLiteGraphStore):
        """Create prerequisite entity nodes for edge FK constraints."""
        gs.put_node(_make_node("ent_a", "Alice"))
        gs.put_node(_make_node("ent_b", "Bob"))

    def test_put_get_edge(self, gs: SQLiteGraphStore):
        self._setup_nodes(gs)
        edge = _make_edge("edge_1", "ent_a", "Alice", "knows", "ent_b", "Bob")
        gs.put_edge(edge)
        result = gs.get_edge("edge_1")
        assert result is not None
        assert result["predicate"] == "knows"

    def test_get_edge_not_found(self, gs: SQLiteGraphStore):
        assert gs.get_edge("nonexistent") is None

    def test_delete_edge_soft(self, gs: SQLiteGraphStore):
        self._setup_nodes(gs)
        gs.put_edge(
            _make_edge("edge_1", "ent_a", "Alice", "knows", "ent_b", "Bob")
        )
        assert gs.delete_edge("edge_1") is True
        raw = gs.get_edge("edge_1")
        assert raw is not None
        assert raw["is_active"] is False

    def test_delete_edge_not_found(self, gs: SQLiteGraphStore):
        assert gs.delete_edge("nonexistent") is False

    def test_put_edge_upsert(self, gs: SQLiteGraphStore):
        self._setup_nodes(gs)
        gs.put_edge(
            _make_edge("edge_1", "ent_a", "Alice", "knows", "ent_b", "Bob",
                        confidence="low")
        )
        gs.put_edge(
            _make_edge("edge_1", "ent_a", "Alice", "friends_with", "ent_b", "Bob",
                        confidence="high")
        )
        result = gs.get_edge("edge_1")
        assert result["predicate"] == "friends_with"
        assert result["confidence"] == "high"


# ============================================================
# 4. Search Edges
# ============================================================


class TestSearchEdges:
    def test_search_both_direction(self, populated_gs: SQLiteGraphStore):
        results = populated_gs.search_edges(USER, "Alice")
        # Alice has 2 outgoing edges: works_at, knows
        assert len(results) == 2

    def test_search_outgoing(self, populated_gs: SQLiteGraphStore):
        results = populated_gs.search_edges(USER, "Alice", direction="outgoing")
        assert len(results) == 2
        # All edges have Alice as subject
        for e in results:
            assert e["subject_name"] == "Alice"

    def test_search_incoming(self, populated_gs: SQLiteGraphStore):
        results = populated_gs.search_edges(USER, "Acme", direction="incoming")
        # Acme is the object in "works_at" from Alice
        assert len(results) == 1
        assert results[0]["subject_name"] == "Alice"

    def test_search_predicate_filter(self, populated_gs: SQLiteGraphStore):
        results = populated_gs.search_edges(USER, "Alice", predicate="knows")
        assert len(results) == 1
        assert results[0]["object_name"] == "Bob"

    def test_search_limit(self, populated_gs: SQLiteGraphStore):
        results = populated_gs.search_edges(USER, "Alice", limit=1)
        assert len(results) == 1

    def test_search_excludes_inactive(self, populated_gs: SQLiteGraphStore):
        populated_gs.delete_edge("edge_1")  # soft-delete works_at
        results = populated_gs.search_edges(USER, "Alice")
        assert len(results) == 1
        assert results[0]["predicate"] == "knows"

    def test_search_excludes_other_agent(self, populated_gs: SQLiteGraphStore):
        results = populated_gs.search_edges("other-agent", "Alice")
        assert len(results) == 0


# ============================================================
# 5. Traverse (BFS)
# ============================================================


class TestTraverse:
    def test_single_hop(self, populated_gs: SQLiteGraphStore):
        nodes, edges = populated_gs.traverse(USER, "Alice", max_hops=1)
        node_names = {n["name"] for n in nodes}
        # Alice + Acme + Bob (direct neighbors)
        assert "Alice" in node_names
        assert "Acme" in node_names
        assert "Bob" in node_names

    def test_multi_hop(self, populated_gs: SQLiteGraphStore):
        nodes, edges = populated_gs.traverse(USER, "Alice", max_hops=2)
        node_names = {n["name"] for n in nodes}
        # Alice → Acme → Tokyo (2 hops)
        assert "Tokyo" in node_names
        assert len(edges) == 3

    def test_hop_clamped_to_3(self, populated_gs: SQLiteGraphStore):
        # max_hops > 3 should be clamped
        nodes1, edges1 = populated_gs.traverse(USER, "Alice", max_hops=10)
        nodes2, edges2 = populated_gs.traverse(USER, "Alice", max_hops=3)
        assert len(nodes1) == len(nodes2)

    def test_empty_start(self, populated_gs: SQLiteGraphStore):
        nodes, edges = populated_gs.traverse(USER, "Nonexistent")
        assert len(nodes) == 0
        assert len(edges) == 0

    def test_cycle_handling(self, gs: SQLiteGraphStore):
        """BFS should handle cycles without infinite loops."""
        gs.put_node(_make_node("ent_a", "A"))
        gs.put_node(_make_node("ent_b", "B"))
        gs.put_edge(
            _make_edge("edge_ab", "ent_a", "A", "links", "ent_b", "B")
        )
        gs.put_edge(
            _make_edge("edge_ba", "ent_b", "B", "links", "ent_a", "A")
        )
        nodes, edges = gs.traverse(USER, "A", max_hops=3)
        assert len(nodes) == 2
        assert len(edges) == 2


# ============================================================
# 6. Get Stats
# ============================================================


class TestGetStats:
    def test_empty_stats(self, gs: SQLiteGraphStore):
        stats = gs.get_stats(USER)
        assert stats["total_entities"] == 0
        assert stats["total_edges"] == 0

    def test_stats_with_data(self, populated_gs: SQLiteGraphStore):
        stats = populated_gs.get_stats(USER)
        assert stats["total_entities"] == 4
        assert stats["total_edges"] == 3

    def test_stats_confidence_distribution(self, populated_gs: SQLiteGraphStore):
        stats = populated_gs.get_stats(USER)
        assert stats["confidence_distribution"]["medium"] == 4

    def test_stats_type_distribution(self, populated_gs: SQLiteGraphStore):
        stats = populated_gs.get_stats(USER)
        assert stats["entity_type_distribution"]["person"] == 2
        assert stats["entity_type_distribution"]["organization"] == 1
        assert stats["entity_type_distribution"]["location"] == 1

    def test_stats_excludes_other_agent(self, populated_gs: SQLiteGraphStore):
        stats = populated_gs.get_stats("other-agent")
        assert stats["total_entities"] == 0

    def test_stats_excludes_inactive(self, populated_gs: SQLiteGraphStore):
        populated_gs.delete_node("ent_alice")
        stats = populated_gs.get_stats(USER)
        assert stats["total_entities"] == 3


# ============================================================
# 7. Factory
# ============================================================


class TestFactory:
    def test_create_sqlite(self):
        store = SQLiteStore(":memory:")
        gs = create_graphstore("sqlite", sqlite_store=store)
        assert isinstance(gs, SQLiteGraphStore)

    def test_create_memory_raises(self):
        with pytest.raises(ValueError, match="Unknown graphstore backend"):
            create_graphstore("memory")

    def test_create_unknown_raises(self):
        with pytest.raises(ValueError, match="Unknown graphstore backend"):
            create_graphstore("redis")


# ============================================================
# 8. Handler Integration
# ============================================================


class TestHandlerIntegration:
    """Verify handlers auto-create the correct graphstore fallback."""

    def test_persistent_handler_fallback(self):
        """PersistentHandler without graphstore creates SQLiteGraphStore."""
        from conftest import MockAIClient

        try:
            from trustmem.handler.persistent import PersistentHandler
        except ImportError:
            pytest.skip("PersistentHandler dependencies not available")

        h = PersistentHandler(database_url=":memory:", ai_client=MockAIClient())
        assert isinstance(h._gs, SQLiteGraphStore)

    def test_persistent_handler_custom_gs(self):
        """PersistentHandler accepts a custom graphstore."""
        from conftest import MockAIClient

        try:
            from trustmem.handler.persistent import PersistentHandler
        except ImportError:
            pytest.skip("PersistentHandler dependencies not available")

        custom = SQLiteGraphStore(SQLiteStore(":memory:"))
        h = PersistentHandler(database_url=":memory:", ai_client=MockAIClient(), graphstore=custom)
        assert h._gs is custom
