"""Tests for OPTIONAL MATCH feature."""

import networkx as nx
import pytest

from . import NXCypher

ACCEPTED_GRAPH_TYPES = [nx.MultiDiGraph, nx.DiGraph]


class TestOptionalMatch:
    """Test OPTIONAL MATCH functionality."""

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_no_result(self, graph_type):
        """Test OPTIONAL MATCH returns None when no match found."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_edge("a", "b")  # Regular edge, not KNOWS

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)
            RETURN a.name, friend.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["friend.name"] == [None]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_with_result(self, graph_type):
        """Test OPTIONAL MATCH returns values when match found."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_edge("a", "b", __labels__={"KNOWS"})

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)
            RETURN a.name, friend.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["friend.name"] == ["Bob"]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_partial_results(self, graph_type):
        """Test OPTIONAL MATCH with some nodes having matches and some not."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_node("c", name="Charlie")
        G.add_edge("a", "c", __labels__={"KNOWS"})
        # Bob doesn't know anyone

        result = NXCypher(G).run("""
            MATCH (person)
            OPTIONAL MATCH (person)-[:KNOWS]->(friend)
            RETURN person.name, friend.name
        """)

        # Check results (order may vary)
        names_friends = list(zip(result["person.name"], result["friend.name"]))

        # Alice knows Charlie
        assert ("Alice", "Charlie") in names_friends
        # Bob doesn't know anyone - friend should be None
        bob_results = [(p, f) for p, f in names_friends if p == "Bob"]
        assert len(bob_results) == 1
        assert bob_results[0][1] is None

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_untyped_edge(self, graph_type):
        """Test OPTIONAL MATCH without edge type."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_node("c", name="Charlie")
        G.add_edge("a", "b")

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-->(b)
            RETURN a.name, b.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["b.name"] == ["Bob"]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_no_outgoing(self, graph_type):
        """Test OPTIONAL MATCH on node with no outgoing edges."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_edge("b", "a")  # Edge to Alice, not from Alice

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-->(other)
            RETURN a.name, other.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["other.name"] == [None]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_multiple_optional_matches(self, graph_type):
        """Test multiple OPTIONAL MATCH clauses where both match.

        Note: When multiple OPTIONAL MATCH clauses are present and one fails,
        the current implementation sets all optional nodes to None because
        the patterns are combined. This differs from Neo4j's behavior where
        each OPTIONAL MATCH is processed sequentially.
        """
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_node("c", name="Charlie")
        G.add_edge("a", "b", __labels__={"KNOWS"})
        G.add_edge("a", "c", __labels__={"WORKS_WITH"})  # Both edges exist

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)
            OPTIONAL MATCH (a)-[:WORKS_WITH]->(colleague)
            RETURN a.name, friend.name, colleague.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["friend.name"] == ["Bob"]
        assert result["colleague.name"] == ["Charlie"]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_preserves_required_match(self, graph_type):
        """Test that OPTIONAL MATCH doesn't affect required MATCH."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        # No edges

        # Required MATCH for a node that exists, optional for non-existent relationship
        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-->(other)
            RETURN a.name, other.name
        """)

        assert len(result["a.name"]) == 1
        assert result["a.name"] == ["Alice"]
        assert result["other.name"] == [None]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_with_property_filter(self, graph_type):
        """Test OPTIONAL MATCH with property filter on optional node."""
        G = graph_type()
        G.add_node("a", name="Alice", age=30)
        G.add_node("b", name="Bob", age=40)
        G.add_node("c", name="Charlie", age=25)
        G.add_edge("a", "b")
        G.add_edge("a", "c")

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-->(older {age: 40})
            RETURN a.name, older.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["older.name"] == ["Bob"]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_chain(self, graph_type):
        """Test OPTIONAL MATCH with chained pattern."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_node("c", name="Charlie")
        G.add_edge("a", "b", __labels__={"KNOWS"})
        G.add_edge("b", "c", __labels__={"KNOWS"})

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)-[:KNOWS]->(foaf)
            RETURN a.name, friend.name, foaf.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["friend.name"] == ["Bob"]
        assert result["foaf.name"] == ["Charlie"]


