"""Integration tests for graph algorithms — requires a running PostgreSQL."""

from __future__ import annotations

import pandas as pd
import pytest

from piggie._algorithms.centrality import centrality
from piggie._algorithms.community import community_detection
from piggie._algorithms.link_prediction import preferential_attachment, resource_allocation
from piggie._algorithms.paths import all_shortest_paths, dijkstra, shortest_path
from piggie._algorithms.ranking import pagerank
from piggie._algorithms.similarity import adamic_adar, common_neighbors, jaccard_similarity

pytestmark = pytest.mark.integration


@pytest.fixture()
def social_graph(fresh_graph):
    """A small social graph with 5 nodes and 6 edges."""
    db = fresh_graph
    nodes = pd.DataFrame([
        {"name": "Alice"}, {"name": "Bob"}, {"name": "Carol"},
        {"name": "Dave"}, {"name": "Eve"},
    ])
    db.load_nodes(nodes, "Person")
    edges = pd.DataFrame([
        {"name": "Alice", "friend": "Bob"},
        {"name": "Alice", "friend": "Carol"},
        {"name": "Bob", "friend": "Carol"},
        {"name": "Carol", "friend": "Dave"},
        {"name": "Dave", "friend": "Eve"},
        {"name": "Eve", "friend": "Alice"},
    ])
    db.load_edges(
        edges, "KNOWS",
        source_col="name", source_label="Person",
        target_col="friend", target_label="Person",
        target_key="name",
    )
    return db


def _get_node_id(db, name: str) -> int:
    """Helper to get a node ID by name."""
    result = db.cypher(
        f"MATCH (n:Person) WHERE n.name = '{name}' RETURN id(n) AS nid"
    )
    return result.single()["nid"]


class TestPageRank:
    def test_basic(self, social_graph):
        df = pagerank(social_graph, social_graph.graph)
        assert len(df) == 5
        assert "pagerank" in df.columns
        assert df["pagerank"].sum() == pytest.approx(1.0, abs=0.01)

    def test_custom_damping(self, social_graph):
        df = pagerank(social_graph, social_graph.graph, damping=0.5)
        assert len(df) == 5


class TestCommunityDetection:
    def test_louvain(self, social_graph):
        df = community_detection(social_graph, social_graph.graph, method="louvain")
        assert len(df) == 5
        assert "community_id" in df.columns

    def test_label_propagation(self, social_graph):
        df = community_detection(
            social_graph, social_graph.graph, method="label_propagation"
        )
        assert len(df) == 5

    def test_greedy_modularity(self, social_graph):
        df = community_detection(
            social_graph, social_graph.graph, method="greedy_modularity"
        )
        assert len(df) == 5
        assert "community_id" in df.columns

    def test_leiden(self, social_graph):
        try:
            import igraph  # noqa: F401
            import leidenalg  # noqa: F401
        except ImportError:
            pytest.skip("leidenalg or igraph not installed")
        df = community_detection(
            social_graph, social_graph.graph, method="leiden"
        )
        assert len(df) == 5
        assert "community_id" in df.columns


class TestShortestPath:
    def test_path_exists(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        dave_id = _get_node_id(social_graph, "Dave")

        path = shortest_path(social_graph, social_graph.graph, alice_id, dave_id)
        assert len(path) >= 2
        assert path[0] == alice_id
        assert path[-1] == dave_id

    def test_no_path(self, fresh_graph):
        db = fresh_graph
        # Create isolated nodes
        db.cypher("CREATE (:Island {name: 'A'})", graph=db.graph)
        db.commit()
        db.cypher("CREATE (:Island {name: 'B'})", graph=db.graph)
        db.commit()

        result = db.cypher("MATCH (n:Island) RETURN id(n) AS nid ORDER BY n.name")
        ids = [r["nid"] for r in result]
        assert len(ids) == 2

        path = shortest_path(db, db.graph, ids[0], ids[1])
        assert path == []

    def test_all_shortest_paths(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        dave_id = _get_node_id(social_graph, "Dave")

        paths = all_shortest_paths(social_graph, social_graph.graph, alice_id, dave_id)
        assert len(paths) >= 1
        for p in paths:
            assert p[0] == alice_id
            assert p[-1] == dave_id

    def test_dijkstra(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        dave_id = _get_node_id(social_graph, "Dave")

        df = dijkstra(social_graph, social_graph.graph, alice_id, dave_id)
        assert len(df) == 1
        assert "path" in df.columns
        assert "total_weight" in df.columns
        assert df.iloc[0]["path"][0] == alice_id
        assert df.iloc[0]["path"][-1] == dave_id


class TestCentrality:
    def test_degree(self, social_graph):
        df = centrality(social_graph, social_graph.graph, measure="degree")
        assert len(df) == 5
        assert "centrality" in df.columns

    def test_betweenness(self, social_graph):
        df = centrality(social_graph, social_graph.graph, measure="betweenness")
        assert len(df) == 5

    def test_closeness(self, social_graph):
        df = centrality(social_graph, social_graph.graph, measure="closeness")
        assert len(df) == 5

    def test_eigenvector(self, social_graph):
        df = centrality(social_graph, social_graph.graph, measure="eigenvector")
        assert len(df) == 5
        assert "centrality" in df.columns
        assert all(df["centrality"] >= 0)

    def test_harmonic(self, social_graph):
        df = centrality(social_graph, social_graph.graph, measure="harmonic")
        assert len(df) == 5
        assert "centrality" in df.columns
        assert all(df["centrality"] >= 0)


class TestSimilarity:
    def test_jaccard(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        bob_id = _get_node_id(social_graph, "Bob")

        df = jaccard_similarity(
            social_graph, social_graph.graph, [(alice_id, bob_id)]
        )
        assert len(df) >= 1
        assert list(df.columns) == ["node_u", "node_v", "score"]
        assert all(df["score"] >= 0)

    def test_adamic_adar(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        bob_id = _get_node_id(social_graph, "Bob")

        df = adamic_adar(
            social_graph, social_graph.graph, [(alice_id, bob_id)]
        )
        assert len(df) >= 1
        assert list(df.columns) == ["node_u", "node_v", "score"]

    def test_common_neighbors(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        bob_id = _get_node_id(social_graph, "Bob")

        df = common_neighbors(
            social_graph, social_graph.graph, alice_id, bob_id
        )
        assert len(df) == 1
        assert list(df.columns) == ["node_u", "node_v", "score"]
        assert df.iloc[0]["score"] >= 0


class TestLinkPrediction:
    def test_preferential_attachment(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        dave_id = _get_node_id(social_graph, "Dave")

        df = preferential_attachment(
            social_graph, social_graph.graph, [(alice_id, dave_id)]
        )
        assert len(df) >= 1
        assert list(df.columns) == ["node_u", "node_v", "score"]
        assert all(df["score"] >= 0)

    def test_resource_allocation(self, social_graph):
        alice_id = _get_node_id(social_graph, "Alice")
        dave_id = _get_node_id(social_graph, "Dave")

        df = resource_allocation(
            social_graph, social_graph.graph, [(alice_id, dave_id)]
        )
        assert len(df) >= 1
        assert list(df.columns) == ["node_u", "node_v", "score"]
        assert all(df["score"] >= 0)
