"""
Smoke tests for all four OCG backend classes.

Tests basic CRUD operations, Cypher execution, and selected algorithm methods
for each of the four backends: Graph, NetworKitGraph, RustworkxGraph, GraphrsGraph.
"""

import pytest
from ocg import Graph, NetworKitGraph, RustworkxGraph, GraphrsGraph


# ─── Parametrize over all backend classes ─────────────────────────────────────

ALL_BACKENDS = [Graph, NetworKitGraph, RustworkxGraph, GraphrsGraph]
BACKEND_IDS = ["petgraph", "networkit", "rustworkx", "graphrs"]


@pytest.fixture(params=ALL_BACKENDS, ids=BACKEND_IDS)
def empty_graph(request):
    """Return an empty graph instance for each backend."""
    return request.param()


@pytest.fixture(params=ALL_BACKENDS, ids=BACKEND_IDS)
def simple_graph(request):
    """Return a graph with 3 nodes and 2 edges for each backend."""
    g = request.param()
    n1 = g.create_node(["Person"], {"name": "Alice"})
    n2 = g.create_node(["Person"], {"name": "Bob"})
    n3 = g.create_node(["Person"], {"name": "Charlie"})
    g.create_relationship(n1, n2, "KNOWS")
    g.create_relationship(n2, n3, "KNOWS")
    return g, n1, n2, n3


# ─── Construction tests ───────────────────────────────────────────────────────

class TestConstruction:
    def test_empty_graph_starts_empty(self, empty_graph):
        assert empty_graph.node_count() == 0
        assert empty_graph.edge_count() == 0

    def test_repr(self, empty_graph):
        r = repr(empty_graph)
        assert "nodes=0" in r
        assert "relationships=0" in r


# ─── CRUD tests ───────────────────────────────────────────────────────────────

class TestCRUD:
    def test_create_node_returns_id(self, empty_graph):
        node_id = empty_graph.create_node(["Person"], {"name": "Alice"})
        assert isinstance(node_id, int)
        assert node_id >= 1

    def test_create_node_increments_count(self, empty_graph):
        empty_graph.create_node(["Person"], {"name": "Alice"})
        assert empty_graph.node_count() == 1
        empty_graph.create_node(["Person"], {"name": "Bob"})
        assert empty_graph.node_count() == 2

    def test_create_relationship_returns_id(self, empty_graph):
        n1 = empty_graph.create_node(["Person"], {"name": "Alice"})
        n2 = empty_graph.create_node(["Person"], {"name": "Bob"})
        edge_id = empty_graph.create_relationship(n1, n2, "KNOWS")
        assert isinstance(edge_id, int)

    def test_create_relationship_increments_edge_count(self, empty_graph):
        n1 = empty_graph.create_node(["Person"], {"name": "Alice"})
        n2 = empty_graph.create_node(["Person"], {"name": "Bob"})
        empty_graph.create_relationship(n1, n2, "KNOWS")
        assert empty_graph.edge_count() == 1

    def test_create_node_no_labels_or_properties(self, empty_graph):
        node_id = empty_graph.create_node([], {})
        assert node_id >= 1

    def test_multiple_nodes(self, empty_graph):
        ids = [empty_graph.create_node(["X"], {"i": i}) for i in range(5)]
        assert len(ids) == 5
        assert len(set(ids)) == 5
        assert empty_graph.node_count() == 5


# ─── Cypher execution tests ────────────────────────────────────────────────────

