"""Unit tests for the Cypher query builder — no database required."""

from __future__ import annotations

import pytest

from piggie._cypher.builder import Create, Match, Merge  # noqa: I001
from piggie.exceptions import BuilderError

# ---------------------------------------------------------------------------
# Match builder
# ---------------------------------------------------------------------------


class TestMatch:
    def test_simple_match_all(self):
        q, p = Match("Person", alias="n").return_all().build()
        assert q == "MATCH (n:Person) RETURN *"
        assert p == ()

    def test_match_with_properties(self):
        q, p = Match("Person", alias="n", name="Alice").return_("n").build()
        assert q == "MATCH (n:Person {name: %s}) RETURN n"
        assert p == ("Alice",)

    def test_match_no_label(self):
        q, p = Match(alias="n").return_("n").build()
        assert q == "MATCH (n) RETURN n"
        assert p == ()

    def test_match_with_relationship(self):
        q, p = (
            Match("Person", alias="p")
            .rel("KNOWS", alias="r")
            .node("Person", alias="q")
            .return_("p.name", "q.name")
            .build()
        )
        assert q == "MATCH (p:Person)-[r:KNOWS]->(q:Person) RETURN p.name, q.name"
        assert p == ()

    def test_match_incoming_relationship(self):
        q, p = (
            Match("Person", alias="p")
            .rel("FOLLOWS", direction="in")
            .node("Person", alias="q")
            .return_("p", "q")
            .build()
        )
        assert q == "MATCH (p:Person)<-[:FOLLOWS]-(q:Person) RETURN p, q"

    def test_match_undirected_relationship(self):
        q, p = (
            Match("Person", alias="p")
            .rel("KNOWS", direction="both")
            .node("Person", alias="q")
            .return_("p", "q")
            .build()
        )
        assert q == "MATCH (p:Person)-[:KNOWS]-(q:Person) RETURN p, q"

    def test_where_equality(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__name="Alice")
            .return_("p")
            .build()
        )
        assert q == "MATCH (p:Person) WHERE p.name = %s RETURN p"
        assert p == ("Alice",)

    def test_where_gt_operator(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__age__gt=30)
            .return_("p.name")
            .build()
        )
        assert q == "MATCH (p:Person) WHERE p.age > %s RETURN p.name"
        assert p == (30,)

    def test_where_multiple_conditions(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__age__gte=18, p__name="Bob")
            .return_("p")
            .build()
        )
        assert "WHERE" in q
        assert "p.age >= %s" in q
        assert "p.name = %s" in q
        assert "AND" in q
        assert set(p) == {18, "Bob"}

    def test_where_contains(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__name__contains="lic")
            .return_("p")
            .build()
        )
        assert q == "MATCH (p:Person) WHERE p.name CONTAINS %s RETURN p"
        assert p == ("lic",)

    def test_where_starts_with(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__name__starts_with="Al")
            .return_("p")
            .build()
        )
        assert q == "MATCH (p:Person) WHERE p.name STARTS WITH %s RETURN p"
        assert p == ("Al",)

    def test_where_is_null(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__email__is_null=True)
            .return_("p")
            .build()
        )
        assert q == "MATCH (p:Person) WHERE p.email IS NULL RETURN p"
        assert p == ()

    def test_where_raw(self):
        q, p = (
            Match("Person", alias="p")
            .where(raw="p.age > 30 OR p.name = 'Alice'")
            .return_("p")
            .build()
        )
        assert "p.age > 30 OR p.name = 'Alice'" in q

    def test_where_invalid_operator(self):
        with pytest.raises(BuilderError, match="Unknown operator"):
            Match("Person", alias="p").where(p__name__bogus="x").build()

    def test_where_invalid_key_format(self):
        with pytest.raises(BuilderError, match="Invalid where key"):
            Match("Person", alias="p").where(name="x").build()

    def test_order_by(self):
        q, p = (
            Match("Person", alias="p")
            .return_("p.name", "p.age")
            .order_by("p.age")
            .build()
        )
        assert q == "MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age"

    def test_order_by_desc(self):
        q, p = (
            Match("Person", alias="p")
            .return_("p.name")
            .order_by("p.age", desc=True)
            .build()
        )
        assert "ORDER BY p.age DESC" in q

    def test_limit_and_skip(self):
        q, p = (
            Match("Person", alias="p")
            .return_("p")
            .skip(10)
            .limit(5)
            .build()
        )
        assert "SKIP 10" in q
        assert "LIMIT 5" in q

    def test_with_clause(self):
        q, p = (
            Match("Person", alias="p")
            .with_("p", "count(*) AS cnt")
            .return_("p.name", "cnt")
            .build()
        )
        assert "WITH p, count(*) AS cnt" in q

    def test_set_properties(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__name="Alice")
            .set("p", age=31, active=True)
            .return_("p")
            .build()
        )
        assert "SET p.age = %s, p.active = %s" in q
        assert "Alice" in p
        assert 31 in p
        assert True in p

    def test_delete(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__name="Alice")
            .delete("p")
            .build()
        )
        assert "DELETE p" in q
        assert "DETACH" not in q

    def test_detach_delete(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__name="Alice")
            .delete("p", detach=True)
            .build()
        )
        assert "DETACH DELETE p" in q

    def test_rel_with_properties(self):
        q, p = (
            Match("Person", alias="p")
            .rel("KNOWS", alias="r", since=2020)
            .node("Person", alias="q")
            .return_("p", "r", "q")
            .build()
        )
        assert "r:KNOWS {since: %s}" in q
        assert p == (2020,)

    def test_where_in_operator(self):
        q, p = (
            Match("Person", alias="p")
            .where(p__name__in=["Alice", "Bob"])
            .return_("p")
            .build()
        )
        assert "p.name IN %s" in q
        assert p == (["Alice", "Bob"],)


