"""Tests for expression equality comparison.

All expression types must support ``==`` / ``!=`` and produce hashes
consistent with equality. Order of operands is irrelevant for ``In``,
``AllExpr``, and ``AnyExpr``.
"""

import re

import pytest

from therismos import (
    FALSE,
    TRUE,
    AllExpr,
    AnyExpr,
    Between,
    Eq,
    F,
    FalseExpr,
    Ge,
    Gt,
    In,
    IsNull,
    Le,
    Lt,
    Ne,
    NotExpr,
    Regex,
    TrueExpr,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_a = F("a")
_b = F("b")
_c = F("c")
_a_int = F("a", int)


# ---------------------------------------------------------------------------
# ComparisonExpr  (Eq, Ne, Lt, Le, Gt, Ge)
# ---------------------------------------------------------------------------


class TestComparisonExprEquality:
    """Equality for the six comparison expression types."""

    @pytest.mark.parametrize(
        "cls",
        [Eq, Ne, Lt, Le, Gt, Ge],
    )
    def test_same_type_same_field_same_value_is_equal(self, cls: type) -> None:
        """Two instances with identical type, field, and value are equal."""
        assert cls(_a, 5) == cls(_a, 5)

    @pytest.mark.parametrize(
        "cls",
        [Eq, Ne, Lt, Le, Gt, Ge],
    )
    def test_same_type_different_fields_is_not_equal(self, cls: type) -> None:
        """Instances on different fields are not equal."""
        assert cls(_a, 5) != cls(_b, 5)

    @pytest.mark.parametrize(
        "cls",
        [Eq, Ne, Lt, Le, Gt, Ge],
    )
    def test_same_type_different_values_is_not_equal(self, cls: type) -> None:
        """Instances with different values are not equal."""
        assert cls(_a, 5) != cls(_a, 9)

    @pytest.mark.parametrize(
        "left,right",
        [
            (Eq(_a, 5), Ne(_a, 5)),
            (Eq(_a, 5), Lt(_a, 5)),
            (Lt(_a, 5), Le(_a, 5)),
            (Gt(_a, 5), Ge(_a, 5)),
        ],
    )
    def test_different_comparison_types_are_not_equal(self, left: object, right: object) -> None:
        """Instances of different comparison types are never equal."""
        assert left != right

    @pytest.mark.parametrize(
        "cls",
        [Eq, Ne, Lt, Le, Gt, Ge],
    )
    def test_field_type_annotation_matters(self, cls: type) -> None:
        """Fields with the same name but different type_ are not equal."""
        assert cls(_a, 5) != cls(_a_int, 5)

    @pytest.mark.parametrize(
        "cls",
        [Eq, Ne, Lt, Le, Gt, Ge],
    )
    def test_hash_consistency_for_equal_instances(self, cls: type) -> None:
        """Equal instances must have equal hashes."""
        assert hash(cls(_a, 5)) == hash(cls(_a, 5))

    @pytest.mark.parametrize(
        "cls",
        [Eq, Ne, Lt, Le, Gt, Ge],
    )
    def test_not_equal_to_unrelated_type(self, cls: type) -> None:
        """Comparison expressions are not equal to arbitrary objects."""
        assert cls(_a, 5) != 42
        assert cls(_a, 5) != "x"
        assert cls(_a, 5) is not None


# ---------------------------------------------------------------------------
# Between
# ---------------------------------------------------------------------------


class TestBetweenEquality:
    """Equality for Between expressions."""

    def test_same_bounds_equal(self) -> None:
        """Same field and bounds produce equal instances."""
        assert Between(_a, 1, 10) == Between(_a, 1, 10)

    def test_different_lower_not_equal(self) -> None:
        """Different lower bound produces unequal instances."""
        assert Between(_a, 1, 10) != Between(_a, 2, 10)

    def test_different_upper_not_equal(self) -> None:
        """Different upper bound produces unequal instances."""
        assert Between(_a, 1, 10) != Between(_a, 1, 11)

    def test_different_field_not_equal(self) -> None:
        """Different field name produces unequal instances."""
        assert Between(_a, 1, 10) != Between(_b, 1, 10)

    def test_field_type_annotation_matters(self) -> None:
        """Fields with same name but different type_ are not equal."""
        assert Between(_a, 1, 10) != Between(_a_int, 1, 10)

    def test_hash_consistency(self) -> None:
        """Equal Between instances have equal hashes."""
        assert hash(Between(_a, 1, 10)) == hash(Between(_a, 1, 10))

    def test_not_equal_to_unrelated_type(self) -> None:
        """Between is not equal to arbitrary objects."""
        assert Between(_a, 1, 10) != Eq(_a, 5)


# ---------------------------------------------------------------------------
# Regex
# ---------------------------------------------------------------------------


class TestRegexEquality:
    """Equality for Regex expressions."""

    def test_same_pattern_no_flags_equal(self) -> None:
        """Same field and pattern without flags are equal."""
        assert Regex(_a, r"\d+") == Regex(_a, r"\d+")

    def test_same_pattern_with_flags_equal(self) -> None:
        """Same field, pattern, and flags are equal."""
        assert Regex(_a, r"\d+", re.IGNORECASE) == Regex(_a, r"\d+", re.IGNORECASE)

    def test_different_flags_not_equal(self) -> None:
        """Different flags produce unequal instances."""
        assert Regex(_a, r"\d+", re.IGNORECASE) != Regex(_a, r"\d+", re.MULTILINE)

    def test_none_flags_vs_flag_not_equal(self) -> None:
        """None flags vs. an actual flag produces unequal instances."""
        assert Regex(_a, r"\d+") != Regex(_a, r"\d+", re.IGNORECASE)

    def test_different_pattern_not_equal(self) -> None:
        """Different patterns produce unequal instances."""
        assert Regex(_a, r"\d+") != Regex(_a, r"\w+")

    def test_different_field_not_equal(self) -> None:
        """Different field names produce unequal instances."""
        assert Regex(_a, r"\d+") != Regex(_b, r"\d+")

    def test_hash_consistency(self) -> None:
        """Equal Regex instances have equal hashes."""
        assert hash(Regex(_a, r"\d+", re.IGNORECASE)) == hash(Regex(_a, r"\d+", re.IGNORECASE))


# ---------------------------------------------------------------------------
# In — order-insensitive values
# ---------------------------------------------------------------------------


class TestInEquality:
    """Equality for In expressions; values order must not matter."""

    def test_same_values_same_order_equal(self) -> None:
        """Same values in the same order are equal."""
        assert In(_a, (1, 2, 3)) == In(_a, (1, 2, 3))

    def test_same_values_different_order_equal(self) -> None:
        """Same values in different order are equal (core requirement)."""
        assert In(_a, (1, 2)) == In(_a, (2, 1))

    def test_same_values_all_permutations_equal(self) -> None:
        """All orderings of the same values are equal."""
        assert In(_a, (1, 2, 3)) == In(_a, (3, 1, 2))
        assert In(_a, (1, 2, 3)) == In(_a, (2, 3, 1))

    def test_different_values_not_equal(self) -> None:
        """Different value sets produce unequal instances."""
        assert In(_a, (1, 2)) != In(_a, (1, 3))

    def test_different_field_not_equal(self) -> None:
        """Different fields produce unequal instances."""
        assert In(_a, (1, 2)) != In(_b, (1, 2))

    def test_field_type_annotation_matters(self) -> None:
        """Fields with same name but different type_ are not equal."""
        assert In(_a, (1, 2)) != In(_a_int, (1, 2))

    def test_hash_consistency_same_order(self) -> None:
        """Equal In instances have equal hashes."""
        assert hash(In(_a, (1, 2))) == hash(In(_a, (1, 2)))

    def test_hash_consistency_different_order(self) -> None:
        """Order-swapped In instances have equal hashes."""
        assert hash(In(_a, (1, 2))) == hash(In(_a, (2, 1)))

    def test_not_equal_to_other_expr_types(self) -> None:
        """In is not equal to non-In expressions."""
        assert In(_a, (1, 2)) != Eq(_a, 1)


# ---------------------------------------------------------------------------
# IsNull
# ---------------------------------------------------------------------------


class TestIsNullEquality:
    """Equality for IsNull expressions."""

    def test_same_field_same_flag_equal(self) -> None:
        """Same field and is_null flag are equal."""
        assert IsNull(_a, is_null=True) == IsNull(_a, is_null=True)
        assert IsNull(_a, is_null=False) == IsNull(_a, is_null=False)

    def test_different_flag_not_equal(self) -> None:
        """Different is_null flag produces unequal instances."""
        assert IsNull(_a, is_null=True) != IsNull(_a, is_null=False)

    def test_different_field_not_equal(self) -> None:
        """Different fields produce unequal instances."""
        assert IsNull(_a, is_null=True) != IsNull(_b, is_null=True)

    def test_hash_consistency(self) -> None:
        """Equal IsNull instances have equal hashes."""
        assert hash(IsNull(_a, is_null=True)) == hash(IsNull(_a, is_null=True))


# ---------------------------------------------------------------------------
# AllExpr — order-insensitive sub-expressions
# ---------------------------------------------------------------------------


class TestAllExprEquality:
    """Equality for AllExpr (AND) expressions; sub-expression order must not matter."""

    def test_same_exprs_same_order_equal(self) -> None:
        """Same sub-expressions in same order are equal."""
        assert AllExpr(_a == 1, _b == 2) == AllExpr(_a == 1, _b == 2)

    def test_same_exprs_different_order_equal(self) -> None:
        """Same sub-expressions in different order are equal (core requirement)."""
        assert AllExpr(_a == 1, _b == 2) == AllExpr(_b == 2, _a == 1)

    def test_different_subexpr_not_equal(self) -> None:
        """Different sub-expression makes AllExpr unequal."""
        assert AllExpr(_a == 1, _b == 2) != AllExpr(_a == 1, _b == 3)

    def test_hash_consistency_same_order(self) -> None:
        """Equal AllExpr instances have equal hashes."""
        assert hash(AllExpr(_a == 1, _b == 2)) == hash(AllExpr(_a == 1, _b == 2))

    def test_hash_consistency_different_order(self) -> None:
        """Order-swapped AllExpr instances have equal hashes."""
        assert hash(AllExpr(_a == 1, _b == 2)) == hash(AllExpr(_b == 2, _a == 1))

    def test_different_length_not_equal(self) -> None:
        """AllExpr with different numbers of sub-expressions are not equal."""
        assert AllExpr(_a == 1, _b == 2) != AllExpr(_a == 1, _b == 2, _c == 3)

    def test_not_equal_to_anyexpr(self) -> None:
        """AllExpr and AnyExpr with identical children are not equal."""
        assert AllExpr(_a == 1, _b == 2) != AnyExpr(_a == 1, _b == 2)


# ---------------------------------------------------------------------------
# AnyExpr — order-insensitive sub-expressions
# ---------------------------------------------------------------------------


class TestAnyExprEquality:
    """Equality for AnyExpr (OR) expressions; sub-expression order must not matter."""

    def test_same_exprs_same_order_equal(self) -> None:
        """Same sub-expressions in same order are equal."""
        assert AnyExpr(_a == 1, _b == 2) == AnyExpr(_a == 1, _b == 2)

    def test_same_exprs_different_order_equal(self) -> None:
        """Same sub-expressions in different order are equal (core requirement)."""
        assert AnyExpr(_a == 1, _b == 2) == AnyExpr(_b == 2, _a == 1)

    def test_different_subexpr_not_equal(self) -> None:
        """Different sub-expression makes AnyExpr unequal."""
        assert AnyExpr(_a == 1, _b == 2) != AnyExpr(_a == 1, _c == 2)

    def test_hash_consistency_different_order(self) -> None:
        """Order-swapped AnyExpr instances have equal hashes."""
        assert hash(AnyExpr(_a == 1, _b == 2)) == hash(AnyExpr(_b == 2, _a == 1))

    def test_different_length_not_equal(self) -> None:
        """AnyExpr with different numbers of sub-expressions are not equal."""
        assert AnyExpr(_a == 1, _b == 2) != AnyExpr(_a == 1)


# ---------------------------------------------------------------------------
# NotExpr
# ---------------------------------------------------------------------------


class TestNotExprEquality:
    """Equality for NotExpr expressions."""

    def test_same_inner_expr_equal(self) -> None:
        """NotExpr wrapping identical inner expressions are equal."""
        assert NotExpr(_a == 1) == NotExpr(_a == 1)

    def test_different_inner_expr_not_equal(self) -> None:
        """NotExpr wrapping different inner expressions are not equal."""
        assert NotExpr(_a == 1) != NotExpr(_a == 2)

    def test_not_equal_to_inner_expr(self) -> None:
        """NotExpr is not equal to the bare inner expression."""
        assert NotExpr(_a == 1) != (_a == 1)

    def test_hash_consistency(self) -> None:
        """Equal NotExpr instances have equal hashes."""
        assert hash(NotExpr(_a == 1)) == hash(NotExpr(_a == 1))


# ---------------------------------------------------------------------------
# TrueExpr / FalseExpr
# ---------------------------------------------------------------------------


class TestTrueFalseEquality:
    """Equality for the TRUE and FALSE constant expressions."""

    def test_true_equals_true(self) -> None:
        """Any two TrueExpr instances are equal."""
        assert TrueExpr() == TrueExpr()
        assert TRUE == TRUE

    def test_false_equals_false(self) -> None:
        """Any two FalseExpr instances are equal."""
        assert FalseExpr() == FalseExpr()
        assert FALSE == FALSE

    def test_true_not_equal_to_false(self) -> None:
        """TrueExpr and FalseExpr are not equal."""
        assert TRUE != FALSE

    def test_hash_consistency(self) -> None:
        """Equal constant expressions have equal hashes."""
        assert hash(TrueExpr()) == hash(TrueExpr())
        assert hash(FalseExpr()) == hash(FalseExpr())


# ---------------------------------------------------------------------------
# Cross-type inequality
# ---------------------------------------------------------------------------


class TestCrossTypeInequality:
    """Expressions of entirely different types are never equal."""

    @pytest.mark.parametrize(
        "left,right",
        [
            (Eq(_a, 5), Between(_a, 1, 10)),
            (Eq(_a, 5), In(_a, (5,))),
            (Eq(_a, 5), IsNull(_a)),
            (Eq(_a, 5), Regex(_a, "5")),
            (Between(_a, 1, 5), In(_a, (1, 2, 3, 4))),
            (AllExpr(Eq(_a, 1)), AnyExpr(Eq(_a, 1))),
            (AllExpr(Eq(_a, 1)), NotExpr(Eq(_a, 1))),
            (Eq(_a, 1), TRUE),
            (Eq(_a, 1), FALSE),
        ],
    )
    def test_cross_type_not_equal(self, left: object, right: object) -> None:
        """Expressions of different concrete types are not equal."""
        assert left != right


# ---------------------------------------------------------------------------
# User requirement — the stated example
# ---------------------------------------------------------------------------


class TestUserRequirement:
    """Acceptance test: the specific requirement stated in the feature request."""

    def test_in_or_comparison_order_insensitive(self) -> None:
        """a IN (1, 2) OR b < 3  ==  b < 3 OR a IN (2, 1)."""
        a = F("a")
        b = F("b")
        expr1 = a.is_in(1, 2) | (b < 3)
        expr2 = (b < 3) | a.is_in(2, 1)
        assert expr1 == expr2

    def test_hash_equal_for_user_requirement(self) -> None:
        """Equal expressions from the user requirement have equal hashes."""
        a = F("a")
        b = F("b")
        expr1 = a.is_in(1, 2) | (b < 3)
        expr2 = (b < 3) | a.is_in(2, 1)
        assert hash(expr1) == hash(expr2)

    def test_and_order_insensitive(self) -> None:
        """a IN (1, 2) AND b < 3  ==  b < 3 AND a IN (2, 1)."""
        a = F("a")
        b = F("b")
        expr1 = a.is_in(1, 2) & (b < 3)
        expr2 = (b < 3) & a.is_in(2, 1)
        assert expr1 == expr2


# ---------------------------------------------------------------------------
# Hash consistency — parametrized
# ---------------------------------------------------------------------------


class TestHashConsistency:
    """Equal expression pairs always have equal hashes."""

    @pytest.mark.parametrize(
        "x,y",
        [
            (Eq(_a, 5), Eq(_a, 5)),
            (Ne(_a, 5), Ne(_a, 5)),
            (Lt(_a, 5), Lt(_a, 5)),
            (Le(_a, 5), Le(_a, 5)),
            (Gt(_a, 5), Gt(_a, 5)),
            (Ge(_a, 5), Ge(_a, 5)),
            (Between(_a, 1, 10), Between(_a, 1, 10)),
            (Regex(_a, r"\d+"), Regex(_a, r"\d+")),
            (In(_a, (1, 2)), In(_a, (2, 1))),
            (In(_a, (1, 2, 3)), In(_a, (3, 2, 1))),
            (IsNull(_a), IsNull(_a)),
            (AllExpr(Eq(_a, 1), Eq(_b, 2)), AllExpr(Eq(_b, 2), Eq(_a, 1))),
            (AnyExpr(Eq(_a, 1), Eq(_b, 2)), AnyExpr(Eq(_b, 2), Eq(_a, 1))),
            (NotExpr(Eq(_a, 1)), NotExpr(Eq(_a, 1))),
            (TrueExpr(), TrueExpr()),
            (FalseExpr(), FalseExpr()),
        ],
    )
    def test_hash_equals_when_exprs_equal(self, x: object, y: object) -> None:
        """If x == y then hash(x) == hash(y)."""
        assert x == y
        assert hash(x) == hash(y)  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# Usability in sets and dicts
# ---------------------------------------------------------------------------


class TestUsableInSets:
    """Equal expressions deduplicate correctly in sets and dict keys."""

    def test_equal_exprs_deduplicate_in_set(self) -> None:
        """Adding two equal expressions to a set yields one element."""
        s = {In(_a, (1, 2)), In(_a, (2, 1))}
        assert len(s) == 1

    def test_all_expr_deduplicates_in_set(self) -> None:
        """Order-swapped AllExpr deduplicates in a set."""
        s = {AllExpr(Eq(_a, 1), Eq(_b, 2)), AllExpr(Eq(_b, 2), Eq(_a, 1))}
        assert len(s) == 1

    def test_different_exprs_coexist_in_set(self) -> None:
        """Distinct expressions all appear in a set."""
        s = {Eq(_a, 1), Eq(_a, 2), Lt(_a, 1), In(_a, (1,))}
        assert len(s) == 4

    def test_expr_as_dict_key(self) -> None:
        """Expressions can be used as dict keys; equal keys resolve to same entry."""
        d: dict[object, str] = {}
        d[In(_a, (1, 2))] = "first"
        d[In(_a, (2, 1))] = "second"
        assert len(d) == 1
        assert d[In(_a, (1, 2))] == "second"