class TestCypherExecution:
    def test_create_and_match(self, empty_graph):
        empty_graph.execute("CREATE (n:Person {name: 'Alice', age: 30})")
        result = empty_graph.execute("MATCH (n:Person) RETURN n.name AS name")
        assert len(result) == 1
        assert result[0]["name"] == "Alice"

    def test_match_returns_empty_on_empty_graph(self, empty_graph):
        result = empty_graph.execute("MATCH (n:Person) RETURN n")
        assert result == []

    def test_relationship_cypher(self, empty_graph):
        empty_graph.execute("""
            CREATE (a:Person {name: 'Alice'}),
                   (b:Person {name: 'Bob'}),
                   (a)-[:KNOWS]->(b)
        """)
        result = empty_graph.execute("""
            MATCH (a:Person)-[:KNOWS]->(b:Person)
            RETURN a.name AS from, b.name AS to
        """)
        assert len(result) == 1
        assert result[0]["from"] == "Alice"
        assert result[0]["to"] == "Bob"

    def test_return_count(self, empty_graph):
        empty_graph.execute("CREATE (n:Item)")
        empty_graph.execute("CREATE (n:Item)")
        result = empty_graph.execute("MATCH (n:Item) RETURN count(n) AS cnt")
        assert result[0]["cnt"] == 2

    def test_parameterized_query(self, empty_graph):
        empty_graph.execute(
            "CREATE (n:Person {name: $name})",
            params={"name": "Alice"}
        )
        result = empty_graph.execute("MATCH (n:Person) RETURN n.name AS name")
        assert result[0]["name"] == "Alice"


# ─── Centrality algorithm tests ────────────────────────────────────────────────