class TestMultipleOptionalMatchIndependent:
    """Test that multiple OPTIONAL MATCH clauses are processed independently.

    Neo4j behavior: Each OPTIONAL MATCH is processed sequentially.
    If one fails, it doesn't affect the others.
    """

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_first_optional_succeeds_second_fails(self, graph_type):
        """First OPTIONAL MATCH succeeds, second fails - both should be independent."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_edge("a", "b", __labels__={"KNOWS"})
        # No WORKS_WITH edge exists

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)
            OPTIONAL MATCH (a)-[:WORKS_WITH]->(colleague)
            RETURN a.name, friend.name, colleague.name
        """)

        assert result["a.name"] == ["Alice"]
        # First OPTIONAL should succeed
        assert result["friend.name"] == ["Bob"]
        # Second OPTIONAL should be None (no WORKS_WITH edge)
        assert result["colleague.name"] == [None]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_first_optional_fails_second_succeeds(self, graph_type):
        """First OPTIONAL MATCH fails, second succeeds - both should be independent."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("c", name="Charlie")
        G.add_edge("a", "c", __labels__={"WORKS_WITH"})
        # No KNOWS edge exists

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)
            OPTIONAL MATCH (a)-[:WORKS_WITH]->(colleague)
            RETURN a.name, friend.name, colleague.name
        """)

        assert result["a.name"] == ["Alice"]
        # First OPTIONAL should be None (no KNOWS edge)
        assert result["friend.name"] == [None]
        # Second OPTIONAL should succeed
        assert result["colleague.name"] == ["Charlie"]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_both_optionals_fail(self, graph_type):
        """Both OPTIONAL MATCHes fail - both should return None."""
        G = graph_type()
        G.add_node("a", name="Alice")
        # No edges at all

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)
            OPTIONAL MATCH (a)-[:WORKS_WITH]->(colleague)
            RETURN a.name, friend.name, colleague.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["friend.name"] == [None]
        assert result["colleague.name"] == [None]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_three_optionals_mixed_results(self, graph_type):
        """Three OPTIONAL MATCHes with mixed success/failure."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_node("d", name="Dave")
        G.add_edge("a", "b", __labels__={"KNOWS"})
        G.add_edge("a", "d", __labels__={"LIVES_WITH"})
        # No WORKS_WITH edge

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-[:KNOWS]->(friend)
            OPTIONAL MATCH (a)-[:WORKS_WITH]->(colleague)
            OPTIONAL MATCH (a)-[:LIVES_WITH]->(roommate)
            RETURN a.name, friend.name, colleague.name, roommate.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["friend.name"] == ["Bob"]  # KNOWS exists
        assert result["colleague.name"] == [None]  # WORKS_WITH doesn't exist
        assert result["roommate.name"] == ["Dave"]  # LIVES_WITH exists


class TestOptionalMatchEdgeCases:
    """Test edge cases for OPTIONAL MATCH."""

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_empty_graph_nodes_only(self, graph_type):
        """Test OPTIONAL MATCH on graph with nodes but no edges."""
        G = graph_type()
        G.add_node("a", name="Alice")

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)-->(b)
            RETURN a.name, b.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["b.name"] == [None]

    @pytest.mark.parametrize("graph_type", ACCEPTED_GRAPH_TYPES)
    def test_optional_match_left_direction(self, graph_type):
        """Test OPTIONAL MATCH with incoming edge direction."""
        G = graph_type()
        G.add_node("a", name="Alice")
        G.add_node("b", name="Bob")
        G.add_edge("b", "a")  # Bob -> Alice

        result = NXCypher(G).run("""
            MATCH (a {name: "Alice"})
            OPTIONAL MATCH (a)<--(follower)
            RETURN a.name, follower.name
        """)

        assert result["a.name"] == ["Alice"]
        assert result["follower.name"] == ["Bob"]