# ---------------------------------------------------------------------------
# Create builder
# ---------------------------------------------------------------------------


class TestCreate:
    def test_simple_create(self):
        q, p = Create("Person", name="Alice", age=30).build()
        assert q == "CREATE (n:Person {name: %s, age: %s})"
        assert p == ("Alice", 30)

    def test_create_with_return(self):
        q, p = Create("Person", name="Alice").return_("n").build()
        assert q == "CREATE (n:Person {name: %s}) RETURN n"
        assert p == ("Alice",)

    def test_create_custom_alias(self):
        q, p = Create("Person", alias="p", name="Bob").return_("p").build()
        assert "(p:Person" in q

    def test_create_with_relationship(self):
        q, p = (
            Create("Person", name="Alice")
            .rel("KNOWS", "Person")
            .return_("n")
            .build()
        )
        assert "CREATE (n:Person {name: %s})-[:KNOWS]->(m0:Person)" in q
        assert p == ("Alice",)

    def test_create_rel_with_properties(self):
        q, p = (
            Create("Person", name="Alice")
            .rel("KNOWS", "Person", target_alias="b", since=2020)
            .build()
        )
        assert ":KNOWS {since: %s}" in q
        assert "(b:Person)" in q
        assert p == ("Alice", 2020)

    def test_create_incoming_rel(self):
        q, p = (
            Create("Person", name="Alice")
            .rel("FOLLOWS", "Person", direction="in", target_alias="b")
            .build()
        )
        assert "<-[:FOLLOWS]-(b:Person)" in q


# ---------------------------------------------------------------------------
# Merge builder
# ---------------------------------------------------------------------------


class TestMerge:
    def test_simple_merge(self):
        q, p = Merge("Person", name="Alice").build()
        assert q == "MERGE (n:Person {name: %s})"
        assert p == ("Alice",)

    def test_merge_on_create_set(self):
        q, p = (
            Merge("Person", name="Alice")
            .on_create(created_at="2024-01-01")
            .return_("n")
            .build()
        )
        assert "ON CREATE SET n.created_at = %s" in q
        assert p == ("Alice", "2024-01-01")

    def test_merge_on_match_set(self):
        q, p = (
            Merge("Person", name="Alice")
            .on_match(last_seen="2024-06-01")
            .build()
        )
        assert "ON MATCH SET n.last_seen = %s" in q

    def test_merge_all_set_variants(self):
        q, p = (
            Merge("Person", name="Alice")
            .on_create(created_at="2024-01-01")
            .on_match(updated_at="2024-06-01")
            .set(active=True)
            .return_("n")
            .build()
        )
        assert "ON CREATE SET" in q
        assert "ON MATCH SET" in q
        assert "SET n.active = %s" in q
        assert p == ("Alice", "2024-01-01", "2024-06-01", True)

    def test_merge_custom_alias(self):
        q, p = Merge("Person", alias="p", name="Bob").build()
        assert "(p:Person" in q

    def test_merge_with_return(self):
        q, p = Merge("Person", name="Alice").return_("n").build()
        assert q == "MERGE (n:Person {name: %s}) RETURN n"