class TestCentralityAlgorithms:
    def test_degree_centrality_returns_dict(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        result = g.degree_centrality()
        assert isinstance(result, dict)
        assert len(result) == 3
        for node_id in [n1, n2, n3]:
            assert node_id in result
            assert isinstance(result[node_id], float)

    def test_degree_centrality_hub_has_highest_score(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        scores = g.degree_centrality()
        # n2 (Bob) is the hub with both in and out edges
        assert scores[n2] >= scores[n1]
        assert scores[n2] >= scores[n3]

    def test_betweenness_centrality(self, simple_graph):
        g, _n1, _n2, _n3 = simple_graph
        result = g.betweenness_centrality(normalized=False)
        assert isinstance(result, dict)
        assert len(result) == 3
        for v in result.values():
            assert isinstance(v, float)
            assert v >= 0.0

    def test_closeness_centrality(self, simple_graph):
        g, _n1, _n2, _n3 = simple_graph
        result = g.closeness_centrality(normalized=False)
        assert isinstance(result, dict)
        assert len(result) == 3

    def test_pagerank(self, simple_graph):
        g, _n1, _n2, _n3 = simple_graph
        result = g.pagerank(damping=0.85, max_iter=100)
        assert isinstance(result, dict)
        assert len(result) == 3
        for v in result.values():
            assert isinstance(v, float)
            assert v >= 0.0


# ─── Pathfinding tests ─────────────────────────────────────────────────────────

class TestPathfinding:
    def test_bfs_path_found(self, simple_graph):
        g, n1, _n2, n3 = simple_graph
        path = g.bfs_path(n1, n3)
        assert path is not None
        assert path[0] == n1
        assert path[-1] == n3

    def test_bfs_path_not_found(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        # n3 -> n1 doesn't exist (directed)
        path = g.bfs_path(n3, n1)
        assert path is None

    def test_dijkstra_path(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        result = g.dijkstra_path(n1, n3)
        assert result is not None
        cost, path = result
        assert isinstance(cost, float)
        assert path[0] == n1
        assert path[-1] == n3

    def test_bellman_ford_distances(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        dists = g.bellman_ford_distances(n1)
        assert dists is not None
        assert isinstance(dists, dict)
        assert n1 in dists


# ─── Spanning tree tests ──────────────────────────────────────────────────────

class TestSpanningTrees:
    def test_minimum_spanning_tree(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        edges = g.minimum_spanning_tree()
        assert isinstance(edges, list)
        for e in edges:
            assert len(e) == 3  # (src, tgt, weight)

    def test_maximum_spanning_tree(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        edges = g.maximum_spanning_tree()
        assert isinstance(edges, list)


# ─── DAG tests ────────────────────────────────────────────────────────────────

class TestDAGAlgorithms:
    def test_is_dag_true_for_acyclic(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        assert g.is_dag() is True

    def test_topological_sort(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        order = g.topological_sort()
        assert isinstance(order, list)
        assert len(order) == 3
        assert set(order) == {n1, n2, n3}

    def test_find_cycles_empty_for_dag(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        cycles = g.find_cycles()
        assert cycles == []


# ─── Community detection tests ────────────────────────────────────────────────

class TestCommunityDetection:
    def test_louvain_communities(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        result = g.louvain_communities()
        assert isinstance(result, dict)
        assert "node_to_community" in result
        assert "communities" in result
        assert "modularity" in result
        assert "num_communities" in result
        assert len(result["node_to_community"]) == 3

    def test_label_propagation_communities(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        result = g.label_propagation_communities()
        assert isinstance(result, dict)
        assert "node_to_community" in result
        assert len(result["node_to_community"]) == 3

    def test_girvan_newman_communities(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        result = g.girvan_newman_communities(k=2)
        assert isinstance(result, dict)
        assert "communities" in result


# ─── Component tests ──────────────────────────────────────────────────────────

class TestComponents:
    def test_connected_components_single_component(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        components = g.connected_components()
        assert isinstance(components, list)
        # All 3 nodes should be in one weakly connected component
        total_nodes = sum(len(c) for c in components)
        assert total_nodes == 3

    def test_strongly_connected_components(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        sccs = g.strongly_connected_components()
        assert isinstance(sccs, list)
        total_nodes = sum(len(c) for c in sccs)
        assert total_nodes == 3

    def test_isolated_nodes_form_separate_components(self, empty_graph):
        n1 = empty_graph.create_node(["X"], {})
        n2 = empty_graph.create_node(["X"], {})
        components = empty_graph.connected_components()
        assert len(components) == 2


# ─── Coloring tests ───────────────────────────────────────────────────────────

class TestColoring:
    def test_node_coloring(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        colors = g.node_coloring()
        assert isinstance(colors, dict)
        assert len(colors) == 3
        for v in colors.values():
            assert isinstance(v, int)
            assert v >= 0

    def test_chromatic_number(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        chi = g.chromatic_number()
        assert isinstance(chi, int)
        assert chi >= 1


# ─── Matching tests ───────────────────────────────────────────────────────────

class TestMatching:
    def test_max_weight_matching(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        matching = g.max_weight_matching()
        assert isinstance(matching, list)

    def test_max_cardinality_matching(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        matching = g.max_cardinality_matching()
        assert isinstance(matching, list)


# ─── Network flow tests ───────────────────────────────────────────────────────

class TestNetworkFlow:
    def test_max_flow(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        result = g.max_flow(n1, n3)
        if result is not None:
            flow, edge_flows = result
            assert isinstance(flow, float)
            assert flow >= 0.0

    def test_min_cut_capacity(self, simple_graph):
        g, n1, n2, n3 = simple_graph
        capacity = g.min_cut_capacity(n1, n3)
        if capacity is not None:
            assert isinstance(capacity, float)
            assert capacity >= 0.0


# ─── Backend-specific tests ────────────────────────────────────────────────────

class TestBackendSpecific:
    def test_networkit_graph_class_name(self):
        g = NetworKitGraph()
        assert "NetworKitGraph" in repr(g)

    def test_rustworkx_graph_class_name(self):
        g = RustworkxGraph()
        assert "RustworkxGraph" in repr(g)

    def test_graphrs_graph_class_name(self):
        g = GraphrsGraph()
        assert "GraphrsGraph" in repr(g)

    def test_petgraph_graph_class_name(self):
        g = Graph()
        assert "Graph" in repr(g)

    def test_all_backends_independent(self):
        """Each backend maintains independent state."""
        g1 = NetworKitGraph()
        g2 = RustworkxGraph()
        g3 = GraphrsGraph()
        g4 = Graph()

        g1.create_node(["Person"], {"name": "Alice"})
        assert g1.node_count() == 1
        assert g2.node_count() == 0
        assert g3.node_count() == 0
        assert g4.node_count() == 0

    def test_all_backends_cypher_compatible(self):
        """All backends execute the same Cypher query consistently."""
        query = "CREATE (n:Person {name: 'Alice'}) RETURN n.name AS name"
        for BackendClass in ALL_BACKENDS:
            g = BackendClass()
            result = g.execute(query)
            assert len(result) == 1
            assert result[0]["name"] == "Alice"
