# Copyright 2025-2026 Gregorio Elias Roecker Momm and nxCypher contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Regression tests for the vendored isomorphism module.

These tests verify that the subgraph isomorphism search functions
produce correct results, matching the behavior of the original
grandiso library.

"""

import pytest
import networkx as nx

from nxcypher.isomorphism import (
    find_motifs,
    find_motifs_iter,
    _is_node_attr_match,
    _is_node_structural_match,
    _is_edge_attr_match,
)


class TestFindMotifsBasic:
    """Basic subgraph isomorphism search tests."""

    def test_single_edge_motif(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("c", "d")])
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host)
        assert len(results) == 3
        pairs = [(r["X"], r["Y"]) for r in results]
        assert ("a", "b") in pairs
        assert ("b", "c") in pairs
        assert ("c", "d") in pairs

    def test_triangle_motif(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("c", "a"), ("d", "e")])
        motif = nx.DiGraph()
        motif.add_edges_from([("X", "Y"), ("Y", "Z"), ("Z", "X")])

        results = find_motifs(motif, host)
        assert len(results) == 3  # 3 rotations of the triangle

    def test_no_match(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("c", "d")])
        motif = nx.DiGraph()
        motif.add_edges_from([("X", "Y"), ("Y", "Z")])

        results = find_motifs(motif, host)
        assert len(results) == 0

    def test_empty_host(self):
        host = nx.DiGraph()
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host)
        assert len(results) == 0

    def test_chain_motif(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("c", "d")])
        motif = nx.DiGraph()
        motif.add_edges_from([("X", "Y"), ("Y", "Z")])

        results = find_motifs(motif, host)
        assert len(results) == 2
        chains = [(r["X"], r["Y"], r["Z"]) for r in results]
        assert ("a", "b", "c") in chains
        assert ("b", "c", "d") in chains


class TestFindMotifsIter:
    """Test the iterator version of find_motifs."""

    def test_iter_yields_same_as_list(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("c", "a")])
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        iter_results = list(find_motifs_iter(motif, host))
        list_results = find_motifs(motif, host)
        assert len(iter_results) == len(list_results)

    def test_iter_is_lazy(self):
        host = nx.DiGraph()
        host.add_edges_from([(i, i + 1) for i in range(100)])
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        it = find_motifs_iter(motif, host)
        first = next(it)
        assert "X" in first and "Y" in first


class TestNodeAttrMatch:
    """Test node attribute matching."""

    def test_matching_attrs(self):
        host = nx.DiGraph()
        host.add_node("a", color="red")
        host.add_node("b", color="blue")
        host.add_edge("a", "b")

        motif = nx.DiGraph()
        motif.add_node("X", color="red")
        motif.add_node("Y")
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host)
        assert len(results) == 1
        assert results[0]["X"] == "a"
        assert results[0]["Y"] == "b"

    def test_no_matching_attrs(self):
        host = nx.DiGraph()
        host.add_node("a", color="red")
        host.add_node("b", color="blue")
        host.add_edge("a", "b")

        motif = nx.DiGraph()
        motif.add_node("X", color="green")
        motif.add_node("Y")
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host)
        assert len(results) == 0


class TestEdgeAttrMatch:
    """Test edge attribute matching."""

    def test_matching_edge_attrs(self):
        host = nx.DiGraph()
        host.add_edge("a", "b", weight=10)
        host.add_edge("b", "c", weight=20)

        motif = nx.DiGraph()
        motif.add_edge("X", "Y", weight=10)

        results = find_motifs(motif, host)
        assert len(results) == 1
        assert results[0]["X"] == "a"
        assert results[0]["Y"] == "b"


class TestHints:
    """Test hint-based constraint propagation."""

    def test_single_hint(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("c", "d")])
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host, hints=[{"X": "b"}])
        assert len(results) == 1
        assert results[0]["X"] == "b"
        assert results[0]["Y"] == "c"

    def test_multiple_hints(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("c", "d")])
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host, hints=[{"X": "a"}, {"X": "c"}])
        assert len(results) == 2
        pairs = [(r["X"], r["Y"]) for r in results]
        assert ("a", "b") in pairs
        assert ("c", "d") in pairs


class TestCountOnly:
    """Test count_only mode."""

    def test_count_only(self):
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("c", "d")])
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        count = find_motifs(motif, host, count_only=True)
        assert count == 3


class TestLimit:
    """Test limit parameter."""

    def test_limit(self):
        host = nx.DiGraph()
        host.add_edges_from([(i, i + 1) for i in range(20)])
        motif = nx.DiGraph()
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host, limit=5)
        assert len(results) == 5


class TestUndirected:
    """Test undirected graph matching."""

    def test_undirected_match(self):
        host = nx.Graph()
        host.add_edges_from([("a", "b"), ("b", "c")])
        motif = nx.Graph()
        motif.add_edge("X", "Y")

        results = find_motifs(motif, host, directed=False)
        # Undirected: (a,b), (b,a), (b,c), (c,b) = 4 matches
        assert len(results) == 4


class TestIsomorphismsOnly:
    """Test isomorphisms_only parameter."""

    def test_isomorphisms_only_filters_extra_edges(self):
        # Host has a complete graph K3 (triangle with all edges)
        host = nx.DiGraph()
        host.add_edges_from([("a", "b"), ("b", "c"), ("a", "c"),
                             ("b", "a"), ("c", "b"), ("c", "a")])
        # Motif is just a chain: X -> Y -> Z (no edge X -> Z)
        motif = nx.DiGraph()
        motif.add_edges_from([("X", "Y"), ("Y", "Z")])

        mono_results = find_motifs(motif, host, isomorphisms_only=False)
        iso_results = find_motifs(motif, host, isomorphisms_only=True)
        # Isomorphisms should be a subset of monomorphisms
        assert len(iso_results) <= len(mono_results)


class TestIntegrationWithNXCypher:
    """Integration tests ensuring isomorphism works correctly with NXCypher."""

    def test_basic_cypher_query_uses_isomorphism(self):
        from nxcypher import NXCypher

        G = nx.DiGraph()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_edge("a", "b")

        result = NXCypher(G).run("""
            MATCH (x)-[]->(y)
            RETURN x.name, y.name
        """)
        assert result["x.name"] == ["Alice"]
        assert result["y.name"] == ["Bob"]

    def test_cypher_with_node_attrs(self):
        from nxcypher import NXCypher

        G = nx.DiGraph()
        G.add_node("a", name="Alice", age=30)
        G.add_node("b", name="Bob", age=25)
        G.add_node("c", name="Charlie", age=35)
        G.add_edge("a", "b")
        G.add_edge("a", "c")

        result = NXCypher(G).run("""
            MATCH (x)-[]->(y)
            WHERE x.name = "Alice" AND y.age > 30
            RETURN y.name
        """)
        assert result["y.name"] == ["Charlie"]

    def test_cypher_with_hints(self):
        from nxcypher import NXCypher

        G = nx.DiGraph()
        G.add_edges_from([("a", "b"), ("b", "c"), ("c", "d")])

        result = NXCypher(G).run(
            "MATCH (x)-[]->(y) RETURN x, y",
            hints=[{"x": "b"}]
        )
        assert len(result["x"]) == 1

    def test_cypher_triangle_pattern(self):
        from nxcypher import NXCypher

        G = nx.DiGraph()
        G.add_edges_from([("a", "b"), ("b", "c"), ("c", "a")])

        result = NXCypher(G).run("""
            MATCH (x)-[]->(y)-[]->(z)-[]->(x)
            RETURN x, y, z
        """)
        assert len(result["x"]) == 3  # 3 rotations
